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