Alert thresholds (#429)
* feat: add Graylog threshold alert route and schema for incident alerts * fix: correct spelling of ALERT_DESCRIPTION in Graylog schema and update alert creation logic
taylor_socfortress committed
Mar 24, 2025 at 08:49 UTC
8af07813b7e85653995ecd86f98cae437da3363c
4 files changed
+154
-2
backend/app/active_response/routes/graylog.py
+1
@@ -21,6 +21,7 @@ async def verify_graylog_header(graylog: str = Header(None)):
21
expected_header = os.getenv("GRAYLOG_API_HEADER_VALUE", "ab73de7a-6f61-4dde-87cd-3af5175a7281")
22
23
if graylog != expected_header:
24
+ logger.error("Invalid or missing Graylog header")
25
raise HTTPException(status_code=403, detail="Invalid or missing Graylog header")
26
return graylog
27
backend/app/active_response/schema/graylog.py
+93
@@ -29,6 +29,18 @@ class GraylogEventFields(BaseModel):
29
populate_by_name = True # Process alias fields
30
31
32
+class GraylogThresholdEventFields(BaseModel):
33
+ CUSTOMER_CODE: str
34
+ SOURCE: str
35
+ ALERT_DESCRIPTION: str
36
+ # Allow additional fields
37
+ additional_fields: Dict[str, Any] = Field(default_factory=dict, alias="__extra__")
38
+
39
+ class Config:
40
+ extra = "allow"
41
+ populate_by_name = True
42
+
43
+
44
class GraylogEvent(BaseModel):
45
id: str
46
event_definition_type: str
@@ -104,3 +116,84 @@ class GraylogEventNotification(BaseModel):
116
"backlog": [],
117
},
118
}
119
+
120
+
121
+class GraylogThresholdEvent(BaseModel):
122
+ id: str
123
+ event_definition_type: str
124
+ event_definition_id: str
125
+ origin_context: Optional[str] = None
126
+ timestamp: datetime
127
+ timestamp_processing: datetime
128
+ timerange_start: Optional[datetime] = None
129
+ timerange_end: Optional[datetime] = None
130
+ streams: List[str] = Field(default_factory=list)
131
+ source_streams: List[str]
132
+ message: str
133
+ source: str
134
+ key_tuple: List[Any] = Field(default_factory=list)
135
+ key: str
136
+ priority: int
137
+ scores: Dict[str, Any] = Field(default_factory=dict)
138
+ associated_assets: List[Any] = Field(default_factory=list)
139
+ alert: bool
140
+ fields: GraylogThresholdEventFields
141
+ group_by_fields: Dict[str, Any] = Field(default_factory=dict)
142
+ replay_info: ReplayInfo
143
+
144
+
145
+class GraylogThresholdEventNotification(BaseModel):
146
+ event_definition_id: str
147
+ event_definition_type: str
148
+ event_definition_title: str
149
+ event_definition_description: str = ""
150
+ job_definition_id: str
151
+ job_trigger_id: str
152
+ event: GraylogThresholdEvent
153
+ backlog: List[Any] = Field(default_factory=list)
154
+
155
+ class Config:
156
+ schema_extra = {
157
+ "example": {
158
+ "event_definition_id": "67b6687184088513bdc6cd1b",
159
+ "event_definition_type": "aggregation-v1",
160
+ "event_definition_title": "DELL SWITCHES - MULTIPLE AUTH FAILURES",
161
+ "event_definition_description": "DELL SWITCHES - MULTIPLE AUTH FAILURES",
162
+ "job_definition_id": "67dde9bc84088513bde5ce29",
163
+ "job_trigger_id": "67ddeabf84088513bde5d283",
164
+ "event": {
165
+ "id": "01JPXDSZ8AECWZ88HR9JQPYHJP",
166
+ "event_definition_type": "aggregation-v1",
167
+ "event_definition_id": "67b6687184088513bdc6cd1b",
168
+ "origin_context": None,
169
+ "timestamp": "2025-03-21T22:39:54.219Z",
170
+ "timestamp_processing": "2025-03-21T22:39:59.754Z",
171
+ "timerange_start": "2024-08-25T14:39:54.219Z",
172
+ "timerange_end": "2025-03-21T22:39:54.219Z",
173
+ "streams": [],
174
+ "source_streams": ["67abcb0a84088513bdc09e32"],
175
+ "message": "DELL SWITCHES - MULTIPLE AUTH FAILURES: 10.0.64.233 - count()=14.0",
176
+ "source": "soc-grlog02",
177
+ "key_tuple": [],
178
+ "key": "",
179
+ "priority": 2,
180
+ "scores": {},
181
+ "associated_assets": [],
182
+ "alert": True,
183
+ "fields": {
184
+ "CUSTOMER_CODE": "6bdd96a0-06a5-11f0-a499-005056b6c109",
185
+ "SOURCE": "DELLSWITCH",
186
+ "ALERT_DESCRIPTION": "THIS IS A TEST",
187
+ },
188
+ "group_by_fields": {"source": "10.0.64.233"},
189
+ "replay_info": {
190
+ "timerange_start": "2024-08-25T14:39:54.219Z",
191
+ "timerange_end": "2025-03-21T22:39:54.219Z",
192
+ "query": '"An invalid user tried to login"',
193
+ "streams": ["67abcb0a84088513bdc09e32"],
194
+ "filters": [],
195
+ },
196
+ },
197
+ "backlog": [],
198
+ },
199
+ }
backend/app/incidents/routes/incident_alert.py
+50
-1
@@ -4,6 +4,8 @@ from fastapi import Security
4
from loguru import logger
5
from sqlalchemy.ext.asyncio import AsyncSession
6
7
+from app.active_response.routes.graylog import verify_graylog_header
8
+from app.active_response.schema.graylog import GraylogThresholdEventNotification
9
from app.auth.utils import AuthHandler
10
from app.db.db_session import get_db
11
from app.incidents.schema.alert_collection import AlertsPayload
@@ -13,6 +15,7 @@ from app.incidents.schema.incident_alert import AutoCreateAlertResponse
15
from app.incidents.schema.incident_alert import CreateAlertRequest
16
from app.incidents.schema.incident_alert import CreateAlertRequestRoute
17
from app.incidents.schema.incident_alert import CreateAlertResponse
18
+from app.incidents.schema.incident_alert import CreatedAlertPayload
19
from app.incidents.schema.incident_alert import IndexNamesResponse
20
from app.incidents.services.alert_collection import add_copilot_alert_id
21
from app.incidents.services.alert_collection import get_alerts_not_created_in_copilot
@@ -20,6 +23,7 @@ from app.incidents.services.alert_collection import get_graylog_event_indices
23
from app.incidents.services.alert_collection import get_original_alert_id
24
from app.incidents.services.alert_collection import get_original_alert_index_name
25
from app.incidents.services.incident_alert import create_alert
26
+from app.incidents.services.incident_alert import create_alert_full
27
from app.incidents.services.incident_alert import get_single_alert_details
28
from app.incidents.services.incident_alert import retrieve_alert_timeline
29
@@ -175,4 +179,49 @@ async def create_alert_auto_route(
179
except Exception as e:
180
logger.error(f"Failed to create alert {alert} in CoPilot: {e}")
181
178
- return AutoCreateAlertResponse(success=True, message=f"{created_alerts_count} alerts created in CoPilot")
182
+
183
+@incidents_alerts_router.post(
184
+ "/create/threshold",
185
+ response_model=CreateAlertResponse,
186
+ description="Creates an incident alert in CoPilot for a Graylog configured threshold alert",
187
+ dependencies=[Depends(verify_graylog_header)],
188
+)
189
+async def invoke_alert_threshold_graylog_route(
190
+ request: GraylogThresholdEventNotification,
191
+ session: AsyncSession = Depends(get_db),
192
+) -> CreateAlertResponse:
193
+ """
194
+ This route accepts an HTTP Post from Graylog for any threshold alerts which needs a dedicated route
195
+ because there is no individual alert with an _id that we can use to grab from the
196
+ wazuh-indexer.
197
+ REQUIRED FILEDS:
198
+ 1. CUSTOMER_CODE: str - the customer code
199
+ 2. SOURCE: str - the source of the alert
200
+ 3. ALERT_DESCRIPTION: str - the description of the alert
201
+
202
+ # ! IMPORTANT: DO NOT ADD THE "COPILOT_ALERT_ID": "NONE" AS A CUSTOM FIELD WHEN CREATING THE ALERT IN GRAYLOG # !
203
+ # ! THIS WILL BREAK THE AUTO-ALERT CREATION FUNCTIONALITY # !
204
+
205
+ Args:
206
+ request (InvokeActiveResponseRequest): The request object containing the command, custom, arguments, and alert.
207
+
208
+ Returns:
209
+ CreateAlertResponse: The response object containing the result of the alert creation.
210
+ """
211
+ logger.info("Invoking alert threshold Graylog...")
212
+ logger.info(f"Timestamp: {request.event.timestamp}")
213
+ alert_id = await create_alert_full(
214
+ alert_payload=CreatedAlertPayload(
215
+ alert_context_payload=request.event.fields.dict(),
216
+ asset_payload=request.event.source,
217
+ timefield_payload=str(request.event.timestamp),
218
+ alert_title_payload=request.event.message,
219
+ source=request.event.fields.SOURCE,
220
+ index_name="gl-events_",
221
+ index_id=request.event.id,
222
+ ),
223
+ customer_code=request.event.fields.CUSTOMER_CODE,
224
+ session=session,
225
+ threshold_alert=True,
226
+ )
227
+ return CreateAlertResponse(success=True, message="Alert threshold Graylog invoked successfully", alert_id=alert_id)
backend/app/incidents/services/incident_alert.py
+10
-1
@@ -494,7 +494,12 @@ async def handle_customer_notifications(
494
)
495
496
497
-async def create_alert_full(alert_payload: CreatedAlertPayload, customer_code: str, session: AsyncSession) -> Alert:
497
+async def create_alert_full(
498
+ alert_payload: CreatedAlertPayload,
499
+ customer_code: str,
500
+ session: AsyncSession,
501
+ threshold_alert: bool = False,
502
+) -> Alert:
503
"""
504
Create an alert in CoPilot.
505
@@ -553,6 +558,10 @@ async def create_alert_full(alert_payload: CreatedAlertPayload, customer_code: s
558
session=session,
559
)
560
561
+ if threshold_alert is True:
562
+ logger.info(f"Threshold alert created for customer code {customer_code} with alert ID {alert_id}")
563
+ return alert_id
564
+
565
await add_alert_to_document(CreateAlertRequest(index_name=alert_payload.index_name, alert_id=alert_payload.index_id), alert_id)
566
567
return alert_id