main
py 1,415 lines 52.7 KB
Raw
1 import os
2 import re
3 from datetime import datetime
4 from datetime import timedelta
5 from typing import Any
6 from typing import Dict
7 from typing import List
8 from typing import Optional
9
10 from fastapi import HTTPException
11 from loguru import logger
12 from sqlalchemy.ext.asyncio import AsyncSession
13 from sqlalchemy.future import select
14 from sqlalchemy.sql import func
15
16 from app.connectors.shuffle.schema.integrations import ExecuteWorkflowRequest
17 from app.connectors.shuffle.services.integrations import execute_workflow
18 from app.connectors.talon.schema.talon import TalonInvestigateRequest
19 from app.connectors.talon.services.talon import (
20 investigate_alert as talon_investigate_alert,
21 )
22 from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
23 from app.connectors.wazuh_indexer.utils.universal import (
24 create_wazuh_indexer_client_async,
25 )
26 from app.db.universal_models import Agents
27 from app.incidents.models import Alert
28 from app.incidents.models import AlertContext
29 from app.incidents.models import AlertToIoC
30 from app.incidents.models import Asset
31 from app.incidents.models import IoC
32 from app.incidents.routes.db_operations import get_configured_sources
33 from app.incidents.schema.db_operations import AlertIoCCreate
34 from app.incidents.schema.db_operations import AlertIocValue
35 from app.incidents.schema.incident_alert import CreateAlertRequest
36 from app.incidents.schema.incident_alert import CreateAlertRequestRoute
37 from app.incidents.schema.incident_alert import CreateAlertResponse
38 from app.incidents.schema.incident_alert import CreatedAlertPayload
39 from app.incidents.schema.incident_alert import FieldNames
40 from app.incidents.schema.incident_alert import GenericAlertModel
41 from app.incidents.schema.incident_alert import GenericSourceModel
42 from app.incidents.services.db_operations import get_alert_title_names
43 from app.incidents.services.db_operations import get_asset_names
44 from app.incidents.services.db_operations import get_customer_ai_trigger
45 from app.incidents.services.db_operations import get_customer_notification
46 from app.incidents.services.db_operations import get_field_names
47 from app.incidents.services.db_operations import get_ioc_names
48 from app.incidents.services.db_operations import get_timefield_names
49 from app.incidents.services.threshold_alert import retrieve_threshold_alert_timeline
50 from app.integrations.alert_creation_settings.models.alert_creation_settings import (
51 AlertCreationSettings,
52 )
53 from app.integrations.alert_escalation.schema.escalate_alert import CustomerCodeKeys
54 from app.integrations.routes import get_customer_by_auth_key
55
56
57 async def fetch_settings(field: str, value: str, session: AsyncSession, case_insensitive: bool = False):
58 """
59 Fetch settings based on the field and value.
60
61 Args:
62 field (str): The field to check.
63 value (str): The value to check.
64 session (AsyncSession): The database session.
65 case_insensitive (bool): Whether to perform case-insensitive comparison.
66
67 Returns:
68 AlertCreationSettings: The settings if found, None otherwise.
69 """
70 logger.info(f"Checking if {field}: {value} is valid (case_insensitive={case_insensitive}).")
71
72 if case_insensitive:
73 result = await session.execute(
74 select(AlertCreationSettings).where(
75 func.lower(getattr(AlertCreationSettings, field)) == value.lower(),
76 ),
77 )
78 else:
79 result = await session.execute(
80 select(AlertCreationSettings).where(
81 getattr(AlertCreationSettings, field) == value,
82 ),
83 )
84
85 settings = result.scalars().first()
86 logger.info(f"Settings: {settings}")
87 return settings
88
89
90 async def is_customer_code_valid(customer_code: str, session: AsyncSession) -> AlertCreationSettings:
91 """
92 Check if the customer code is valid.
93
94 Args:
95 customer_code (str): The customer code to check.
96 session (AsyncSession): The database session.
97
98 Returns:
99 bool: True if the customer code is valid, False otherwise.
100 """
101 settings = await fetch_settings("customer_code", customer_code, session)
102
103 if settings:
104 return settings
105
106 # If no settings found customer_code, try with the lowered customer_name
107 normalized_code = customer_code.lower().replace("_", " ")
108 settings = await fetch_settings("customer_name", normalized_code, session, case_insensitive=True)
109
110 if settings:
111 return settings
112
113 # If no settings found with customer_code, try with office365_organization_id
114 settings = await fetch_settings("office365_organization_id", customer_code, session)
115
116 if settings:
117 return settings
118
119 raise HTTPException(
120 status_code=400,
121 detail=f"Customer code {customer_code} is not valid. Has the customer been provisioned?",
122 )
123
124
125 def clean_alert_title(title: str) -> str:
126 """
127 Clean the alert title by removing BOM characters and normalizing encoding.
128
129 Args:
130 title (str): The raw alert title
131
132 Returns:
133 str: The cleaned alert title
134 """
135 if not title:
136 return title
137
138 # Remove BOM characters
139 title = title.replace("\ufeff", "") # UTF-8 BOM
140 title = title.replace("\ufffe", "") # UTF-16 BE BOM
141 title = title.replace("\xff\xfe", "") # UTF-16 LE BOM
142
143 # Strip whitespace and normalize
144 title = title.strip()
145
146 # Ensure proper UTF-8 encoding
147 if isinstance(title, str):
148 title = title.encode("utf-8", "ignore").decode("utf-8")
149
150 return title
151
152
153 async def update_alert_creation_time(
154 alert_id: int,
155 timefield_payload: Optional[str],
156 session: AsyncSession,
157 ) -> None:
158 """Update an existing alert's creation time to the latest trigger time."""
159 alert_obj = await session.get(Alert, alert_id)
160 if not alert_obj:
161 return
162 if timefield_payload:
163 try:
164 alert_obj.alert_creation_time = datetime.fromisoformat(timefield_payload)
165 except (ValueError, TypeError):
166 alert_obj.alert_creation_time = datetime.utcnow()
167 else:
168 alert_obj.alert_creation_time = datetime.utcnow()
169 session.add(alert_obj)
170 await session.commit()
171 logger.info(f"Updated alert_creation_time for alert ID {alert_id} to {alert_obj.alert_creation_time}")
172
173
174 async def get_single_alert_details(
175 alert_details: CreateAlertRequest,
176 ) -> GenericAlertModel:
177 """
178 Fetches the details of a single alert.
179
180 Args:
181 alert_details (CreateAlertRequest): The details of the alert to fetch.
182
183 Returns:
184 GenericAlertModel: The model representing the fetched alert.
185
186 Raises:
187 HTTPException: If there is an error while fetching the alert details.
188 """
189 logger.info(
190 f"Fetching alert details for alert {alert_details.alert_id} in index {alert_details.index_name}",
191 )
192 es_client = await create_wazuh_indexer_client_async("Wazuh-Indexer")
193 try:
194 alert = await es_client.get(index=alert_details.index_name, id=alert_details.alert_id)
195 source_model = GenericSourceModel(**alert["_source"])
196 syslog_type = getattr(source_model, "syslog_type", None)
197 if syslog_type is None:
198 syslog_type = getattr(source_model, "integration", None)
199 if syslog_type is None:
200 raise HTTPException(status_code=400, detail="Neither syslog_type nor integration field found in source_model")
201 return GenericAlertModel(
202 _source=source_model,
203 _id=alert["_id"],
204 _index=alert["_index"],
205 _version=alert["_version"],
206 syslog_type=syslog_type,
207 )
208 except Exception as e:
209 logger.debug(f"Failed to collect alert details: {e}")
210 raise HTTPException(
211 status_code=400,
212 detail=f"Failed to collect alert details: {e}",
213 )
214
215
216 def remove_process_name_if_osquery(source_dict: dict) -> None:
217 """
218 Remove the process_name field from the source dictionary if rule_group1 is 'osquery'.
219
220 Args:
221 source_dict (dict): The source dictionary.
222 """
223 rule_group1 = source_dict.get("rule_group1")
224 if rule_group1 == "osquery":
225 logger.info("Removing process_name field")
226 source_dict.pop("process_name", None)
227
228
229 def get_process_image(source_dict: dict) -> str:
230 """
231 Get the process_image field from the source dictionary.
232
233 Args:
234 source_dict (dict): The source dictionary.
235
236 Returns:
237 str: The process image.
238 """
239 process_image = source_dict.get("process_image")
240 if not process_image:
241 process_image = source_dict.get("data_win_eventdata_image")
242 if not process_image:
243 process_image = source_dict.get("data_event_Image")
244 if not process_image:
245 process_image = source_dict.get("data_win_eventdata_sourceImage")
246 logger.info(f"Process image: {process_image}")
247 return process_image
248
249
250 def get_process_name_from_image(process_image: str) -> str:
251 """
252 Get the process name from the process image.
253
254 Args:
255 process_image (str): The process image.
256
257 Returns:
258 str: The process name.
259 """
260 process_name = os.path.basename(process_image) if process_image else None
261 logger.info(f"Process name: {process_name}")
262 return process_name
263
264
265 async def construct_soc_alert_url(root_url: str, soc_alert_id: int) -> str:
266 """Constructs the full URL for the SOC alert.
267
268 Args:
269 root_url (str): The root URL of the SOC alert system.
270 soc_alert_id (int): The ID of the SOC alert.
271
272 Returns:
273 str: The full URL for the SOC alert.
274
275 """
276 url_path = f"/alerts?cid=1&page=1&per_page=10&sort=desc&alert_ids={soc_alert_id}"
277 return f"{root_url}{url_path}"
278
279
280 async def get_customer_code(alert_details: dict, session: AsyncSession = None):
281 """
282 Fetch the customer code from alert details.
283
284 For Office365 organization IDs, uses the integration auth key lookup service.
285 For other customer code types, uses the standard lookup process.
286
287 Args:
288 alert_details (dict): The alert details dictionary containing potential customer code fields
289 session (AsyncSession, optional): Database session for integration lookups
290
291 Returns:
292 str: The customer code
293
294 Raises:
295 HTTPException: If no valid customer code can be found
296 """
297 logger.info(f"Fetching customer code for alert {alert_details}")
298
299 # Iterate over the possible keys and return the value if the key is present
300 for key in CustomerCodeKeys:
301 logger.info(f"Checking for key {key.value}")
302 if key.value in alert_details:
303 value = alert_details[key.value]
304
305 # Handle Office365 OrganizationId specially - lookup from integrations
306 if key == CustomerCodeKeys.DATA_OFFICE365_ORGANIZATION_ID and session:
307 try:
308 logger.info(f"Looking up customer by Office365 organization ID: {value}")
309 customer_response = await get_customer_by_auth_key(
310 integration_name="Office365",
311 auth_key_name="TENANT_ID",
312 auth_key_value=value,
313 session=session,
314 )
315 logger.info(f"Found customer {customer_response.customer_code} for Office365 organization ID {value}")
316 return customer_response.customer_code
317 except HTTPException as e:
318 logger.warning(f"Failed to get customer code from Office365 organization ID: {str(e)}")
319 # Continue checking other keys if this lookup fails
320 continue
321
322 # Handle cluster node special processing
323 if key == CustomerCodeKeys.CLUSTER_NODE:
324 processed_value = CustomerCodeKeys.get_processed_value(key, value)
325 logger.info(f"Processed value for {key.value} is {processed_value}")
326 return processed_value
327
328 # For other keys, return the value directly
329 return value
330
331 # If none of the keys are present, raise an exception
332 logger.info(f"Failed to fetch customer code. Valid customer code field names are {', '.join([key.value for key in CustomerCodeKeys])}")
333 raise HTTPException(
334 status_code=400,
335 detail=f"Failed to fetch customer code. Valid customer code field names are {', '.join([key.value for key in CustomerCodeKeys])}",
336 )
337
338
339 async def add_alert_to_document(
340 alert: CreateAlertRequest,
341 soc_alert_id: int,
342 ) -> Optional[str]:
343 """
344 Update the alert document in Elasticsearch with the provided SOC alert ID URL.
345
346 Parameters:
347 - es_client: The Elasticsearch client instance to use for the update.
348 - alert: The alert request object containing alert_id and index_name.
349 - soc_alert_id: The alert ID as it exists within IRIS.
350 - session: The database session for retrieving connector information.
351
352 Returns:
353 - True if the update is successful, False otherwise.
354 """
355 es_client = await create_wazuh_indexer_client_async("Wazuh-Indexer")
356 try:
357 await es_client.update(
358 index=alert.index_name,
359 id=alert.alert_id,
360 body={"doc": {"alert_id": soc_alert_id}},
361 )
362 logger.info(
363 f"Added alert ID {soc_alert_id} to alert {alert.alert_id} in index {alert.index_name}",
364 )
365 return soc_alert_id
366 except Exception as e:
367 logger.error(
368 f"Failed to add alert ID {soc_alert_id} to alert {alert.alert_id} in index {alert.index_name}: {e}",
369 )
370 # Attempt to remove read-only block
371 try:
372 await es_client.indices.put_settings(
373 index=alert.index_name,
374 body={"index.blocks.write": None},
375 )
376 logger.info(
377 f"Removed read-only block from index {alert.index_name}. Retrying update.",
378 )
379
380 # Retry the update operation
381 await es_client.update(
382 index=alert.index_name,
383 id=alert.alert_id,
384 body={"doc": {"alert_id": soc_alert_id}},
385 )
386 logger.info(
387 f"Added alert ID {soc_alert_id} to alert {alert.alert_id} in index {alert.index_name} after removing read-only block",
388 )
389
390 # Reenable the write block
391 await es_client.indices.put_settings(
392 index=alert.index_name,
393 body={"index.blocks.write": True},
394 )
395 return soc_alert_id
396 except Exception as e2:
397 logger.error(
398 f"Failed to remove read-only block from index {alert.index_name}: {e2}",
399 )
400 return False
401
402
403 async def retrieve_agent_details_from_db(agent_name: str, session: AsyncSession):
404 """
405 Retrieve agent details from the database.
406
407 Args:
408 agent_name (str): The name of the agent.
409 session (AsyncSession): The database session.
410
411 Returns:
412 Agents: The agent details.
413 """
414 logger.info(f"Retrieving agent details for {agent_name}")
415 result = await session.execute(
416 select(Agents).where(Agents.hostname == agent_name),
417 )
418 agent = result.scalars().first()
419 if agent:
420 return agent
421 return None
422
423
424 async def validate_syslog_type_source(source: str, session: AsyncSession) -> bool:
425 """
426 Invoke the `get_configured_sources` to ensure the `source` has been configured
427 """
428 sources = await get_configured_sources(session)
429 if source not in sources.sources:
430 raise HTTPException(
431 status_code=400,
432 detail=f"Incident Management: {source} Source must be configured",
433 )
434
435
436 async def get_all_field_names(syslog_type: str, session: AsyncSession) -> FieldNames:
437 """
438 Get the field names for the given syslog type.
439
440 Args:
441 syslog_type (str): The syslog type.
442 session (AsyncSession): The database session.
443
444 Returns:
445 FieldNames: The field names.
446 """
447 return FieldNames(
448 field_names=await get_field_names(syslog_type, session),
449 asset_name=await get_asset_names(syslog_type, session),
450 timefield_name=await get_timefield_names(syslog_type, session),
451 alert_title_name=await get_alert_title_names(syslog_type, session),
452 ioc_field_names=await get_ioc_names(syslog_type, session),
453 )
454
455
456 async def resolve_asset_name_from_payload(asset_name_field: str, alert_payload: dict) -> Optional[str]:
457 """
458 Resolve the actual asset name value from the alert payload.
459 Supports multiple asset name fields separated by commas.
460
461 Args:
462 asset_name_field (str): The asset name field(s) from config (can be comma-separated)
463 alert_payload (dict): The alert payload containing the data
464
465 Returns:
466 Optional[str]: The resolved asset name value, or None if not found
467 """
468 # Split by comma and strip whitespace to support multiple fields
469 possible_asset_fields = [field.strip() for field in asset_name_field.split(",")]
470
471 logger.info(f"Checking for asset name in fields: {possible_asset_fields}")
472
473 # Try each possible asset field in order
474 for field in possible_asset_fields:
475 if field in alert_payload and alert_payload[field]:
476 asset_value = alert_payload[field]
477 logger.info(f"Found asset name '{asset_value}' in field '{field}'")
478 return asset_value
479
480 logger.warning(f"No asset name found in any of these fields: {possible_asset_fields}")
481 return None
482
483
484 async def get_process_name(source_dict: dict) -> List[str]:
485 """
486 Get the process name from the source dictionary.
487
488 Args:
489 source_dict (dict): The source dictionary.
490
491 Returns:
492 List[str]: The process name as a list.
493 """
494 remove_process_name_if_osquery(source_dict)
495 process_image = get_process_image(source_dict)
496 process_name = get_process_name_from_image(process_image)
497 return [process_name] if process_name else []
498
499
500 async def build_alert_context_payload(alert_payload: dict, field_names: Any) -> Dict[str, Any]:
501 """
502 Build the alert context payload.
503
504 Args:
505 alert_payload (dict): The alert payload.
506 field_names (Any): The field names.
507
508 Returns:
509 dict: The alert context payload.
510 """
511 process_name = await get_process_name(alert_payload)
512 alert_context_payload = {field: alert_payload[field] for field in field_names.field_names if field in alert_payload}
513 alert_context_payload["process_name"] = process_name
514 return alert_context_payload
515
516
517 def get_ioc_type(ioc_value: str) -> Optional[AlertIocValue]:
518 """
519 Determine the IOC type based on the value.
520
521 Args:
522 ioc_value (str): The IOC value.
523
524 Returns:
525 AlertIocValue: The IOC type (IP, DOMAIN, HASH, or URL), or None if the type cannot be determined.
526 """
527 # Regular expression patterns for IP, domain, and hash
528 ip_pattern = re.compile(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$")
529 domain_pattern = re.compile(r"^(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$")
530 hash_pattern = re.compile(r"^[a-fA-F0-9]{32}$|^[a-fA-F0-9]{40}$|^[a-fA-F0-9]{64}$")
531
532 if ip_pattern.match(ioc_value):
533 return AlertIocValue.IP
534 elif domain_pattern.match(ioc_value):
535 return AlertIocValue.DOMAIN
536 elif hash_pattern.match(ioc_value):
537 return AlertIocValue.HASH
538 else:
539 return None
540
541
542 async def build_ioc_payload(alert_payload: dict, field_names: Any) -> Optional[Dict[str, Any]]:
543 """
544 Build the alert context payload.
545
546 Args:
547 alert_payload (dict): The alert payload.
548 field_names (Any): The field names.
549
550 Returns:
551 Optional[dict]: The alert context payload or None if no ioc_value is found.
552 """
553 logger.info(f"Building IOC payload for alert {alert_payload}")
554 ioc_payload = {field: alert_payload[field] for field in field_names.ioc_field_names if field in alert_payload}
555
556 # Determine the IOC value
557 ioc_value = next(iter(ioc_payload.values()), None)
558 if not ioc_value:
559 logger.info("No IOC value found, returning None")
560 return None
561
562 # Determine the IOC type
563 ioc_payload["ioc_value"] = ioc_value
564 ioc_payload["ioc_type"] = get_ioc_type(ioc_value)
565
566 ioc_payload["ioc_description"] = "IOC Auto-Generated From SOCFortress CoPilot"
567 logger.info(f"IOC Payload: {ioc_payload}")
568 return ioc_payload
569
570
571 async def build_alert_payload(
572 syslog_type: str,
573 index_name: str,
574 index_id: str,
575 alert_payload: dict,
576 session: AsyncSession,
577 ) -> CreatedAlertPayload:
578 """
579 Build the alert payload based on the syslog type and the alert payload.
580
581 Args:
582 syslog_type (str): The syslog type.
583 alert_payload (dict): The alert payload.
584 session (AsyncSession): The database session.
585
586 Returns:
587 dict: The built alert payload.
588 """
589 field_names = await get_all_field_names(syslog_type, session)
590
591 # Resolve the actual asset name from the payload using multiple possible fields
592 asset_name_value = await resolve_asset_name_from_payload(field_names.asset_name, alert_payload)
593
594 # Validate alert_title_name exists
595 if field_names.alert_title_name not in alert_payload:
596 raise HTTPException(
597 status_code=400,
598 detail=f"Field name {field_names.alert_title_name} not found in alert payload",
599 )
600
601 # Resolve timefield with fallback to 'timestamp'
602 timefield_value = None
603 if field_names.timefield_name in alert_payload:
604 timefield_value = alert_payload[field_names.timefield_name]
605 elif "timestamp" in alert_payload:
606 logger.warning(
607 f"Configured time field '{field_names.timefield_name}' not found in alert payload. " f"Falling back to 'timestamp' field.",
608 )
609 timefield_value = alert_payload["timestamp"]
610 else:
611 logger.warning(
612 f"Neither configured time field '{field_names.timefield_name}' nor 'timestamp' found in alert payload. "
613 f"Using current UTC time as fallback.",
614 )
615 timefield_value = datetime.utcnow().isoformat()
616
617 # Clean the alert title to remove BOM and normalize encoding
618 raw_alert_title = alert_payload.get(field_names.alert_title_name)
619 cleaned_alert_title = clean_alert_title(raw_alert_title) if raw_alert_title else None
620
621 return CreatedAlertPayload(
622 alert_context_payload=await build_alert_context_payload(alert_payload, field_names),
623 asset_payload=asset_name_value,
624 timefield_payload=timefield_value,
625 alert_title_payload=cleaned_alert_title,
626 ioc_payload=await build_ioc_payload(alert_payload, field_names),
627 source=syslog_type,
628 index_name=index_name,
629 index_id=index_id,
630 )
631
632
633 async def handle_customer_notifications(
634 customer_code: str,
635 asset_name: str,
636 alert_payload: CreatedAlertPayload,
637 session: AsyncSession,
638 type: str = "alert",
639 ) -> None:
640 customer_notifications = await get_customer_notification(customer_code, session)
641 if customer_notifications and customer_notifications[0].enabled:
642 logger.info(f"Executing workflow for customer code {customer_code}")
643 await execute_workflow(
644 ExecuteWorkflowRequest(
645 workflow_id=customer_notifications[0].shuffle_workflow_id,
646 execution_arguments={
647 "type": type,
648 "customer_code": customer_code,
649 "asset_name": asset_name,
650 "alert_context_payload": alert_payload.alert_context_payload,
651 "alert_title": alert_payload.alert_title_payload,
652 "alert_id": alert_payload.alert_id,
653 },
654 start="",
655 ),
656 )
657
658 # Forward alerts (not cases) to SOCFortress MDR when the customer has the
659 # integration deployed. Best-effort: never breaks alert creation.
660 if type == "alert":
661 from app.incidents.services.mdr_forwarder import forward_alert_to_mdr
662
663 await forward_alert_to_mdr(
664 customer_code=customer_code,
665 alert_payload=alert_payload,
666 session=session,
667 )
668
669
670 # ! OLD FUNCTION ! #
671 # async def create_alert_full(
672 # alert_payload: CreatedAlertPayload,
673 # customer_code: str,
674 # session: AsyncSession,
675 # threshold_alert: bool = False,
676 # velo_sigma_alert: bool = False,
677 # ) -> Alert:
678 # """
679 # Create an alert in CoPilot.
680
681 # Args:
682 # alert_payload (dict): The alert payload.
683 # customer_code (str): The customer code.
684 # session (AsyncSession): The database session.
685 # threshold_alert (bool, optional): Whether this is a threshold alert. Defaults to False.
686 # velo_sigma_alert (bool, optional): Whether this is a Velociraptor Sigma alert. Defaults to False.
687
688 # Returns:
689 # CreateAlertResponse: The response object containing the created alert details.
690
691 # Raises:
692 # HTTPException: If there is an error creating the alert.
693 # """
694 # # For velo_sigma_alert, check if an open alert with the same title already exists
695 # if velo_sigma_alert:
696 # existing_alert_id = await open_alert_exists(alert_payload, customer_code, session)
697 # if existing_alert_id:
698 # logger.info(
699 # f"Found existing open alert ID {existing_alert_id} for Velociraptor Sigma alert with title {alert_payload.alert_title_payload}",
700 # )
701
702 # # Add the asset to the existing alert if it doesn't already exist
703 # asset_exists = await does_assit_exist(alert_payload, existing_alert_id, session)
704 # if not asset_exists and alert_payload.asset_payload:
705 # logger.info(f"Adding asset {alert_payload.asset_payload} to existing alert ID {existing_alert_id}")
706 # await add_asset_to_copilot_alert(
707 # alert_payload=alert_payload,
708 # alert_id=existing_alert_id,
709 # customer_code=customer_code,
710 # session=session,
711 # )
712
713 # # Add IOC if present and doesn't already exist
714 # if alert_payload.ioc_payload is not None:
715 # ioc_exists = await does_ioc_exist(alert_payload, existing_alert_id, session)
716 # if not ioc_exists:
717 # logger.info(f"Adding IOC {alert_payload.ioc_payload['ioc_value']} to existing alert ID {existing_alert_id}")
718 # await add_ioc_to_copilot_alert(
719 # alert_payload=alert_payload,
720 # alert_id=existing_alert_id,
721 # customer_code=customer_code,
722 # session=session,
723 # )
724
725 # # Update the document reference if needed
726 # if alert_payload.index_name and alert_payload.index_id:
727 # await add_alert_to_document(
728 # CreateAlertRequest(index_name=alert_payload.index_name, alert_id=alert_payload.index_id),
729 # existing_alert_id,
730 # )
731
732 # # Set alert ID for notifications
733 # alert_payload.alert_id = existing_alert_id
734
735 # # Handle customer notifications
736 # if alert_payload.asset_payload:
737 # await handle_customer_notifications(
738 # customer_code=customer_code,
739 # asset_name=alert_payload.asset_payload,
740 # alert_payload=alert_payload,
741 # session=session,
742 # )
743 # else:
744 # await handle_customer_notifications(
745 # customer_code=customer_code,
746 # asset_name="No asset found",
747 # alert_payload=alert_payload,
748 # session=session,
749 # )
750
751 # return existing_alert_id
752
753 # # If not velo_sigma_alert or no existing alert found, proceed with normal alert creation
754 # alert_id = (await create_alert_in_copilot(alert_payload=alert_payload, customer_code=customer_code, session=session)).id
755 # alert_context_id = (
756 # await create_alert_context_payload(source=alert_payload.source, alert_payload=alert_payload.alert_context_payload, session=session)
757 # ).id
758 # asset = await create_asset_context_payload(
759 # customer_code=customer_code,
760 # asset_payload=alert_payload,
761 # alert_context_id=alert_context_id,
762 # alert_id=alert_id,
763 # session=session,
764 # )
765 # if alert_payload.ioc_payload is not None:
766 # ioc_id = (
767 # await create_ioc_payload(
768 # ioc_payload=AlertIoCCreate(
769 # alert_id=alert_id,
770 # ioc_value=alert_payload.ioc_payload["ioc_value"],
771 # ioc_type=alert_payload.ioc_payload["ioc_type"],
772 # ioc_description=alert_payload.ioc_payload["ioc_description"],
773 # ),
774 # alert_id=alert_id,
775 # session=session,
776 # )
777 # ).id
778 # logger.info(
779 # f"Creating alert for customer code {customer_code} with alert context ID {alert_context_id} and asset ID {asset.id} and ioc ID {ioc_id}",
780 # )
781 # logger.info(f"Creating alert for customer code {customer_code} with alert context ID {alert_context_id} and asset ID {asset.id}")
782 # alert_payload.alert_id = alert_id
783 # if asset is not None:
784 # await handle_customer_notifications(
785 # customer_code=customer_code,
786 # asset_name=asset.asset_name,
787 # alert_payload=alert_payload,
788 # session=session,
789 # )
790 # else:
791 # await handle_customer_notifications(
792 # customer_code=customer_code,
793 # asset_name="No asset found",
794 # alert_payload=alert_payload,
795 # session=session,
796 # )
797
798 # if threshold_alert is True or velo_sigma_alert is True:
799 # logger.info(
800 # f"{'Threshold' if threshold_alert else 'Velociraptor Sigma'} alert created for customer code {customer_code} with alert ID {alert_id}",
801 # )
802 # return alert_id
803
804 # await add_alert_to_document(CreateAlertRequest(index_name=alert_payload.index_name, alert_id=alert_payload.index_id), alert_id)
805
806 # return alert_id
807
808
809 # ! NEW FUNCTION ! #
810 async def create_alert_full(
811 alert_payload: CreatedAlertPayload,
812 customer_code: str,
813 session: AsyncSession,
814 threshold_alert: bool = False,
815 velo_sigma_alert: bool = False,
816 ) -> Alert:
817 """
818 Create an alert in CoPilot.
819
820 Args:
821 alert_payload (dict): The alert payload.
822 customer_code (str): The customer code.
823 session (AsyncSession): The database session.
824 threshold_alert (bool, optional): Whether this is a threshold alert. Defaults to False.
825 velo_sigma_alert (bool, optional): Whether this is a Velociraptor Sigma alert. Defaults to False.
826
827 Returns:
828 CreateAlertResponse: The response object containing the created alert details.
829
830 Raises:
831 HTTPException: If there is an error creating the alert.
832 """
833 # For velo_sigma_alert, check if an open alert with the same title already exists
834 if velo_sigma_alert:
835 existing_alert_id = await open_alert_exists(alert_payload, customer_code, session)
836 if existing_alert_id:
837 logger.info(
838 f"Found existing open alert ID {existing_alert_id} for Velociraptor Sigma alert with title {alert_payload.alert_title_payload}",
839 )
840
841 # Update the alert_creation_time to the latest trigger time
842 await update_alert_creation_time(existing_alert_id, alert_payload.timefield_payload, session)
843
844 # Check if the asset already exists for this alert (to determine if we should skip notifications)
845 asset_already_exists = False
846 if alert_payload.asset_payload:
847 asset_already_exists = await does_assit_exist(alert_payload, existing_alert_id, session)
848
849 # Add the asset to the existing alert if it doesn't already exist
850 if not asset_already_exists and alert_payload.asset_payload:
851 logger.info(f"Adding asset {alert_payload.asset_payload} to existing alert ID {existing_alert_id}")
852 await add_asset_to_copilot_alert(
853 alert_payload=alert_payload,
854 alert_id=existing_alert_id,
855 customer_code=customer_code,
856 session=session,
857 )
858
859 # Add IOC if present and doesn't already exist
860 if alert_payload.ioc_payload is not None:
861 ioc_exists = await does_ioc_exist(alert_payload, existing_alert_id, session)
862 if not ioc_exists:
863 logger.info(f"Adding IOC {alert_payload.ioc_payload['ioc_value']} to existing alert ID {existing_alert_id}")
864 await add_ioc_to_copilot_alert(
865 alert_payload=alert_payload,
866 alert_id=existing_alert_id,
867 customer_code=customer_code,
868 session=session,
869 )
870
871 # Update the document reference if needed
872 if alert_payload.index_name and alert_payload.index_id:
873 await add_alert_to_document(
874 CreateAlertRequest(index_name=alert_payload.index_name, alert_id=alert_payload.index_id),
875 existing_alert_id,
876 )
877
878 # Set alert ID for notifications
879 alert_payload.alert_id = existing_alert_id
880
881 # Handle customer notifications only if the asset didn't already exist
882 if not asset_already_exists:
883 logger.info(f"Sending notifications for new asset {alert_payload.asset_payload} in existing alert {existing_alert_id}")
884 if alert_payload.asset_payload:
885 await handle_customer_notifications(
886 customer_code=customer_code,
887 asset_name=alert_payload.asset_payload,
888 alert_payload=alert_payload,
889 session=session,
890 )
891 else:
892 await handle_customer_notifications(
893 customer_code=customer_code,
894 asset_name="No asset found",
895 alert_payload=alert_payload,
896 session=session,
897 )
898 else:
899 logger.info(f"Skipping notifications for existing asset {alert_payload.asset_payload} in alert {existing_alert_id}")
900
901 # Trigger Talon investigation if enabled for this customer
902 await handle_talon_investigation(existing_alert_id, customer_code, session)
903
904 return existing_alert_id
905
906 # If not velo_sigma_alert or no existing alert found, proceed with normal alert creation
907 alert_id = (await create_alert_in_copilot(alert_payload=alert_payload, customer_code=customer_code, session=session)).id
908 alert_context_id = (
909 await create_alert_context_payload(source=alert_payload.source, alert_payload=alert_payload.alert_context_payload, session=session)
910 ).id
911 asset = await create_asset_context_payload(
912 customer_code=customer_code,
913 asset_payload=alert_payload,
914 alert_context_id=alert_context_id,
915 alert_id=alert_id,
916 session=session,
917 )
918 if alert_payload.ioc_payload is not None:
919 ioc_id = (
920 await create_ioc_payload(
921 ioc_payload=AlertIoCCreate(
922 alert_id=alert_id,
923 ioc_value=alert_payload.ioc_payload["ioc_value"],
924 ioc_type=alert_payload.ioc_payload["ioc_type"],
925 ioc_description=alert_payload.ioc_payload["ioc_description"],
926 ),
927 alert_id=alert_id,
928 session=session,
929 )
930 ).id
931 logger.info(
932 f"Creating alert for customer code {customer_code} with alert context ID {alert_context_id} and asset ID {asset.id} and ioc ID {ioc_id}",
933 )
934 logger.info(f"Creating alert for customer code {customer_code} with alert context ID {alert_context_id} and asset ID {asset.id}")
935 alert_payload.alert_id = alert_id
936 if asset is not None:
937 await handle_customer_notifications(
938 customer_code=customer_code,
939 asset_name=asset.asset_name,
940 alert_payload=alert_payload,
941 session=session,
942 )
943 else:
944 await handle_customer_notifications(
945 customer_code=customer_code,
946 asset_name="No asset found",
947 alert_payload=alert_payload,
948 session=session,
949 )
950
951 # Trigger Talon investigation if enabled for this customer
952 await handle_talon_investigation(alert_id, customer_code, session)
953
954 if threshold_alert is True or velo_sigma_alert is True:
955 logger.info(
956 f"{'Threshold' if threshold_alert else 'Velociraptor Sigma'} alert created for customer code {customer_code} with alert ID {alert_id}",
957 )
958 return alert_id
959
960 await add_alert_to_document(CreateAlertRequest(index_name=alert_payload.index_name, alert_id=alert_payload.index_id), alert_id)
961
962 return alert_id
963
964
965 async def handle_talon_investigation(alert_id: int, customer_code: str, session: AsyncSession) -> None:
966 """
967 Trigger a Talon investigation if AI analyst triggers are enabled for the customer.
968
969 Args:
970 alert_id: The alert ID to investigate.
971 customer_code: The customer code.
972 session: The database session.
973 """
974 try:
975 ai_triggers = await get_customer_ai_trigger(customer_code, session)
976 if ai_triggers and ai_triggers[0].enabled:
977 logger.info(f"AI analyst trigger enabled for customer {customer_code}, invoking Talon investigation for alert {alert_id}")
978 await talon_investigate_alert(
979 TalonInvestigateRequest(
980 alert_id=alert_id,
981 customer_code=customer_code,
982 ),
983 )
984 else:
985 logger.info(f"AI analyst trigger not enabled for customer {customer_code}, skipping Talon investigation")
986 except Exception as e:
987 logger.error(f"Failed to trigger Talon investigation for alert {alert_id}: {e}")
988
989
990 async def does_assit_exist(alert_payload: CreatedAlertPayload, alert_id: int, session: AsyncSession) -> bool:
991 """
992 Check if the asset exists for the given alert payload.
993
994 Args:
995 alert_payload (dict): The alert payload.
996 alert_id (int): The alert ID.
997 session (AsyncSession): The database session.
998
999 Returns:
1000 bool: True if the asset exists, None otherwise.
1001 """
1002 logger.info(f"Checking if an asset exists for alert ID {alert_id} with asset name {alert_payload.asset_payload}")
1003 result = await session.execute(
1004 select(Asset).where(
1005 Asset.alert_linked == alert_id,
1006 Asset.asset_name == alert_payload.asset_payload,
1007 ),
1008 )
1009 asset = result.scalars().first()
1010 if asset:
1011 logger.info(f"Asset exists for alert ID {alert_id} with asset name {alert_payload.asset_payload}")
1012 return True
1013 logger.info(f"No asset exists for alert ID {alert_id} with asset name {alert_payload.asset_payload}")
1014 return False
1015
1016
1017 async def add_asset_to_copilot_alert(alert_payload: CreatedAlertPayload, alert_id: int, customer_code: str, session: AsyncSession) -> None:
1018 """
1019 Add the asset to the alert in CoPilot.
1020
1021 Args:
1022 alert_payload (dict): The alert payload.
1023 alert_id (int): The alert ID.
1024 session (AsyncSession): The database session.
1025 """
1026 if await does_assit_exist(alert_payload, alert_id, session):
1027 return None
1028 agent_details = await retrieve_agent_details_from_db(alert_payload.asset_payload, session)
1029 alert_context_id = (
1030 await create_alert_context_payload(source=alert_payload.source, alert_payload=alert_payload.alert_context_payload, session=session)
1031 ).id
1032 agent_id = agent_details.agent_id if agent_details else None
1033 velociraptor_id = agent_details.velociraptor_id if agent_details else None
1034 asset_context = Asset(
1035 alert_linked=alert_id,
1036 asset_name=alert_payload.asset_payload,
1037 alert_context_id=alert_context_id,
1038 agent_id=agent_id,
1039 velociraptor_id=velociraptor_id,
1040 customer_code=customer_code,
1041 index_name=alert_payload.index_name,
1042 index_id=alert_payload.index_id,
1043 )
1044 # Commit it to the database
1045 session.add(asset_context)
1046 await session.commit()
1047 return asset_context
1048
1049
1050 async def does_ioc_exist(alert_payload: CreatedAlertPayload, alert_id: int, session: AsyncSession) -> bool:
1051 """
1052 Check if the IoC exists for the given alert payload.
1053
1054 Args:
1055 alert_payload (dict): The alert payload.
1056 alert_id (int): The alert ID.
1057 session (AsyncSession): The database session.
1058
1059 Returns:
1060 bool: True if the IoC exists, None otherwise.
1061 """
1062 logger.info(f"Checking if an IoC exists for alert ID {alert_id} with IoC value {alert_payload.ioc_payload['ioc_value']}")
1063 result = await session.execute(
1064 select(IoC)
1065 .join(AlertToIoC, AlertToIoC.ioc_id == IoC.id)
1066 .where(AlertToIoC.alert_id == alert_id, IoC.value == alert_payload.ioc_payload["ioc_value"]),
1067 )
1068 ioc = result.scalars().first()
1069 if ioc:
1070 logger.info(f"IoC exists for alert ID {alert_id} with IoC value {alert_payload.ioc_payload['ioc_value']}")
1071 return True
1072 logger.info(f"No IoC exists for alert ID {alert_id} with IoC value {alert_payload.ioc_payload['ioc_value']}")
1073 return False
1074
1075
1076 async def add_ioc_to_copilot_alert(alert_payload: CreatedAlertPayload, alert_id: int, customer_code: str, session: AsyncSession) -> None:
1077 """
1078 Add the IoC to the alert in CoPilot.
1079
1080 Args:
1081 alert_payload (dict): The alert payload.
1082 alert_id (int): The alert ID.
1083 customer_code (str): The customer code.
1084 session (AsyncSession): The database session.
1085 """
1086 if await does_ioc_exist(alert_payload, alert_id, session):
1087 return None
1088
1089 ioc_payload = AlertIoCCreate(
1090 alert_id=alert_id,
1091 ioc_value=alert_payload.ioc_payload["ioc_value"],
1092 ioc_type=alert_payload.ioc_payload["ioc_type"],
1093 ioc_description=alert_payload.ioc_payload["ioc_description"],
1094 )
1095
1096 ioc_context = IoC(
1097 value=ioc_payload.ioc_value,
1098 type=ioc_payload.ioc_type,
1099 description=ioc_payload.ioc_description,
1100 )
1101 # Add the IoC context to the session
1102 session.add(ioc_context)
1103 await session.commit()
1104 await session.refresh(ioc_context)
1105
1106 # Create the AlertToIoC relationship
1107 alert_to_ioc = AlertToIoC(
1108 alert_id=alert_id,
1109 ioc_id=ioc_context.id,
1110 )
1111 # Add the AlertToIoC relationship to the session
1112 session.add(alert_to_ioc)
1113 await session.commit()
1114 return ioc_context
1115
1116
1117 async def create_alert_in_copilot(alert_payload: CreatedAlertPayload, customer_code: str, session: AsyncSession) -> Alert:
1118 """
1119 Create an alert in CoPilot.
1120
1121 Args:
1122 alert_payload (dict): The alert payload.
1123 customer_code (str): The customer code.
1124
1125 Returns:
1126 CreateAlertResponse: The response object containing the created alert details.
1127
1128 Raises:
1129 HTTPException: If there is an error creating the alert.
1130 """
1131 logger.info(f"Creating alert for customer code {customer_code} with payload {alert_payload}")
1132 alert = Alert(
1133 alert_name=alert_payload.alert_title_payload,
1134 alert_description=alert_payload.alert_title_payload,
1135 status="OPEN",
1136 alert_creation_time=datetime.utcnow(),
1137 customer_code=customer_code,
1138 source=alert_payload.source,
1139 assigned_to=None,
1140 )
1141 # Commit it to the database
1142 session.add(alert)
1143 await session.commit()
1144 return alert
1145
1146
1147 async def create_alert_context_payload(source: str, alert_payload: dict, session: AsyncSession) -> AlertContext:
1148 """
1149 Build the alert context payload based on the valid field names and the alert payload. Then
1150 create the alert context in the database.
1151 """
1152 logger.info(f"Creating alert context for source {source} with payload {alert_payload}")
1153 alert_context = AlertContext(
1154 source=source,
1155 context=alert_payload,
1156 )
1157 # Commit it to the database
1158 session.add(alert_context)
1159 await session.commit()
1160 return alert_context
1161
1162
1163 async def create_asset_context_payload(
1164 customer_code: str,
1165 asset_payload: CreatedAlertPayload,
1166 alert_context_id: int,
1167 alert_id: int,
1168 session: AsyncSession,
1169 ) -> Asset:
1170 """
1171 Build the asset context payload based on the valid field names and the asset payload. Then
1172 create the asset context in the database.
1173 """
1174 agent_details = await retrieve_agent_details_from_db(asset_payload.asset_payload, session)
1175 agent_id = agent_details.agent_id if agent_details else None
1176 velociraptor_id = agent_details.velociraptor_id if agent_details else None
1177
1178 asset_context = Asset(
1179 alert_linked=alert_id,
1180 asset_name=asset_payload.asset_payload,
1181 alert_context_id=alert_context_id,
1182 agent_id=agent_id,
1183 velociraptor_id=velociraptor_id,
1184 customer_code=customer_code,
1185 index_name=asset_payload.index_name,
1186 index_id=asset_payload.index_id,
1187 )
1188 # Commit it to the database
1189 session.add(asset_context)
1190 await session.commit()
1191 return asset_context
1192
1193
1194 async def create_ioc_payload(
1195 ioc_payload: AlertIoCCreate,
1196 alert_id: int,
1197 session: AsyncSession,
1198 ) -> IoC:
1199 """
1200 Build the ioc context payload based on the valid field names and the ioc payload. Then
1201 create the ioc context in the database.
1202 """
1203 logger.info(f"Creating IoC context for alert ID {alert_id} with payload {ioc_payload}")
1204
1205 ioc_context = IoC(
1206 value=ioc_payload.ioc_value,
1207 type=ioc_payload.ioc_type,
1208 description=ioc_payload.ioc_description,
1209 )
1210 # Add the IoC context to the session
1211 session.add(ioc_context)
1212 await session.flush()
1213
1214 # Create the AlertToIoC relationship
1215 alert_to_ioc = AlertToIoC(
1216 alert_id=alert_id,
1217 ioc_id=ioc_context.id,
1218 )
1219 # Add the AlertToIoC relationship to the session
1220 session.add(alert_to_ioc)
1221 await session.commit()
1222
1223 return ioc_context
1224
1225
1226 async def open_alert_exists(alert_payload: CreatedAlertPayload, customer_code: str, session: AsyncSession) -> bool:
1227 """
1228 Check if an open alert exists for the given alert payload.
1229
1230 Args:
1231 alert_payload (dict): The alert payload.
1232 customer_code (str): The customer code.
1233
1234 Returns:
1235 bool: True if an open alert exists, None otherwise.
1236 """
1237 logger.info(f"Checking if an open alert exists for customer code {customer_code} with alert title {alert_payload.alert_title_payload}")
1238 result = await session.execute(
1239 select(Alert).where(
1240 Alert.customer_code == customer_code,
1241 Alert.alert_name == alert_payload.alert_title_payload,
1242 Alert.status == "OPEN",
1243 ),
1244 )
1245 alert = result.scalars().first()
1246 if alert:
1247 logger.info(f"Open alert exists for customer code {customer_code} with alert title {alert_payload.alert_title_payload}")
1248 return alert.id
1249 logger.info(f"No open alert exists for customer code {customer_code} with alert title {alert_payload.alert_title_payload}")
1250 return None
1251
1252
1253 async def create_alert(
1254 alert: CreateAlertRequest,
1255 session: AsyncSession,
1256 simga_alert: str = None,
1257 ) -> CreateAlertResponse:
1258 """
1259 Creates an alert in CoPilot.
1260
1261 Args:
1262 alert (CreateAlertRequest): The request object containing the alert details.
1263 session (AsyncSession): The database session.
1264
1265 Returns:
1266 CreateAlertResponse: The response object containing the created alert details.
1267
1268 Raises:
1269 HTTPException: If there is an error creating the alert.
1270 """
1271 logger.info(f"Creating alert {alert.alert_id} in CoPilot")
1272 alert_details = await get_single_alert_details(alert_details=alert)
1273 await validate_syslog_type_source(alert_details.syslog_type, session)
1274 customer_code = await get_customer_code(dict(alert_details.source), session=session)
1275 logger.info(f"Customer code: {customer_code}")
1276 customer_alert_creation_settings = await is_customer_code_valid(customer_code=customer_code, session=session)
1277 logger.info(f"Customer creation settings: {customer_alert_creation_settings}")
1278 alert_payload = await build_alert_payload(
1279 alert_details.syslog_type,
1280 alert_details.index,
1281 alert_details.id,
1282 alert_details.source.to_dict(),
1283 session,
1284 )
1285 if simga_alert is not None:
1286 return await create_alert_full(alert_payload, customer_code, session)
1287
1288 existing_alert = await open_alert_exists(alert_payload, customer_code, session)
1289 if existing_alert:
1290 logger.info(
1291 f"Open alert exists for customer code {customer_code} with alert title {alert_payload.alert_title_payload} and alert ID {existing_alert}",
1292 )
1293 # Update the alert_creation_time to the latest trigger time
1294 await update_alert_creation_time(existing_alert, alert_payload.timefield_payload, session)
1295 await add_alert_to_document(
1296 CreateAlertRequest(index_name=alert_payload.index_name, alert_id=alert_payload.index_id),
1297 existing_alert,
1298 )
1299 await add_asset_to_copilot_alert(alert_payload, existing_alert, customer_code, session)
1300 # If the alert has an IoC, add it to the alert
1301 if alert_payload.ioc_payload is not None:
1302 logger.info(f"Adding IoC to alert {existing_alert}")
1303 await add_ioc_to_copilot_alert(alert_payload, existing_alert, customer_code, session)
1304 else:
1305 logger.info(f"No IoC found for alert {existing_alert}")
1306 return existing_alert
1307 return await create_alert_full(alert_payload, customer_code, session)
1308
1309
1310 async def retrieve_alert_timeline(alert: CreateAlertRequestRoute, session: AsyncSession) -> List[Dict[str, Any]]:
1311 """
1312 Retrieve the alert timeline for the given alert.
1313
1314 For threshold alerts (identified by having a ThresholdAlertMetadata record),
1315 the timeline is built from the stored replay query and group_by_fields.
1316 For standard alerts, the timeline is built from the process_id and agent_name.
1317
1318 Args:
1319 alert (CreateAlertRequestRoute): The alert details.
1320 session (AsyncSession): The database session.
1321
1322 Returns:
1323 List[Dict[str, Any]]: The alert timeline.
1324 """
1325 # Check if this is a threshold alert and retrieve its timeline if so
1326 threshold_timeline = await retrieve_threshold_alert_timeline(
1327 alert_id=alert.alert_id,
1328 index_name=alert.index_name,
1329 index_id=alert.index_id,
1330 session=session,
1331 )
1332 if threshold_timeline is not None:
1333 logger.info(f"Retrieved threshold alert timeline with {len(threshold_timeline)} events")
1334 return threshold_timeline
1335
1336 alert_details = await get_alert_details(alert)
1337 if alert_details.source.process_id is not None:
1338 alert_timestamp = alert_details.source.timestamp
1339 start_of_day, end_of_day = calculate_day_range(alert_timestamp)
1340 return await fetch_alert_timeline(
1341 alert.index_name,
1342 alert_details.source.process_id,
1343 alert_details.source.agent_name,
1344 start_of_day,
1345 end_of_day,
1346 )
1347 return []
1348
1349
1350 async def get_alert_details(alert: CreateAlertRequestRoute) -> Any:
1351 """
1352 Get the details of a single alert.
1353
1354 Args:
1355 alert (CreateAlertRequestRoute): The alert details.
1356
1357 Returns:
1358 Any: The alert details.
1359 """
1360 return await get_single_alert_details(CreateAlertRequest(index_name=alert.index_name, alert_id=alert.index_id))
1361
1362
1363 def calculate_day_range(timestamp: str) -> (str, str):
1364 """
1365 Calculate the start and end of the day for the given timestamp.
1366
1367 Args:
1368 timestamp (str): The timestamp.
1369
1370 Returns:
1371 (str, str): The start and end of the day in the required format.
1372 """
1373 dt = datetime.fromisoformat(timestamp)
1374 start_of_day = dt.replace(hour=0, minute=0, second=0, microsecond=0)
1375 end_of_day = start_of_day + timedelta(days=1)
1376 return start_of_day.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3], end_of_day.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
1377
1378
1379 async def fetch_alert_timeline(
1380 index_name: str,
1381 process_id: str,
1382 agent_name: str,
1383 start_of_day: str,
1384 end_of_day: str,
1385 ) -> List[Dict[str, Any]]:
1386 """
1387 Fetch the alert timeline from the indexer.
1388
1389 Args:
1390 index_name (str): The name of the index.
1391 process_id (str): The process ID.
1392 agent_name (str): The agent name.
1393 start_of_day (str): The start of the day in the required format.
1394 end_of_day (str): The end of the day in the required format.
1395
1396 Returns:
1397 List[Dict[str, Any]]: The alert timeline.
1398 """
1399 es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
1400 alert_timeline = es_client.search(
1401 index=index_name,
1402 body={
1403 "query": {
1404 "bool": {
1405 "must": [
1406 {"match": {"process_id": process_id}},
1407 {"match": {"agent_name": agent_name}},
1408 {"range": {"timestamp": {"gte": start_of_day, "lt": end_of_day}}},
1409 ],
1410 },
1411 },
1412 },
1413 size=50,
1414 )
1415 return alert_timeline["hits"]["hits"]