暂无描述

alerts_db.py 54KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569
  1. # IRIS Source Code
  2. # Copyright (C) 2023 - DFIR-IRIS
  3. # contact@dfir-iris.org
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU Lesser General Public
  7. # License as published by the Free Software Foundation; either
  8. # version 3 of the License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. # Lesser General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU Lesser General Public License
  16. # along with this program; if not, write to the Free Software Foundation,
  17. # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. from copy import deepcopy
  19. import json
  20. from datetime import datetime, timedelta
  21. from flask_login import current_user
  22. from sqlalchemy import desc, asc, func, tuple_, or_, not_, and_
  23. from sqlalchemy.orm import aliased, make_transient, selectinload
  24. from typing import List, Tuple
  25. import app
  26. from app import db
  27. from app.datamgmt.case.case_assets_db import create_asset
  28. from app.datamgmt.case.case_assets_db import set_ioc_links
  29. from app.datamgmt.case.case_assets_db import get_unspecified_analysis_status_id
  30. from app.datamgmt.case.case_events_db import update_event_assets
  31. from app.datamgmt.case.case_events_db import update_event_iocs
  32. from app.datamgmt.case.case_iocs_db import add_ioc
  33. from app.datamgmt.manage.manage_access_control_db import get_user_clients_id
  34. from app.datamgmt.manage.manage_case_state_db import get_case_state_by_name
  35. from app.datamgmt.manage.manage_case_templates_db import get_case_template_by_id
  36. from app.datamgmt.manage.manage_case_templates_db import case_template_post_modifier
  37. from app.datamgmt.states import update_timeline_state
  38. from app.iris_engine.access_control.utils import ac_current_user_has_permission
  39. from app.iris_engine.utils.common import parse_bf_date_format
  40. from app.models.cases import Cases
  41. from app.models.models import EventCategory
  42. from app.models.models import Tags
  43. from app.models.models import AssetsType
  44. from app.models.models import Comments
  45. from app.models.models import CaseAssets
  46. from app.models.models import alert_assets_association
  47. from app.models.models import alert_iocs_association
  48. from app.models.models import Ioc
  49. from app.models.models import Client
  50. from app.models.alerts import Alert
  51. from app.models.alerts import AlertStatus
  52. from app.models.alerts import AlertCaseAssociation
  53. from app.models.alerts import SimilarAlertsCache
  54. from app.models.alerts import AlertResolutionStatus
  55. from app.models.alerts import AlertSimilarity
  56. from app.models.alerts import Severity
  57. from app.models.authorization import Permissions
  58. from app.models.authorization import User
  59. from app.schema.marshables import EventSchema
  60. from app.util import add_obj_history_entry
  61. relationship_model_map = {
  62. 'owner': User,
  63. 'severity': Severity,
  64. 'status': AlertStatus,
  65. 'customer': Client,
  66. 'resolution_status': AlertResolutionStatus,
  67. 'cases': Cases,
  68. 'comments': Comments,
  69. 'assets': CaseAssets,
  70. 'iocs': Ioc
  71. }
  72. RESTRICTED_USER_FIELDS = {
  73. 'password',
  74. 'mfa_secrets',
  75. 'webauthn_credentials',
  76. 'api_key',
  77. 'external_id',
  78. 'ctx_case',
  79. 'ctx_human_case',
  80. 'is_service_account'
  81. }
  82. def db_list_all_alerts():
  83. """
  84. List all alerts in the database
  85. """
  86. return db.session.query(Alert).all()
  87. def build_condition(column, operator, value):
  88. if hasattr(column, 'property') and hasattr(column.property, 'local_columns'):
  89. # It's a relationship attribute
  90. fk_cols = list(column.property.local_columns)
  91. if operator in ['in', 'not_in']:
  92. if len(fk_cols) == 1:
  93. # Use the single FK column for the condition
  94. fk_col = fk_cols[0]
  95. if operator == 'in':
  96. return fk_col.in_(value)
  97. else:
  98. return ~fk_col.in_(value)
  99. else:
  100. raise NotImplementedError(
  101. "in_() on a relationship with multiple FK columns not supported. Specify a direct column.")
  102. else:
  103. raise ValueError(
  104. "Non-in operators on relationships require specifying a related model column, e.g., owner.id or assets.asset_name.")
  105. # If we get here, 'column' should be an actual column, not a relationship.
  106. if operator == 'not':
  107. return column != value
  108. elif operator == 'in':
  109. return column.in_(value)
  110. elif operator == 'not_in':
  111. return ~column.in_(value)
  112. elif operator == 'eq':
  113. return column == value
  114. elif operator == 'like':
  115. return column.ilike(f"%{value}%")
  116. else:
  117. raise ValueError(f"Unsupported operator: {operator}")
  118. def combine_conditions(conditions, logical_operator):
  119. if len(conditions) > 1:
  120. if logical_operator == 'or':
  121. return or_(*conditions)
  122. elif logical_operator == 'not':
  123. return not_(and_(*conditions))
  124. else: # Default to 'and'
  125. return and_(*conditions)
  126. elif conditions:
  127. return conditions[0]
  128. else:
  129. return None
  130. def get_filtered_alerts(
  131. start_date: str = None,
  132. end_date: str = None,
  133. source_start_date: str = None,
  134. source_end_date: str = None,
  135. title: str = None,
  136. description: str = None,
  137. status: int = None,
  138. severity: int = None,
  139. owner: int = None,
  140. source: str = None,
  141. tags: str = None,
  142. case_id: int = None,
  143. client: int = None,
  144. classification: int = None,
  145. alert_ids: List[int] = None,
  146. assets: List[str] = None,
  147. iocs: List[str] = None,
  148. resolution_status: List[int] = None,
  149. logical_operator: str = 'and', # Logical operator: 'and', 'or', 'not'
  150. page: int = 1,
  151. per_page: int = 10,
  152. sort: str = 'desc',
  153. current_user_id: int = None,
  154. source_reference=None,
  155. custom_conditions: List[dict] = None):
  156. """
  157. Get a list of alerts that match the given filter conditions
  158. args:
  159. start_date (datetime): The start date of the alert creation time
  160. end_date (datetime): The end date of the alert creation time
  161. title (str): The title of the alert
  162. description (str): The description of the alert
  163. status (str): The status of the alert
  164. severity (str): The severity of the alert
  165. owner (str): The owner of the alert
  166. source (str): The source of the alert
  167. tags (str): The tags of the alert
  168. case_id (int): The case id of the alert
  169. client (int): The client id of the alert
  170. classification (int): The classification id of the alert
  171. alert_ids (int): The alert ids
  172. assets (list): The assets of the alert
  173. iocs (list): The iocs of the alert
  174. resolution_status (list): The resolution status of the alert
  175. logical_operator (str): Logical operator to combine conditions ('and', 'or', 'not')
  176. page (int): The page number
  177. per_page (int): The number of alerts per page
  178. sort (str): The sort order
  179. current_user_id (int): The ID of the current user
  180. source_reference (str): Alert source reference
  181. custom_conditions (list): Custom conditions to be applied (e.g., NOT client AND owner_id in [1,2,3])
  182. returns:
  183. list: A list of alerts that match the given filter conditions
  184. ...
  185. fields (List[str]): The list of fields to include in the output
  186. returns:
  187. dict: Dictionary with pagination info and list of serialized alerts
  188. """
  189. conditions = []
  190. if start_date is not None and end_date is not None:
  191. start_date = parse_bf_date_format(start_date)
  192. end_date = parse_bf_date_format(end_date)
  193. if start_date and end_date:
  194. conditions.append(Alert.alert_creation_time.between(start_date, end_date))
  195. if source_start_date is not None and source_end_date is not None:
  196. source_start_date = parse_bf_date_format(source_start_date)
  197. source_end_date = parse_bf_date_format(source_end_date)
  198. if source_start_date and source_end_date:
  199. conditions.append(Alert.alert_source_event_time.between(source_start_date, source_end_date))
  200. if title is not None:
  201. conditions.append(Alert.alert_title.ilike(f'%{title}%'))
  202. if description is not None:
  203. conditions.append(Alert.alert_description.ilike(f'%{description}%'))
  204. if status is not None:
  205. conditions.append(Alert.alert_status_id == status)
  206. if severity is not None:
  207. conditions.append(Alert.alert_severity_id == severity)
  208. if resolution_status is not None:
  209. if isinstance(resolution_status, list):
  210. conditions.append(not_(Alert.alert_resolution_status_id.in_(resolution_status)))
  211. else:
  212. conditions.append(Alert.alert_resolution_status_id == resolution_status)
  213. if source_reference is not None:
  214. conditions.append(Alert.alert_source_ref.like(f'%{source_reference}%'))
  215. if owner is not None:
  216. if owner == -1:
  217. conditions.append(Alert.alert_owner_id.is_(None))
  218. else:
  219. conditions.append(Alert.alert_owner_id == owner)
  220. if source is not None:
  221. conditions.append(Alert.alert_source.ilike(f'%{source}%'))
  222. if tags is not None:
  223. conditions.append(Alert.alert_tags.ilike(f"%{tags}%"))
  224. if client is not None:
  225. conditions.append(Alert.alert_customer_id == client)
  226. if alert_ids is not None:
  227. if isinstance(alert_ids, list):
  228. conditions.append(Alert.alert_id.in_(alert_ids))
  229. if classification is not None:
  230. conditions.append(Alert.alert_classification_id == classification)
  231. if case_id is not None:
  232. conditions.append(Alert.cases.any(AlertCaseAssociation.case_id == case_id))
  233. if assets is not None:
  234. if isinstance(assets, list):
  235. conditions.append(Alert.assets.any(CaseAssets.asset_name.in_(assets)))
  236. if iocs is not None:
  237. if isinstance(iocs, list):
  238. conditions.append(Alert.iocs.any(Ioc.ioc_value.in_(iocs)))
  239. if current_user_id is not None and not ac_current_user_has_permission(Permissions.server_administrator):
  240. clients_filters = get_user_clients_id(current_user_id)
  241. if clients_filters is not None:
  242. conditions.append(Alert.alert_customer_id.in_(clients_filters))
  243. query = db.session.query(
  244. Alert
  245. ).options(
  246. selectinload(Alert.severity),
  247. selectinload(Alert.status),
  248. selectinload(Alert.customer),
  249. selectinload(Alert.cases),
  250. selectinload(Alert.iocs),
  251. selectinload(Alert.assets)
  252. )
  253. # Apply custom conditions if provided
  254. if custom_conditions:
  255. if isinstance(custom_conditions, str):
  256. try:
  257. custom_conditions = json.loads(custom_conditions)
  258. except:
  259. app.app.logger.exception(f"Error parsing custom_conditions: {custom_conditions}")
  260. return
  261. # Keep track of which relationships we've already joined
  262. joined_relationships = set()
  263. for custom_condition in custom_conditions:
  264. field_path = custom_condition['field']
  265. operator = custom_condition['operator']
  266. value = custom_condition['value']
  267. # Check if we need to handle a related field
  268. if '.' in field_path:
  269. relationship_name, related_field_name = field_path.split('.', 1)
  270. # Ensure the relationship name is known
  271. if relationship_name not in relationship_model_map:
  272. raise ValueError(f"Unknown relationship: {relationship_name}")
  273. if related_field_name in RESTRICTED_USER_FIELDS:
  274. app.logger.error(f"Access to the field '{related_field_name}' is restricted.")
  275. app.logger.error(f"Suspicious behavior detected for user {current_user.id} - {current_user.user}.")
  276. continue
  277. related_model = relationship_model_map[relationship_name]
  278. # Join the relationship if not already joined
  279. if relationship_name not in joined_relationships:
  280. query = query.join(getattr(Alert, relationship_name))
  281. joined_relationships.add(relationship_name)
  282. related_field = getattr(related_model, related_field_name, None)
  283. if related_field is None:
  284. raise ValueError(
  285. f"Field '{related_field_name}' not found in related model '{related_model.__name__}'")
  286. # Build the condition
  287. condition = build_condition(related_field, operator, value)
  288. conditions.append(condition)
  289. else:
  290. # Field belongs to Alert model
  291. field = getattr(Alert, field_path, None)
  292. if field is None:
  293. raise ValueError(f"Field '{field_path}' not found in Alert model")
  294. condition = build_condition(field, operator, value)
  295. conditions.append(condition)
  296. # Combine conditions
  297. combined_conditions = combine_conditions(conditions, logical_operator)
  298. order_func = desc if sort == "desc" else asc
  299. try:
  300. # Query the alerts using the filter conditions
  301. if combined_conditions is not None:
  302. query = query.filter(combined_conditions)
  303. filtered_alerts = query.order_by(
  304. order_func(Alert.alert_source_event_time)
  305. ).paginate(page=page, per_page=per_page, error_out=False)
  306. return filtered_alerts
  307. except Exception as e:
  308. app.app.logger.exception(f"Error getting alerts: {str(e)}")
  309. return None
  310. def add_alert(
  311. title,
  312. description,
  313. source,
  314. status,
  315. severity,
  316. owner
  317. ):
  318. """
  319. Add an alert to the database
  320. args:
  321. title (str): The title of the alert
  322. description (str): The description of the alert
  323. source (str): The source of the alert
  324. status (str): The status of the alert
  325. severity (str): The severity of the alert
  326. owner (str): The owner of the alert
  327. returns:
  328. Alert: The alert that was added to the database
  329. """
  330. # Create the alert
  331. alert = Alert()
  332. alert.alert_title = title
  333. alert.alert_description = description
  334. alert.alert_source = source
  335. alert.alert_status = status
  336. alert.alert_severity = severity
  337. alert.alert_owner_id = owner
  338. # Add the alert to the database
  339. db.session.add(alert)
  340. db.session.commit()
  341. return alert
  342. def get_alert_by_id(alert_id: int) -> Alert:
  343. """
  344. Get an alert from the database
  345. args:
  346. alert_id (int): The ID of the alert
  347. returns:
  348. Alert: The alert that was retrieved from the database
  349. """
  350. return (
  351. db.session.query(Alert)
  352. .options(selectinload(Alert.iocs), selectinload(Alert.assets))
  353. .filter(Alert.alert_id == alert_id)
  354. .first()
  355. )
  356. def get_unspecified_event_category():
  357. """
  358. Get the id of the 'Unspecified' event category
  359. """
  360. event_cat = EventCategory.query.filter(
  361. EventCategory.name == 'Unspecified'
  362. ).first()
  363. return event_cat
  364. def create_case_from_alerts(alerts: List[Alert], iocs_list: List[str], assets_list: List[str], case_title: str,
  365. note: str, import_as_event: bool, case_tags: str, template_id: int) -> Cases:
  366. """
  367. Create a case from multiple alerts
  368. args:
  369. alerts (Alert): The Alerts
  370. iocs_list (list): The list of IOCs
  371. assets_list (list): The list of assets
  372. note (str): The note to add to the case
  373. import_as_event (bool): Whether to import the alert as an event
  374. case_tags (str): The tags to add to the case
  375. case_title (str): The title of the case
  376. template_id (int): The ID of the template to use
  377. returns:
  378. Cases: The case that was created from the alert
  379. """
  380. escalation_note = ""
  381. if note:
  382. escalation_note = f"\n\n### Escalation note\n\n{note}\n\n"
  383. if template_id is not None and template_id != 0 and template_id != '':
  384. case_template = get_case_template_by_id(template_id)
  385. if case_template:
  386. case_template_title_prefix = case_template.title_prefix
  387. # Create the case
  388. case = Cases(
  389. name=f"[ALERT]{case_template_title_prefix} "
  390. f"Merge of alerts {', '.join([str(alert.alert_id) for alert in alerts])}" if not case_title else
  391. f"{case_template_title_prefix} {case_title}",
  392. description=f"*Alerts escalated by {current_user.name}*\n\n{escalation_note}"
  393. f"[Alerts link](/alerts?alert_ids={','.join([str(alert.alert_id) for alert in alerts])})",
  394. soc_id='',
  395. client_id=alerts[0].alert_customer_id,
  396. user=current_user,
  397. classification_id=alerts[0].alert_classification_id,
  398. state_id=get_case_state_by_name('Open').state_id
  399. )
  400. case.save()
  401. for tag in case_tags.split(','):
  402. tag = Tags(tag_title=tag)
  403. tag = tag.save()
  404. case.tags.append(tag)
  405. db.session.commit()
  406. # Link the alert to the case
  407. for alert in alerts:
  408. alert.cases.append(case)
  409. ioc_links = []
  410. asset_links = []
  411. # Add the IOCs to the case
  412. for ioc_uuid in iocs_list:
  413. for alert_ioc in alert.iocs:
  414. if str(alert_ioc.ioc_uuid) == ioc_uuid:
  415. add_ioc(alert_ioc, current_user.id, case.case_id)
  416. ioc_links.append(alert_ioc.ioc_id)
  417. # Add the assets to the case
  418. for asset_uuid in assets_list:
  419. for alert_asset in alert.assets:
  420. if str(alert_asset.asset_uuid) == asset_uuid:
  421. alert_asset.analysis_status_id = get_unspecified_analysis_status_id()
  422. asset = create_asset(asset=alert_asset,
  423. caseid=case.case_id,
  424. user_id=current_user.id
  425. )
  426. asset.asset_uuid = alert_asset.asset_uuid
  427. set_ioc_links(ioc_links, asset.asset_id)
  428. asset_links.append(asset.asset_id)
  429. # Add event to timeline
  430. if import_as_event:
  431. unspecified_cat = get_unspecified_event_category()
  432. event_schema = EventSchema()
  433. event = event_schema.load({
  434. 'event_title': f"[ALERT] {alert.alert_title}",
  435. 'event_content': alert.alert_description,
  436. 'event_source': alert.alert_source,
  437. 'event_raw': json.dumps(alert.alert_source_content, indent=4),
  438. 'event_date': alert.alert_source_event_time.strftime("%Y-%m-%dT%H:%M:%S.%f"),
  439. 'event_date_wtz': alert.alert_source_event_time.strftime("%Y-%m-%dT%H:%M:%S.%f"),
  440. 'event_iocs': ioc_links,
  441. 'event_assets': asset_links,
  442. 'event_tags': alert.alert_tags,
  443. 'event_tz': '+00:00',
  444. 'event_category_id': unspecified_cat.id,
  445. }, session=db.session)
  446. event.case_id = case.case_id
  447. event.user_id = current_user.id
  448. event.event_added = datetime.utcnow()
  449. add_obj_history_entry(event, 'created')
  450. db.session.add(event)
  451. update_timeline_state(caseid=case.case_id)
  452. event.category = [unspecified_cat]
  453. update_event_assets(event_id=event.event_id,
  454. caseid=case.case_id,
  455. assets_list=asset_links,
  456. iocs_list=ioc_links,
  457. sync_iocs_assets=False)
  458. update_event_iocs(event_id=event.event_id,
  459. caseid=case.case_id,
  460. iocs_list=ioc_links)
  461. if template_id is not None and template_id != 0 and template_id != '':
  462. case, logs = case_template_post_modifier(case, template_id)
  463. db.session.commit()
  464. return case
  465. def create_case_from_alert(alert: Alert, iocs_list: List[str], assets_list: List[str], case_title: str,
  466. note: str, import_as_event: bool, case_tags: str, template_id: int) -> Cases:
  467. """
  468. Create a case from an alert
  469. args:
  470. alert (Alert): The Alert
  471. iocs_list (list): The list of IOCs
  472. assets_list (list): The list of assets
  473. note (str): The note to add to the case
  474. import_as_event (bool): Whether to import the alert as an event
  475. case_tags (str): The tags to add to the case
  476. case_title (str): The title of the case
  477. template_id (int): The template to use for the case
  478. returns:
  479. Cases: The case that was created from the alert
  480. """
  481. escalation_note = ""
  482. if note:
  483. escalation_note = f"\n\n### Escalation note\n\n{note}\n\n"
  484. case_template_title_prefix = ""
  485. if template_id is not None and template_id != 0 and template_id != '':
  486. case_template = get_case_template_by_id(template_id)
  487. if case_template:
  488. case_template_title_prefix = case_template.title_prefix
  489. # Create the case
  490. case = Cases(
  491. name=f"[ALERT]{case_template_title_prefix} {alert.alert_title}" if not case_title else f"{case_template_title_prefix} {case_title}",
  492. description=f"*Alert escalated by {current_user.name}*\n\n{escalation_note}"
  493. f"### Alert description\n\n{alert.alert_description}"
  494. f"\n\n### IRIS alert link\n\n"
  495. f"[<i class='fa-solid fa-bell'></i> #{alert.alert_id}](/alerts?alert_ids={alert.alert_id})",
  496. soc_id=alert.alert_id,
  497. client_id=alert.alert_customer_id,
  498. user=current_user,
  499. classification_id=alert.alert_classification_id,
  500. state_id=get_case_state_by_name('Open').state_id
  501. )
  502. case.save()
  503. for tag in case_tags.split(','):
  504. tag = Tags(tag_title=tag)
  505. tag = tag.save()
  506. case.tags.append(tag)
  507. case.severity_id = alert.alert_severity_id
  508. db.session.commit()
  509. # Link the alert to the case
  510. alert.cases.append(case)
  511. ioc_links = []
  512. asset_links = []
  513. # Add the IOCs to the case
  514. for ioc_uuid in iocs_list:
  515. for alert_ioc in alert.iocs:
  516. if str(alert_ioc.ioc_uuid) == ioc_uuid:
  517. # Make sure we don't have an existing IOC already
  518. tmp_ioc = Ioc.query.filter(
  519. Ioc.case_id == case.case_id,
  520. Ioc.ioc_value == alert_ioc.ioc_value,
  521. Ioc.ioc_type_id == alert_ioc.ioc_type_id
  522. ).first()
  523. if tmp_ioc:
  524. # Skip as we already have it in the case
  525. ioc_links.append(tmp_ioc.ioc_id)
  526. continue
  527. if alert_ioc.case_id is not None:
  528. # Make a deep copy of the ioc
  529. # prevent the ioc to conflict with the existing ioc
  530. new_alert_ioc = deepcopy(alert_ioc)
  531. make_transient(new_alert_ioc)
  532. new_alert_ioc.ioc_id = None
  533. new_alert_ioc.ioc_uuid = ioc_uuid
  534. new_alert_ioc.user_id = current_user.id
  535. new_alert_ioc.case_id = case.case_id
  536. db.session.add(new_alert_ioc)
  537. db.session.commit()
  538. alert_ioc = new_alert_ioc
  539. add_ioc(alert_ioc, current_user.id, case.case_id)
  540. ioc_links.append(alert_ioc.ioc_id)
  541. # Add the assets to the case
  542. for asset_uuid in assets_list:
  543. for alert_asset in alert.assets:
  544. if str(alert_asset.asset_uuid) == asset_uuid:
  545. alert_asset.analysis_status_id = get_unspecified_analysis_status_id()
  546. if alert_asset.case_id is not None:
  547. # Make a deep copy of the asset
  548. # prevent the asset to conflict with the existing asset
  549. new_alert_asset = deepcopy(alert_asset)
  550. make_transient(new_alert_asset)
  551. new_alert_asset.asset_id = None
  552. new_alert_asset.asset_uuid = asset_uuid
  553. db.session.add(new_alert_asset)
  554. db.session.commit()
  555. alert_asset = new_alert_asset
  556. asset = create_asset(asset=alert_asset,
  557. caseid=case.case_id,
  558. user_id=current_user.id
  559. )
  560. asset.asset_uuid = alert_asset.asset_uuid
  561. set_ioc_links(ioc_links, asset.asset_id)
  562. asset_links.append(asset.asset_id)
  563. # Add event to timeline
  564. if import_as_event:
  565. unspecified_cat = get_unspecified_event_category()
  566. event_schema = EventSchema()
  567. event = event_schema.load({
  568. 'event_title': f"[ALERT] {alert.alert_title}",
  569. 'event_content': alert.alert_description if alert.alert_description else "",
  570. 'event_source': alert.alert_source if alert.alert_source else "",
  571. 'event_raw': json.dumps(alert.alert_source_content, indent=4) if alert.alert_source_content else "",
  572. 'event_date': alert.alert_source_event_time.strftime("%Y-%m-%dT%H:%M:%S.%f") if alert.alert_source_event_time else datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f"),
  573. 'event_date_wtz': alert.alert_source_event_time.strftime("%Y-%m-%dT%H:%M:%S.%f") if alert.alert_source_event_time else datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f"),
  574. 'event_iocs': ioc_links,
  575. 'event_assets': asset_links,
  576. 'event_tags': alert.alert_tags + ',alert' if alert.alert_tags else "alert",
  577. 'event_tz': '+00:00',
  578. 'event_category_id': unspecified_cat.id,
  579. 'event_in_graph': True,
  580. 'event_in_summary': True
  581. }, session=db.session)
  582. event.case_id = case.case_id
  583. event.user_id = current_user.id
  584. event.event_added = datetime.utcnow()
  585. add_obj_history_entry(event, 'created')
  586. db.session.add(event)
  587. update_timeline_state(caseid=case.case_id)
  588. event.category = [unspecified_cat]
  589. update_event_assets(event_id=event.event_id,
  590. caseid=case.case_id,
  591. assets_list=asset_links,
  592. iocs_list=ioc_links,
  593. sync_iocs_assets=False)
  594. update_event_iocs(event_id=event.event_id,
  595. caseid=case.case_id,
  596. iocs_list=ioc_links)
  597. if template_id is not None and template_id != 0 and template_id != '':
  598. case, logs = case_template_post_modifier(case, template_id)
  599. db.session.commit()
  600. return case
  601. def merge_alert_in_case(alert: Alert, case: Cases, iocs_list: List[str],
  602. assets_list: List[str], note: str, import_as_event: bool, case_tags: str):
  603. """
  604. Merge an alert in a case
  605. args:
  606. alert (Alert): The Alert
  607. case (Cases): The Case
  608. iocs_list (list): The list of IOCs
  609. case_title (str): The title of the case
  610. assets_list (list): The list of assets
  611. note (str): The note to add to the case
  612. import_as_event (bool): Whether to import the alert as an event
  613. case_tags (str): The tags to add to the case
  614. """
  615. if case in alert.cases:
  616. return case
  617. escalation_note = ""
  618. if note:
  619. escalation_note = f"\n\n### Escalation note\n\n{note}\n\n"
  620. case.description += f"\n\n*Alert [#{alert.alert_id}](/alerts?alert_ids={alert.alert_id}) escalated by {current_user.name}*\n\n{escalation_note}"
  621. for tag in case_tags.split(',') if case_tags else []:
  622. tag = Tags(tag_title=tag).save()
  623. case.tags.append(tag)
  624. # Link the alert to the case
  625. alert.cases.append(case)
  626. ioc_links = []
  627. asset_links = []
  628. # Add the IOCs to the case
  629. for ioc_uuid in iocs_list:
  630. for alert_ioc in alert.iocs:
  631. if str(alert_ioc.ioc_uuid) == ioc_uuid:
  632. tmp_ioc = Ioc.query.filter(
  633. Ioc.case_id == case.case_id,
  634. Ioc.ioc_value == alert_ioc.ioc_value,
  635. Ioc.ioc_type_id == alert_ioc.ioc_type_id
  636. ).first()
  637. if tmp_ioc:
  638. alert_ioc = tmp_ioc
  639. add_ioc(alert_ioc, current_user.id, case.case_id)
  640. ioc_links.append(alert_ioc.ioc_id)
  641. # Add the assets to the case
  642. for asset_uuid in assets_list:
  643. for alert_asset in alert.assets:
  644. # Filter selected assets by the user
  645. if str(alert_asset.asset_uuid) == asset_uuid:
  646. alert_asset.analysis_status_id = get_unspecified_analysis_status_id()
  647. # Check if the asset exists already in the case
  648. tmp_asset = CaseAssets.query.filter(and_(
  649. CaseAssets.asset_name == alert_asset.asset_name,
  650. CaseAssets.asset_type_id == alert_asset.asset_type_id,
  651. CaseAssets.case_id == case.case_id
  652. )).first()
  653. if tmp_asset:
  654. asset = tmp_asset
  655. else:
  656. asset = create_asset(asset=alert_asset,
  657. caseid=case.case_id,
  658. user_id=current_user.id
  659. )
  660. set_ioc_links(ioc_links, asset.asset_id)
  661. asset_links.append(asset.asset_id)
  662. # Add event to timeline
  663. if import_as_event:
  664. unspecified_cat = get_unspecified_event_category()
  665. event_schema = EventSchema()
  666. event = event_schema.load({
  667. 'event_title': f"[ALERT] {alert.alert_title}",
  668. 'event_content': alert.alert_description,
  669. 'event_source': alert.alert_source,
  670. 'event_raw': json.dumps(alert.alert_source_content, indent=4),
  671. 'event_date': alert.alert_source_event_time.strftime("%Y-%m-%dT%H:%M:%S.%f"),
  672. 'event_date_wtz': alert.alert_source_event_time.strftime("%Y-%m-%dT%H:%M:%S.%f"),
  673. 'event_iocs': ioc_links,
  674. 'event_assets': asset_links,
  675. 'event_tags': alert.alert_tags,
  676. 'event_tz': '+00:00',
  677. 'event_category_id': unspecified_cat.id,
  678. 'event_in_graph': True,
  679. 'event_in_summary': True
  680. }, session=db.session)
  681. event.case_id = case.case_id
  682. event.user_id = current_user.id
  683. event.event_added = datetime.utcnow()
  684. add_obj_history_entry(event, 'created')
  685. db.session.add(event)
  686. update_timeline_state(caseid=case.case_id)
  687. event.category = [unspecified_cat]
  688. update_event_assets(event_id=event.event_id,
  689. caseid=case.case_id,
  690. assets_list=asset_links,
  691. iocs_list=ioc_links,
  692. sync_iocs_assets=False)
  693. update_event_iocs(event_id=event.event_id,
  694. caseid=case.case_id,
  695. iocs_list=ioc_links)
  696. db.session.commit()
  697. def unmerge_alert_from_case(alert: Alert, case: Cases):
  698. """
  699. Unmerge an alert from a case
  700. args:
  701. alert (Alert): The Alert
  702. case (Cases): The Case
  703. """
  704. # Check if the case is in the alert.cases list
  705. if case in alert.cases:
  706. # Unlink the alert from the case
  707. alert.cases.remove(case)
  708. db.session.commit()
  709. else:
  710. return False, f"Case {case.case_id} not linked with alert {alert.alert_id}"
  711. return True, f"Alert {alert.alert_id} unlinked from case {case.case_id}"
  712. def get_alert_status_list():
  713. """
  714. Get a list of alert statuses
  715. returns:
  716. list: A list of alert statuses
  717. """
  718. return db.session.query(AlertStatus).distinct().all()
  719. def get_alert_status_by_id(status_id: int) -> AlertStatus:
  720. """
  721. Get an alert status from the database
  722. args:
  723. status_id (int): The ID of the alert status
  724. returns:
  725. AlertStatus: The alert status that was retrieved from the database
  726. """
  727. return db.session.query(AlertStatus).filter(AlertStatus.status_id == status_id).first()
  728. def search_alert_status_by_name(status_name: str, exact_match: False) -> AlertStatus:
  729. """
  730. Get an alert status from the database from its name
  731. args:
  732. status_name (str): The name of the alert status
  733. exact_match (bool): Whether to perform an exact match or not
  734. returns:
  735. AlertStatus: The alert status that was retrieved from the database
  736. """
  737. if exact_match:
  738. return db.session.query(AlertStatus).filter(func.lower(AlertStatus.status_name) == status_name.lower()).first()
  739. return db.session.query(AlertStatus).filter(AlertStatus.status_name.ilike(f"%{status_name}%")).all()
  740. def get_alert_resolution_list():
  741. """
  742. Get a list of alert resolutions
  743. returns:
  744. list: A list of alert resolutions
  745. """
  746. return db.session.query(AlertResolutionStatus).distinct().all()
  747. def get_alert_resolution_by_id(resolution_id: int) -> AlertResolutionStatus:
  748. """
  749. Get an alert resolution from the database
  750. args:
  751. resolution_id (int): The ID of the alert resolution
  752. returns:
  753. Alertresolution: The alert resolution that was retrieved from the database
  754. """
  755. return db.session.query(AlertResolutionStatus).filter(AlertResolutionStatus.resolution_status_id == resolution_id).first()
  756. def search_alert_resolution_by_name(resolution_status_name: str, exact_match: False) -> AlertResolutionStatus:
  757. """
  758. Get an alert resolution from the database from its name
  759. args:
  760. resolution_name (str): The name of the alert resolution
  761. exact_match (bool): Whether to perform an exact match or not
  762. returns:
  763. Alertresolution: The alert resolution that was retrieved from the database
  764. """
  765. if exact_match:
  766. return db.session.query(AlertResolutionStatus).filter(func.lower(
  767. AlertResolutionStatus.resolution_status_name) == resolution_status_name.lower()).first()
  768. return db.session.query(AlertResolutionStatus).filter(
  769. AlertResolutionStatus.resolution_status_name.ilike(f"%{resolution_status_name}%")).all()
  770. def cache_similar_alert(customer_id, assets, iocs, alert_id, creation_date):
  771. """
  772. Cache similar alerts
  773. args:
  774. customer_id (int): The ID of the customer
  775. assets (list): The list of assets
  776. iocs (list): The list of IOCs
  777. alert_id (int): The ID of the alert
  778. creation_date (datetime): The creation date of the alert
  779. returns:
  780. None
  781. """
  782. for asset in assets:
  783. cache_entry = SimilarAlertsCache(customer_id=customer_id, asset_name=asset['asset_name'],
  784. asset_type_id=asset["asset_type_id"], alert_id=alert_id,
  785. created_at=creation_date)
  786. db.session.add(cache_entry)
  787. for ioc in iocs:
  788. cache_entry = SimilarAlertsCache(customer_id=customer_id, ioc_value=ioc['ioc_value'],
  789. ioc_type_id=ioc['ioc_type_id'], alert_id=alert_id,
  790. created_at=creation_date)
  791. db.session.add(cache_entry)
  792. db.session.commit()
  793. def register_related_alerts(new_alert=None, assets_list=None, iocs_list=None):
  794. """
  795. Register related alerts
  796. """
  797. # Step 1: Identify similar alerts based on title, assets, and IOCs
  798. similar_alerts = db.session.query(Alert).filter(
  799. Alert.alert_customer_id == new_alert.alert_customer_id,
  800. Alert.alert_id != new_alert.alert_id,
  801. or_(
  802. Alert.alert_title == new_alert.alert_title,
  803. Alert.assets.any(CaseAssets.asset_name.in_([asset.asset_name for asset in new_alert.assets])),
  804. Alert.iocs.any(Ioc.ioc_value.in_([ioc.ioc_value for ioc in new_alert.iocs]))
  805. )
  806. ).all()
  807. # Step 2: Create relationships in the AlertSimilarity table
  808. for similar_alert in similar_alerts:
  809. # Matching on title
  810. if new_alert.alert_title == similar_alert.alert_title:
  811. alert_similarity = AlertSimilarity(
  812. alert_id=new_alert.alert_id,
  813. similar_alert_id=similar_alert.alert_id,
  814. similarity_type="title_match"
  815. )
  816. db.session.add(alert_similarity)
  817. # Matching on assets
  818. for asset in new_alert.assets:
  819. if asset in similar_alert.assets:
  820. alert_similarity = AlertSimilarity(
  821. alert_id=new_alert.alert_id,
  822. similar_alert_id=similar_alert.alert_id,
  823. similarity_type="asset_match",
  824. matching_asset_id=asset.asset_id
  825. )
  826. db.session.add(alert_similarity)
  827. # Matching on IOCs
  828. for ioc in new_alert.iocs:
  829. if ioc in similar_alert.iocs:
  830. alert_similarity = AlertSimilarity(
  831. alert_id=new_alert.alert_id,
  832. similar_alert_id=similar_alert.alert_id,
  833. similarity_type="ioc_match",
  834. matching_ioc_id=ioc.ioc_id
  835. )
  836. db.session.add(alert_similarity)
  837. def delete_similar_alert_cache(alert_id):
  838. """
  839. Delete the similar alert cache
  840. args:
  841. alert_id (int): The ID of the alert
  842. returns:
  843. None
  844. """
  845. SimilarAlertsCache.query.filter(SimilarAlertsCache.alert_id == alert_id).delete()
  846. db.session.commit()
  847. def delete_related_alert_cache(alert_id):
  848. """
  849. Delete the related alerts cache
  850. args:
  851. alert_id (int): The ID of the alert
  852. returns:
  853. None
  854. """
  855. AlertSimilarity.query.filter(
  856. or_(
  857. AlertSimilarity.alert_id == alert_id,
  858. AlertSimilarity.similar_alert_id == alert_id
  859. )
  860. ).delete()
  861. db.session.commit()
  862. def delete_similar_alerts_cache(alert_ids: List[int]):
  863. """
  864. Delete the similar alerts cache
  865. args:
  866. alert_ids (List(int)): The ID of the alert
  867. returns:
  868. None
  869. """
  870. SimilarAlertsCache.query.filter(SimilarAlertsCache.alert_id.in_(alert_ids)).delete()
  871. db.session.commit()
  872. def delete_related_alerts_cache(alert_ids: List[int]):
  873. """
  874. Delete the related alerts cache
  875. args:
  876. alert_ids (List(int)): The ID of the alert
  877. returns:
  878. None
  879. """
  880. AlertSimilarity.query.filter(
  881. or_(
  882. AlertSimilarity.alert_id.in_(alert_ids),
  883. AlertSimilarity.similar_alert_id.in_(alert_ids)
  884. )
  885. ).delete()
  886. db.session.commit()
  887. def get_related_alerts(customer_id, assets, iocs, details=False):
  888. """
  889. Check if an alert is related to another alert
  890. args:
  891. customer_id (int): The ID of the customer
  892. assets (list): The list of assets
  893. iocs (list): The list of IOCs
  894. details (bool): Whether to return the details of the related alerts
  895. returns:
  896. bool: True if the alert is related to another alert, False otherwise
  897. """
  898. asset_names = [asset.asset_name for asset in assets]
  899. ioc_values = [ioc.ioc_value for ioc in iocs]
  900. similar_assets = SimilarAlertsCache.query.filter(
  901. SimilarAlertsCache.customer_id == customer_id,
  902. SimilarAlertsCache.asset_name.in_(asset_names)
  903. ).all()
  904. similar_iocs = SimilarAlertsCache.query.filter(
  905. SimilarAlertsCache.customer_id == customer_id,
  906. SimilarAlertsCache.ioc_value.in_(ioc_values)
  907. ).all()
  908. similarities = {
  909. 'assets': [asset.alert_id for asset in similar_assets],
  910. 'iocs': [ioc.alert_id for ioc in similar_iocs]
  911. }
  912. return similarities
  913. def get_related_alerts_details(customer_id, assets, iocs, open_alerts, closed_alerts, open_cases, closed_cases,
  914. days_back=30, number_of_results=200):
  915. """
  916. Get the details of the related alerts
  917. args:
  918. customer_id (int): The ID of the customer
  919. assets (list): The list of assets
  920. iocs (list): The list of IOCs
  921. open_alerts (bool): Include open alerts
  922. closed_alerts (bool): Include closed alerts
  923. open_cases (bool): Include open cases
  924. closed_cases (bool): Include closed cases
  925. days_back (int): The number of days to look back
  926. number_of_results (int): The maximum number of alerts to return
  927. returns:
  928. dict: The details of the related alerts with matched assets and/or IOCs
  929. """
  930. if not assets and not iocs:
  931. return {
  932. 'nodes': [],
  933. 'edges': []
  934. }
  935. asset_names = [(asset.asset_name, asset.asset_type_id) for asset in assets]
  936. ioc_values = [(ioc.ioc_value, ioc.ioc_type_id) for ioc in iocs]
  937. asset_type_alias = aliased(AssetsType)
  938. alert_status_filter = []
  939. if open_alerts:
  940. open_alert_status_ids = AlertStatus.query.with_entities(
  941. AlertStatus.status_id
  942. ).filter(AlertStatus.status_name.in_(['New', 'Assigned', 'In progress', 'Pending', 'Unspecified'])).all()
  943. alert_status_filter += [status_id[0] for status_id in open_alert_status_ids]
  944. if closed_alerts:
  945. closed_alert_status_ids = AlertStatus.query.with_entities(
  946. AlertStatus.status_id
  947. ).filter(AlertStatus.status_name.in_(['Closed', 'Merged', 'Escalated'])).all()
  948. alert_status_filter += [status_id[0] for status_id in closed_alert_status_ids]
  949. conditions = and_(
  950. SimilarAlertsCache.customer_id == customer_id,
  951. and_(or_(
  952. tuple_(SimilarAlertsCache.asset_name, SimilarAlertsCache.asset_type_id).in_(asset_names),
  953. tuple_(SimilarAlertsCache.ioc_value, SimilarAlertsCache.ioc_type_id).in_(ioc_values)
  954. ),
  955. SimilarAlertsCache.created_at >= (func.now() - timedelta(days=days_back))
  956. )
  957. )
  958. if alert_status_filter:
  959. conditions = and_(conditions, Alert.alert_status_id.in_(alert_status_filter))
  960. related_alerts = (
  961. db.session.query(Alert, SimilarAlertsCache.asset_name, SimilarAlertsCache.ioc_value,
  962. asset_type_alias.asset_icon_not_compromised)
  963. .join(SimilarAlertsCache, Alert.alert_id == SimilarAlertsCache.alert_id)
  964. .outerjoin(Alert.resolution_status)
  965. .outerjoin(asset_type_alias, SimilarAlertsCache.asset_type_id == asset_type_alias.asset_id)
  966. .filter(conditions)
  967. .limit(number_of_results)
  968. .all()
  969. )
  970. alerts_dict = {}
  971. for alert, asset_name, ioc_value, asset_icon_not_compromised in related_alerts:
  972. if alert.alert_id not in alerts_dict:
  973. alerts_dict[alert.alert_id] = {'alert': alert, 'assets': [], 'iocs': []}
  974. if any(name == asset_name for name, _ in asset_names):
  975. asset_info = {'asset_name': asset_name, 'icon': asset_icon_not_compromised}
  976. alerts_dict[alert.alert_id]['assets'].append(asset_info)
  977. if any(value == ioc_value for value, _ in ioc_values):
  978. alerts_dict[alert.alert_id]['iocs'].append(ioc_value)
  979. nodes = []
  980. edges = []
  981. added_assets = set()
  982. added_iocs = set()
  983. added_cases = set()
  984. for alert_id, alert_info in alerts_dict.items():
  985. alert_color = '#c95029' if alert_info['alert'].status.status_name in ['Closed', 'Merged', 'Escalated'] else ''
  986. alert_resolution_title = f'[{alert_info["alert"].resolution_status.resolution_status_name}]\n' if alert_info["alert"].resolution_status else ""
  987. nodes.append({
  988. 'id': f'alert_{alert_id}',
  989. 'label': f'[Closed]{alert_resolution_title} {alert_info["alert"].alert_title}' if alert_color != '' else f'{alert_resolution_title}{alert_info["alert"].alert_title}',
  990. 'title': f'{alert_info["alert"].alert_description}',
  991. 'group': 'alert',
  992. 'shape': 'icon',
  993. 'icon': {
  994. 'face': 'FontAwesome',
  995. 'code': '\uf0f3',
  996. 'color': alert_color,
  997. 'weight': "bold"
  998. },
  999. 'font': "12px verdana white" if current_user.in_dark_mode else ''
  1000. })
  1001. for asset_info in alert_info['assets']:
  1002. asset_id = asset_info['asset_name']
  1003. if asset_id not in added_assets:
  1004. nodes.append({
  1005. 'id': f'asset_{asset_id}',
  1006. 'label': asset_id,
  1007. 'group': 'asset',
  1008. 'shape': 'image',
  1009. 'image': '/static/assets/img/graph/' + asset_info['icon'],
  1010. 'font': "12px verdana white" if current_user.in_dark_mode else ''
  1011. })
  1012. added_assets.add(asset_id)
  1013. edges.append({
  1014. 'from': f'alert_{alert_id}',
  1015. 'to': f'asset_{asset_id}'
  1016. })
  1017. for ioc_value in alert_info['iocs']:
  1018. if ioc_value not in added_iocs:
  1019. nodes.append({
  1020. 'id': f'ioc_{ioc_value}',
  1021. 'label': ioc_value,
  1022. 'group': 'ioc',
  1023. 'shape': 'icon',
  1024. 'icon': {
  1025. 'face': 'FontAwesome',
  1026. 'code': '\ue4a8',
  1027. 'color': 'white' if current_user.in_dark_mode else '',
  1028. 'weight': "bold"
  1029. },
  1030. 'font': "12px verdana white" if current_user.in_dark_mode else ''
  1031. })
  1032. added_iocs.add(ioc_value)
  1033. edges.append({
  1034. 'from': f'alert_{alert_id}',
  1035. 'to': f'ioc_{ioc_value}',
  1036. 'dashes': True
  1037. })
  1038. if open_cases or closed_cases:
  1039. close_condition = None
  1040. if open_cases and not closed_cases:
  1041. close_condition = Cases.close_date.is_(None)
  1042. if closed_cases and not open_cases:
  1043. close_condition = Cases.close_date.isnot(None)
  1044. if open_cases and closed_cases:
  1045. close_condition = Cases.close_date.isnot(None) | Cases.close_date.is_(None)
  1046. matching_ioc_cases = (
  1047. db.session.query(Ioc)
  1048. .with_entities(Ioc.case_id, Ioc.ioc_value, Cases.name, Cases.close_date, Cases.description)
  1049. .join(Ioc.case)
  1050. .filter(
  1051. and_(
  1052. and_(
  1053. Ioc.ioc_value.in_(added_iocs),
  1054. close_condition,
  1055. ),
  1056. Cases.client_id == customer_id
  1057. )
  1058. )
  1059. .distinct()
  1060. .all()
  1061. )
  1062. matching_asset_cases = (
  1063. db.session.query(CaseAssets)
  1064. .with_entities(CaseAssets.case_id, CaseAssets.asset_name, Cases.name, Cases.close_date, Cases.description)
  1065. .join(CaseAssets.case)
  1066. .filter(
  1067. and_(
  1068. and_(
  1069. CaseAssets.asset_name.in_(added_assets),
  1070. close_condition
  1071. ),
  1072. Cases.client_id == customer_id
  1073. )
  1074. )
  1075. .distinct(CaseAssets.case_id)
  1076. .all()
  1077. )
  1078. cases_data = {}
  1079. for case_id, ioc_value, case_name, close_date, case_desc in matching_ioc_cases:
  1080. if case_id not in cases_data:
  1081. cases_data[case_id] = {'name': case_name, 'matching_ioc': [], 'matching_assets': [],
  1082. 'close_date': close_date, 'description': case_desc}
  1083. cases_data[case_id]['matching_ioc'].append(ioc_value)
  1084. for case_id, asset_name, case_name, close_date, case_desc in matching_asset_cases:
  1085. if case_id not in cases_data:
  1086. cases_data[case_id] = {'name': case_name, 'matching_ioc': [], 'matching_assets': [],
  1087. 'close_date': close_date, 'description': case_desc}
  1088. cases_data[case_id]['matching_assets'].append(asset_name)
  1089. for case_id in cases_data:
  1090. if case_id not in added_cases:
  1091. nodes.append({
  1092. 'id': f'case_{case_id}',
  1093. 'label': f'[Closed] Case #{case_id}' if cases_data[case_id].get('close_date') else f'Case #{case_id}',
  1094. 'title': cases_data[case_id].get("description"),
  1095. 'group': 'case',
  1096. 'shape': 'icon',
  1097. 'icon': {
  1098. 'face': 'FontAwesome',
  1099. 'code': '\uf0b1',
  1100. 'color': '#c95029' if cases_data[case_id].get('close_date') else '#4cba4f'
  1101. },
  1102. 'font': "12px verdana white" if current_user.in_dark_mode else ''
  1103. })
  1104. added_cases.add(case_id)
  1105. for ioc_value in cases_data[case_id]['matching_ioc']:
  1106. edges.append({
  1107. 'from': f'ioc_{ioc_value}',
  1108. 'to': f'case_{case_id}',
  1109. 'dashes': True
  1110. })
  1111. for asset_name in cases_data[case_id]['matching_assets']:
  1112. edges.append({
  1113. 'from': f'asset_{asset_name}',
  1114. 'to': f'case_{case_id}',
  1115. 'dashes': True
  1116. })
  1117. return {
  1118. 'nodes': nodes,
  1119. 'edges': edges
  1120. }
  1121. def get_alert_comments(alert_id: int) -> List[Comments]:
  1122. """
  1123. Get the comments of an alert
  1124. args:
  1125. alert_id (int): The ID of the alert
  1126. returns:
  1127. list: The list of comments
  1128. """
  1129. return Comments.query.filter(Comments.comment_alert_id == alert_id).all()
  1130. def get_alert_comment(alert_id: int, comment_id: int) -> Comments:
  1131. """
  1132. Get a comment of an alert
  1133. args:
  1134. alert_id (int): The ID of the alert
  1135. comment_id (int): The ID of the comment
  1136. returns:
  1137. Comments: The comment
  1138. """
  1139. return Comments.query.filter(
  1140. Comments.comment_alert_id == alert_id,
  1141. Comments.comment_id == comment_id
  1142. ).first()
  1143. def delete_alert_comment(comment_id: int, alert_id: int) -> Tuple[bool, str]:
  1144. """
  1145. Delete a comment of an alert
  1146. args:
  1147. comment_id (int): The ID of the comment
  1148. """
  1149. comment = Comments.query.filter(
  1150. Comments.comment_id == comment_id,
  1151. Comments.comment_user_id == current_user.id,
  1152. Comments.comment_alert_id == alert_id
  1153. ).first()
  1154. if not comment:
  1155. return False, "You are not allowed to delete this comment"
  1156. db.session.delete(comment)
  1157. db.session.commit()
  1158. return True, "Comment deleted successfully"
  1159. def remove_alerts_from_assets_by_ids(alert_ids: List[int]) -> None:
  1160. """
  1161. Remove the alerts from the CaseAssets based on the alert_ids
  1162. args:
  1163. alert_ids (List[int]): list of alerts to remove
  1164. returns:
  1165. None
  1166. """
  1167. # Query the affected CaseAssets based on the alert_ids
  1168. affected_case_assets = (
  1169. db.session.query(CaseAssets)
  1170. .join(alert_assets_association)
  1171. .join(Alert, alert_assets_association.c.alert_id == Alert.alert_id)
  1172. .filter(Alert.alert_id.in_(alert_ids))
  1173. .all()
  1174. )
  1175. # Remove the alerts and delete the CaseAssets if not related to a case
  1176. for case_asset in affected_case_assets:
  1177. # Remove the alerts based on alert_ids
  1178. case_asset.alerts = [alert for alert in case_asset.alerts if alert.alert_id not in alert_ids]
  1179. # Delete the CaseAsset if it's not related to a case
  1180. if case_asset.case_id is None:
  1181. db.session.delete(case_asset)
  1182. # Commit the changes
  1183. db.session.commit()
  1184. def remove_alerts_from_iocs_by_ids(alert_ids: List[int]) -> None:
  1185. """
  1186. Remove the alerts from the Ioc based on the alert_ids
  1187. args:
  1188. alert_ids (List[int]): list of alerts to remove
  1189. returns:
  1190. None
  1191. """
  1192. # Query the affected CaseAssets based on the alert_ids
  1193. affected_case_iocs = (
  1194. db.session.query(Ioc)
  1195. .join(alert_iocs_association)
  1196. .join(Alert, alert_iocs_association.c.alert_id == Alert.alert_id)
  1197. .filter(Alert.alert_id.in_(alert_ids))
  1198. .all()
  1199. )
  1200. # Remove the alerts and delete the Ioc if not related to a case
  1201. for ioc in affected_case_iocs:
  1202. # Remove the alerts based on alert_ids
  1203. ioc.alerts = [alert for alert in ioc.alerts if alert.alert_id not in alert_ids]
  1204. # Commit the changes
  1205. db.session.commit()
  1206. def remove_case_alerts_by_ids(alert_ids: List[int]) -> None:
  1207. """
  1208. Remove the alerts from the Case based on the alert_ids
  1209. args:
  1210. alert_ids (List[int]): list of alerts to remove
  1211. returns:
  1212. None
  1213. """
  1214. affected_cases = (
  1215. db.session.query(Cases)
  1216. .join(AlertCaseAssociation)
  1217. .join(Alert, AlertCaseAssociation.alert_id == Alert.alert_id)
  1218. .filter(Alert.alert_id.in_(alert_ids))
  1219. .all()
  1220. )
  1221. for case in affected_cases:
  1222. # Remove the alerts based on alert_ids
  1223. case.alerts = [alert for alert in case.alerts if alert.alert_id not in alert_ids]
  1224. db.session.query(AlertCaseAssociation).filter(
  1225. AlertCaseAssociation.alert_id.in_(alert_ids)
  1226. ).delete(synchronize_session='fetch')
  1227. db.session.commit()
  1228. def delete_alerts(alert_ids: List[int]) -> tuple[bool, str]:
  1229. """
  1230. Delete multiples alerts from the database
  1231. args:
  1232. alert_ids (List[int]): list of alerts to delete
  1233. returns:
  1234. True if deleted successfully
  1235. """
  1236. try:
  1237. delete_similar_alerts_cache(alert_ids)
  1238. delete_related_alerts_cache(alert_ids)
  1239. remove_alerts_from_assets_by_ids(alert_ids)
  1240. remove_alerts_from_iocs_by_ids(alert_ids)
  1241. remove_case_alerts_by_ids(alert_ids)
  1242. Comments.query.filter(Comments.comment_alert_id.in_(alert_ids)).delete()
  1243. Alert.query.filter(Alert.alert_id.in_(alert_ids)).delete()
  1244. except Exception as e:
  1245. db.session.rollback()
  1246. app.logger.exception(str(e))
  1247. return False, "Server side error"
  1248. return True, ""
  1249. def get_alert_status_by_name(name: str) -> AlertStatus:
  1250. """
  1251. Get the alert status by name
  1252. args:
  1253. name (str): The name of the alert status
  1254. returns:
  1255. AlertStatus: The alert status
  1256. """
  1257. return AlertStatus.query.filter(AlertStatus.status_name == name).first()