-
Notifications
You must be signed in to change notification settings - Fork 857
Expand file tree
/
Copy pathsiem.py
More file actions
167 lines (126 loc) · 5.38 KB
/
Copy pathsiem.py
File metadata and controls
167 lines (126 loc) · 5.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# -*- coding: utf-8 -*-
"""Location: ./mcpgateway/routers/siem.py
Copyright contributors to the MCP-CONTEXT-FORGE project
SPDX-License-Identifier: Apache-2.0
SIEM admin API router.
"""
# Standard
import html
import logging
from typing import Any, Dict, List, Optional
# Third-Party
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, ConfigDict, Field
# First-Party
from mcpgateway.middleware.rbac import get_current_user_with_permissions, require_permission
from mcpgateway.services.siem_export_service import get_siem_export_service
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/admin/siem", tags=["SIEM"])
class DestinationFiltersRequest(BaseModel):
"""Optional destination filter config."""
severity: Optional[List[str]] = None
event_types: Optional[List[str]] = None
categories: Optional[List[str]] = None
class DestinationUpsertRequest(BaseModel):
"""Runtime SIEM destination configuration payload."""
name: str = Field(..., min_length=1)
type: str = Field(..., min_length=1)
enabled: bool = True
format: str = "json"
url: Optional[str] = None
host: Optional[str] = None
port: Optional[int] = None
protocol: Optional[str] = None
filters: Optional[DestinationFiltersRequest] = None
model_config = ConfigDict(extra="allow")
class DestinationBulkReplaceRequest(BaseModel):
"""Bulk replacement payload for SIEM destinations."""
destinations: List[DestinationUpsertRequest]
@router.get("/health")
@require_permission("admin.security_audit")
async def get_siem_health(_user=Depends(get_current_user_with_permissions)) -> Dict[str, Any]:
"""Get SIEM exporter health and per-destination delivery stats.
Returns:
Dict[str, Any]: Exporter health payload.
"""
service = get_siem_export_service()
return await service.get_health()
@router.get("/destinations")
@require_permission("admin.security_audit")
async def get_siem_destinations(_user=Depends(get_current_user_with_permissions)) -> Dict[str, Any]:
"""List current SIEM destination configuration (sensitive fields redacted).
Returns:
Dict[str, Any]: Destination list and exporter enablement state.
"""
service = get_siem_export_service()
return {
"enabled": service.enabled,
"destinations": service.list_destinations(),
}
@router.post("/destinations")
@require_permission("admin.security_audit")
async def add_siem_destination(payload: DestinationUpsertRequest, _user=Depends(get_current_user_with_permissions)) -> Dict[str, Any]:
"""Add one SIEM destination at runtime (no restart required).
Args:
payload: Destination configuration payload.
Returns:
Dict[str, Any]: Operation result with sanitized destination.
Raises:
HTTPException: If validation or persistence fails.
"""
service = get_siem_export_service()
try:
created = await service.add_destination(payload.model_dump(exclude_none=True))
except ValueError as exc:
raise HTTPException(status_code=400, detail=html.escape(str(exc))) from exc
except Exception as exc:
logger.error("Failed to add SIEM destination: %s", exc)
raise HTTPException(status_code=500, detail="Failed to add SIEM destination") from exc
return {
"status": "ok",
"destination": created,
}
@router.put("/destinations")
@require_permission("admin.security_audit")
async def replace_siem_destinations(payload: DestinationBulkReplaceRequest, _user=Depends(get_current_user_with_permissions)) -> Dict[str, Any]:
"""Replace full SIEM destination set at runtime.
Args:
payload: Replacement destination list payload.
Returns:
Dict[str, Any]: Operation result with sanitized destinations.
Raises:
HTTPException: If validation or persistence fails.
"""
service = get_siem_export_service()
try:
destinations = await service.replace_destinations([item.model_dump(exclude_none=True) for item in payload.destinations])
except ValueError as exc:
raise HTTPException(status_code=400, detail=html.escape(str(exc))) from exc
except Exception as exc:
logger.error("Failed to replace SIEM destinations: %s", exc)
raise HTTPException(status_code=500, detail="Failed to replace SIEM destinations") from exc
return {
"status": "ok",
"destinations": destinations,
}
@router.post("/test/{destination_name}")
@require_permission("admin.security_audit")
async def test_siem_destination(destination_name: str, _user=Depends(get_current_user_with_permissions)) -> Dict[str, Any]:
"""Send a test event to one destination.
Args:
destination_name: Destination identifier to test.
Returns:
Dict[str, Any]: Delivery test result.
Raises:
HTTPException: If destination is missing or test fails unexpectedly.
"""
# Sanitize destination_name to prevent XSS (CWE-79)
sanitized_name = html.escape(destination_name)
service = get_siem_export_service()
try:
return await service.test_destination(sanitized_name)
except KeyError as exc:
raise HTTPException(status_code=404, detail=html.escape(str(exc))) from exc
except Exception as exc:
logger.error("Failed SIEM destination test for %s: %s", sanitized_name, exc)
raise HTTPException(status_code=500, detail="Failed to test SIEM destination") from exc