@cryptotaxi247 / CoPilot / commits / 8af07813

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