1
+import json
2
+from typing import Optional
3
+from typing import Set
4
+
5
+from fastapi import HTTPException
6
+from loguru import logger
7
+from sqlalchemy.ext.asyncio import AsyncSession
8
+
9
+from app.agents.routes.agents import get_agent
10
+from app.agents.schema.agents import AgentsResponse
11
+from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
12
+from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
13
+from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
14
+from app.db.universal_models import CustomersMeta
15
+from app.integrations.alert_creation.general.schema.alert import CreateAlertRequest
16
+from app.integrations.alert_creation.general.schema.alert import IrisAsset
17
+from app.integrations.alert_creation.general.schema.alert import IrisIoc
18
+from app.integrations.alert_creation.general.schema.alert import ValidIocFields
19
+from app.integrations.alert_creation.general.services.alert_multi_exclude import (
20
+ AlertDetailsService,
21
+)
22
+from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
23
+from app.integrations.monitoring_alert.schema.monitoring_alert import (
24
+ FilterAlertsRequest,
25
+)
26
+from app.integrations.monitoring_alert.schema.monitoring_alert import WazuhAlertModel
27
+from app.integrations.monitoring_alert.schema.monitoring_alert import (
28
+ WazuhAnalysisResponse,
29
+)
30
+from app.integrations.monitoring_alert.schema.monitoring_alert import (
31
+ WazuhIrisAlertContext,
32
+)
33
+from app.integrations.monitoring_alert.schema.monitoring_alert import (
34
+ WazuhIrisAlertPayload,
35
+)
36
+from app.integrations.utils.alerts import get_asset_type_id
37
+from app.integrations.utils.alerts import validate_ioc_type
38
+from app.utils import get_customer_alert_settings
39
+
40
+
41
+def valid_ioc_fields() -> Set[str]:
42
+ """
43
+ Getter for the set of valid IoC fields.
44
+ Returns
45
+ -------
46
+ Set[str]
47
+ The set of valid IoC fields.
48
+ """
49
+ return {field.value for field in ValidIocFields}
50
+
51
+
52
+async def construct_alert_source_link(alert_details: CreateAlertRequest, session: AsyncSession) -> str:
53
+ """
54
+ Construct the alert source link for the alert details.
55
+ Parameters
56
+ ----------
57
+ alert_details: CreateAlertRequest
58
+ The alert details.
59
+ Returns
60
+ -------
61
+ str
62
+ The alert source link.
63
+ """
64
+ # Check if the alert has a process id and that it is not "No process ID found"
65
+ if hasattr(alert_details, "process_id") and alert_details.process_id != "No process ID found":
66
+ query_string = f"%22query%22:%22process_id:%5C%22{alert_details.process_id}%5C%22%20AND%20"
67
+ else:
68
+ query_string = f"%22query%22:%22_id:%5C%22{alert_details.id}%5C%22%20AND%20"
69
+
70
+ grafana_url = (await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)).grafana_url
71
+
72
+ return (
73
+ f"{grafana_url}/explore?left=%5B%22now-6h%22,%22now%22,%22WAZUH%22,%7B%22refId%22:%22A%22,"
74
+ f"{query_string}"
75
+ f"agent_name:%5C%22{alert_details.agent_name}%5C%22%22,"
76
+ "%22alias%22:%22%22,%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:%7B%22limit%22:%22500%22%7D%7D%5D,"
77
+ "%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D"
78
+ )
79
+
80
+
81
+async def build_ioc_payload(alert_details: CreateAlertRequest) -> Optional[IrisIoc]:
82
+ """
83
+ Builds an IoC payload based on the provided alert details.
84
+
85
+ Args:
86
+ alert_details (CreateAlertRequest): The details of the alert.
87
+
88
+ Returns:
89
+ Optional[IrisIoc]: The constructed IoC payload, or None if no valid IoC fields are found.
90
+ """
91
+ for field in valid_ioc_fields():
92
+ if hasattr(alert_details, field):
93
+ ioc_value = getattr(alert_details, field)
94
+ ioc_type = await validate_ioc_type(ioc_value=ioc_value)
95
+ return IrisIoc(
96
+ ioc_value=ioc_value,
97
+ ioc_description="IoC found in alert",
98
+ ioc_tlp_id=1,
99
+ ioc_type_id=ioc_type,
100
+ )
101
+ return None
102
+
103
+
104
+async def build_asset_payload(agent_data: AgentsResponse, alert_details: CreateAlertRequest, session: AsyncSession) -> IrisAsset:
105
+ """
106
+ Build the payload for an IrisAsset object based on the agent data and alert details.
107
+
108
+ Args:
109
+ agent_data (AgentsResponse): The response containing agent data.
110
+ alert_details: The details of the alert.
111
+
112
+ Returns:
113
+ IrisAsset: The constructed IrisAsset object.
114
+ """
115
+ # Get the agent_id based on the hostname from the Agents table
116
+ if agent_data.success:
117
+ return IrisAsset(
118
+ asset_name=agent_data.agents[0].hostname,
119
+ asset_ip=agent_data.agents[0].ip_address,
120
+ asset_description=await construct_alert_source_link(alert_details, session=session),
121
+ asset_type_id=await get_asset_type_id(agent_data.agents[0].os),
122
+ asset_tags=f"agent_id:{agent_data.agents[0].agent_id}",
123
+ )
124
+ return IrisAsset()
125
+
126
+
127
+async def fetch_wazuh_indexer_details(alert_id: str, index: str) -> WazuhAlertModel:
128
+ """
129
+ Fetch the Wazuh alert details from the Wazuh-Indexer.
130
+
131
+ Args:
132
+ alert_id (str): The alert ID.
133
+ index (str): The index.
134
+
135
+ Returns:
136
+ CollectAlertsResponse: The response from the Wazuh-Indexer.
137
+ """
138
+ logger.info(f"Fetching Wazuh alert details for alert_id: {alert_id} and index: {index}")
139
+
140
+ es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
141
+ response = es_client.get(index=index, id=alert_id)
142
+
143
+ return WazuhAlertModel(**response)
144
+
145
+
146
+async def fetch_alert_details(alert: MonitoringAlerts) -> WazuhAlertModel:
147
+ logger.info(f"Analyzing Wazuh alert: {alert.alert_id}")
148
+ alert_details = await fetch_wazuh_indexer_details(alert.alert_id, alert.alert_index)
149
+ logger.info(f"Alert details: {alert_details}")
150
+ return alert_details
151
+
152
+
153
+async def check_event_exclusion(alert_details: WazuhAlertModel, alert_detail_service: AlertDetailsService, session: AsyncSession):
154
+ event_exclude_result = await alert_detail_service.collect_alert_timeline_process_id(
155
+ agent_name=alert_details._source["agent_name"],
156
+ process_id=getattr(alert_details._source, "process_id", "n/a"),
157
+ index=alert_details._index,
158
+ session=session,
159
+ )
160
+ if event_exclude_result is True:
161
+ raise HTTPException(
162
+ status_code=400,
163
+ detail="Alert excluded due to multi exclusion as set in the config.ini file.",
164
+ )
165
+ logger.info("Alert is not excluded due to multi exclusion.")
166
+
167
+
168
+async def check_if_open_alert_exists_in_iris(alert_details: WazuhAlertModel) -> list:
169
+ """
170
+ Check if the alert exists in IRIS.
171
+
172
+ Args:
173
+ alert_details (WazuhAlertModel): The alert details.
174
+ session (AsyncSession): The database session.
175
+
176
+ Returns:
177
+ bool: True if the alert exists in IRIS, False otherwise.
178
+ """
179
+ client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
180
+ request = FilterAlertsRequest(alert_tags=alert_details._source["rule_id"])
181
+ params = construct_params(request)
182
+ alert_exists = await fetch_and_validate_data(client, lambda: alert_client.filter_alerts(**params))
183
+ logger.info(f"Alert exists: {alert_exists['data']['alerts']}")
184
+ return alert_exists["data"]["alerts"][0]["alert_id"] if alert_exists["data"]["alerts"] else []
185
+
186
+
187
+def construct_params(request: FilterAlertsRequest) -> dict:
188
+ """
189
+ Constructs the parameters for the alert filtering request.
190
+
191
+ Args:
192
+ request (FilterAlertsRequest): The request object containing filtering criteria.
193
+
194
+ Returns:
195
+ dict: A dictionary of parameters for the alert filtering request.
196
+ """
197
+ params = {
198
+ "page": request.page,
199
+ "per_page": request.per_page,
200
+ "sort": request.sort,
201
+ "alert_tags": request.alert_tags,
202
+ "alert_status_id": request.alert_status_id,
203
+ # Add more parameters here as needed
204
+ }
205
+
206
+ # Remove parameters that have a value of None
207
+ return {k: v for k, v in params.items() if v is not None}
208
+
209
+
210
+async def build_alert_context_payload(
211
+ alert_details: CreateAlertRequest,
212
+ agent_data: AgentsResponse,
213
+ session: AsyncSession,
214
+) -> WazuhIrisAlertContext:
215
+ """
216
+ Builds the payload for the alert context.
217
+
218
+ Args:
219
+ alert_details (CreateAlertRequest): The details of the alert.
220
+ agent_data (AgentsResponse): The agent data.
221
+ session (AsyncSession): The async session.
222
+
223
+ Returns:
224
+ WazuhIrisAlertContext: The built alert context payload.
225
+ """
226
+ return WazuhIrisAlertContext(
227
+ customer_iris_id=(
228
+ await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
229
+ ).iris_customer_id,
230
+ customer_name=(await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)).customer_name,
231
+ customer_cases_index=(
232
+ await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
233
+ ).iris_index,
234
+ alert_name=alert_details.rule_description,
235
+ alert_level=alert_details.rule_level,
236
+ rule_id=alert_details.rule_id,
237
+ rule_mitre_id=getattr(alert_details, "rule_mitre_id", "No rule mitre id found"),
238
+ rule_mitre_tactic=getattr(
239
+ alert_details,
240
+ "rule_mitre_tactic",
241
+ "No rule mitre tactic found",
242
+ ),
243
+ rule_mitre_technique=getattr(
244
+ alert_details,
245
+ "rule_mitre_technique",
246
+ "No rule mitre technique found",
247
+ ),
248
+ )
249
+
250
+
251
+async def build_alert_payload(
252
+ alert_details: CreateAlertRequest,
253
+ agent_data,
254
+ ioc_payload: Optional[IrisIoc],
255
+ session: AsyncSession,
256
+) -> WazuhIrisAlertPayload:
257
+ """
258
+ Builds the payload for an alert based on the provided alert details, agent data, IoC payload, and session.
259
+
260
+ Args:
261
+ alert_details (CreateAlertRequest): The details of the alert.
262
+ agent_data: The agent data associated with the alert.
263
+ ioc_payload (Optional[IrisIoc]): The IoC payload associated with the alert.
264
+ session (AsyncSession): The session used for database operations.
265
+
266
+ Returns:
267
+ WazuhIrisAlertPayload: The built alert payload.
268
+ """
269
+ asset_payload = await build_asset_payload(agent_data, alert_details=alert_details, session=session)
270
+ context_payload = await build_alert_context_payload(alert_details=alert_details, agent_data=agent_data, session=session)
271
+ timefield = (await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)).timefield
272
+ # Get the timefield value from the alert_details
273
+ if hasattr(alert_details, timefield):
274
+ alert_details.time_field = getattr(alert_details, timefield)
275
+ logger.info(f"Alert has context: {context_payload}")
276
+ if ioc_payload:
277
+ logger.info(f"Alert has IoC: {ioc_payload}")
278
+ return WazuhIrisAlertPayload(
279
+ alert_title=alert_details.rule_description,
280
+ alert_description=alert_details.rule_description,
281
+ alert_source="COPILOT WAZUH ANALYSIS",
282
+ assets=[asset_payload],
283
+ alert_status_id=3,
284
+ alert_severity_id=5,
285
+ alert_customer_id=(
286
+ await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
287
+ ).iris_customer_id,
288
+ alert_source_content=alert_details.to_dict(),
289
+ alert_context=context_payload,
290
+ alert_iocs=[ioc_payload],
291
+ alert_source_event_time=alert_details.time_field,
292
+ )
293
+ else:
294
+ logger.info("Alert does not have IoC")
295
+ return WazuhIrisAlertPayload(
296
+ alert_title=alert_details.rule_description,
297
+ alert_description=alert_details.rule_description,
298
+ alert_source="COPILOT WAZUH ANALYSIS",
299
+ assets=[asset_payload],
300
+ alert_status_id=3,
301
+ alert_severity_id=5,
302
+ alert_customer_id=(
303
+ await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
304
+ ).iris_customer_id,
305
+ alert_source_content=alert_details.to_dict(),
306
+ alert_context=context_payload,
307
+ alert_source_event_time=alert_details.time_field,
308
+ )
309
+
310
+
311
+async def create_alert_details(alert_details: WazuhAlertModel) -> CreateAlertRequest:
312
+ """
313
+ Create an alert details object from the Wazuh alert details.
314
+
315
+ Args:
316
+ alert_details (WazuhAlertModel): The Wazuh alert details.
317
+
318
+ Returns:
319
+ CreateAlertRequest: The alert details object.
320
+ """
321
+ return CreateAlertRequest(
322
+ index=alert_details._index,
323
+ id=alert_details._id,
324
+ rule_id=alert_details._source["rule_id"],
325
+ rule_level=alert_details._source["rule_level"],
326
+ rule_description=alert_details._source["rule_description"],
327
+ agent_name=alert_details._source["agent_name"],
328
+ agent_ip=alert_details._source["agent_ip"],
329
+ agent_id=alert_details._source["agent_id"],
330
+ agent_labels_customer=alert_details._source["agent_labels_customer"],
331
+ timestamp=alert_details._source["timestamp"],
332
+ timestamp_utc=alert_details._source["timestamp_utc"],
333
+ process_id=alert_details._source.get("process_id", "No process ID found"),
334
+ )
335
+
336
+
337
+async def create_and_update_alert_in_iris(alert_details: WazuhAlertModel, session: AsyncSession) -> int:
338
+ """
339
+ Creates the alert, then updates the alert with the asset and IoC if available.
340
+
341
+ Args:
342
+ alert_details (WazuhAlertModel): The details of the alert.
343
+ session (AsyncSession): The async session object.
344
+
345
+ Returns:
346
+ int: The ID of the created alert in IRIS.
347
+ """
348
+ logger.info("Alert does not exist in IRIS. Creating alert.")
349
+ alert_details = await create_alert_details(alert_details)
350
+ agent_details = await get_agent(alert_details.agent_id, session)
351
+ ioc_payload = await build_ioc_payload(alert_details)
352
+ iris_alert_payload = await build_alert_payload(
353
+ alert_details=alert_details,
354
+ agent_data=agent_details,
355
+ ioc_payload=ioc_payload,
356
+ session=session,
357
+ )
358
+ client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
359
+ result = await fetch_and_validate_data(
360
+ client,
361
+ alert_client.add_alert,
362
+ iris_alert_payload.to_dict(),
363
+ )
364
+ alert_id = result["data"]["alert_id"]
365
+ logger.info(f"Successfully created alert {alert_id} in IRIS.")
366
+ await fetch_and_validate_data(
367
+ client,
368
+ alert_client.update_alert,
369
+ alert_id,
370
+ {"alert_tags": f"{alert_details.rule_id}"},
371
+ )
372
+ # Update the alert with the asset payload
373
+ await fetch_and_validate_data(
374
+ client,
375
+ alert_client.update_alert,
376
+ alert_id,
377
+ {"assets": [dict(IrisAsset(**iris_alert_payload.assets[0].to_dict()))]},
378
+ )
379
+ if ioc_payload:
380
+ await fetch_and_validate_data(
381
+ client,
382
+ alert_client.update_alert,
383
+ alert_id,
384
+ {"iocs": [dict(IrisIoc(**iris_alert_payload.alert_iocs[0].to_dict()))]},
385
+ )
386
+ return alert_id
387
+
388
+
389
+async def get_current_assets(client, alert_client, iris_alert_id):
390
+ result = await fetch_and_validate_data(client, alert_client.get_alert, iris_alert_id)
391
+ return result["data"]["assets"]
392
+
393
+
394
+async def update_alert_with_assets(client, alert_client, iris_alert_id, current_assets):
395
+ await fetch_and_validate_data(
396
+ client,
397
+ alert_client.update_alert,
398
+ iris_alert_id,
399
+ {"assets": current_assets},
400
+ )
401
+
402
+
403
+async def remove_duplicate_assets(current_assets):
404
+ """
405
+ Removes duplicate assets from the given list of current_assets.
406
+
407
+ Args:
408
+ current_assets (list): A list of dictionaries representing current assets.
409
+
410
+ Returns:
411
+ list: A list of dictionaries with duplicate assets removed.
412
+ """
413
+ current_assets = list({d["asset_name"]: d for d in current_assets}.values())
414
+ current_assets_str = [json.dumps(d, sort_keys=True) for d in current_assets]
415
+ current_assets_str = list(set(current_assets_str))
416
+ current_assets = [json.loads(s) for s in current_assets_str]
417
+ return current_assets
418
+
419
+
420
+async def analyze_wazuh_alerts(
421
+ monitoring_alerts: MonitoringAlerts,
422
+ customer_meta: CustomersMeta,
423
+ session: AsyncSession,
424
+) -> WazuhAnalysisResponse:
425
+ """
426
+ Analyze the given Wazuh alerts and create an alert if necessary. Otherwise update the existing alert with the asset.
427
+
428
+ 1. For each alert, extract the metadata from the Wazuh-Indexer.
429
+ 2. Check if the alert exists in IRIS. If it does, update the alert with the asset. If it does not, create the alert in IRIS.
430
+ The alert will contain the asset and IoC if available.
431
+ 3. Get the current list of assets from the alert to avoid overwriting them.
432
+
433
+ Args:
434
+ monitoring_alerts (MonitoringAlerts): The monitoring alert details.
435
+ session (AsyncSession): The database session.
436
+
437
+ Returns:
438
+ WazuhAnalysisResponse: The analysis response.
439
+ """
440
+ logger.info(f"Analyzing Wazuh alerts with customer_meta: {customer_meta}")
441
+ alert_detail_service = await AlertDetailsService.create()
442
+ for alert in monitoring_alerts:
443
+ alert_details = await fetch_alert_details(alert)
444
+ await check_event_exclusion(alert_details, alert_detail_service, session)
445
+ iris_alert_id = await check_if_open_alert_exists_in_iris(alert_details)
446
+ if iris_alert_id == []:
447
+ logger.info(f"Alert {alert_details._id} does not exist in IRIS. Creating alert.")
448
+ await create_and_update_alert_in_iris(alert_details, session)
449
+ else:
450
+ logger.info(f"Alert {iris_alert_id} exists in IRIS. Updating alert with the asset.")
451
+ # Fetch the current list of assets from the alert to avoid overwriting them
452
+ client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
453
+ current_assets = await get_current_assets(client, alert_client, iris_alert_id)
454
+ alert_details = await create_alert_details(alert_details)
455
+ agent_details = await get_agent(alert_details.agent_id, session)
456
+ asset_payload = await build_asset_payload(agent_data=agent_details, alert_details=alert_details, session=session)
457
+ current_assets.append(dict(IrisAsset(**asset_payload.to_dict())))
458
+ current_assets = await remove_duplicate_assets(current_assets)
459
+ await update_alert_with_assets(client, alert_client, iris_alert_id, current_assets)
460
+
461
+ return WazuhAnalysisResponse(
462
+ success=True,
463
+ message="Wazuh alerts analyzed successfully",
464
+ )