Нет описания

alerts_routes.py 38KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110
  1. # IRIS Source Code
  2. # Copyright (C) 2024 - 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. import json
  19. import marshmallow
  20. from datetime import datetime
  21. from flask import Blueprint
  22. from flask import request
  23. from flask_login import current_user
  24. from typing import List
  25. from werkzeug import Response
  26. import app
  27. from app import db
  28. from app.blueprints.rest.endpoints import endpoint_deprecated
  29. from app.blueprints.rest.parsing import parse_comma_separated_identifiers
  30. from app.blueprints.rest.case_comments import case_comment_update
  31. from app.datamgmt.alerts.alerts_db import get_filtered_alerts
  32. from app.datamgmt.alerts.alerts_db import get_alert_by_id
  33. from app.datamgmt.alerts.alerts_db import create_case_from_alert
  34. from app.datamgmt.alerts.alerts_db import delete_related_alerts_cache
  35. from app.datamgmt.alerts.alerts_db import merge_alert_in_case
  36. from app.datamgmt.alerts.alerts_db import unmerge_alert_from_case
  37. from app.datamgmt.alerts.alerts_db import cache_similar_alert
  38. from app.datamgmt.alerts.alerts_db import get_related_alerts
  39. from app.datamgmt.alerts.alerts_db import get_related_alerts_details
  40. from app.datamgmt.alerts.alerts_db import get_alert_comments
  41. from app.datamgmt.alerts.alerts_db import delete_alert_comment
  42. from app.datamgmt.alerts.alerts_db import get_alert_comment
  43. from app.datamgmt.alerts.alerts_db import delete_similar_alert_cache
  44. from app.datamgmt.alerts.alerts_db import delete_alerts
  45. from app.datamgmt.alerts.alerts_db import create_case_from_alerts
  46. from app.datamgmt.case.case_db import get_case
  47. from app.datamgmt.manage.manage_access_control_db import check_ua_case_client
  48. from app.datamgmt.manage.manage_access_control_db import user_has_client_access
  49. from app.iris_engine.access_control.utils import ac_set_new_case_access
  50. from app.iris_engine.module_handler.module_handler import call_modules_hook
  51. from app.iris_engine.utils.tracker import track_activity
  52. from app.models.alerts import AlertStatus
  53. from app.models.authorization import Permissions
  54. from app.schema.marshables import AlertSchema
  55. from app.schema.marshables import CaseSchema
  56. from app.schema.marshables import CommentSchema
  57. from app.schema.marshables import CaseAssetsSchema
  58. from app.schema.marshables import IocSchema
  59. from app.blueprints.access_controls import ac_api_requires
  60. from app.blueprints.responses import response_error
  61. from app.util import add_obj_history_entry
  62. from app.blueprints.responses import response_success
  63. alerts_rest_blueprint = Blueprint('alerts_rest', __name__)
  64. @alerts_rest_blueprint.route('/alerts/filter', methods=['GET'])
  65. @endpoint_deprecated('GET', '/api/v2/alerts')
  66. @ac_api_requires(Permissions.alerts_read)
  67. def alerts_list_route() -> Response:
  68. """
  69. Get a list of alerts from the database
  70. args:
  71. caseid (str): The case id
  72. returns:
  73. Response: The response
  74. """
  75. page = request.args.get('page', 1, type=int)
  76. per_page = request.args.get('per_page', 10, type=int)
  77. alert_ids_str = request.args.get('alert_ids')
  78. alert_ids = None
  79. if alert_ids_str:
  80. try:
  81. if ',' in alert_ids_str:
  82. alert_ids = [int(alert_id) for alert_id in alert_ids_str.split(',')]
  83. else:
  84. alert_ids = [int(alert_ids_str)]
  85. except ValueError:
  86. return response_error('Invalid alert id')
  87. alert_assets_str = request.args.get('alert_assets')
  88. alert_assets = None
  89. if alert_assets_str:
  90. try:
  91. if ',' in alert_assets_str:
  92. alert_assets = [str(alert_asset) for alert_asset in alert_assets_str.split(',')]
  93. else:
  94. alert_assets = [str(alert_assets_str)]
  95. except ValueError:
  96. return response_error('Invalid alert asset')
  97. alert_iocs_str = request.args.get('alert_iocs')
  98. alert_iocs = None
  99. if alert_iocs_str:
  100. try:
  101. if ',' in alert_iocs_str:
  102. alert_iocs = [str(alert_ioc) for alert_ioc in alert_iocs_str.split(',')]
  103. else:
  104. alert_iocs = [str(alert_iocs_str)]
  105. except ValueError:
  106. return response_error('Invalid alert ioc')
  107. fields_str = request.args.get('fields')
  108. if fields_str:
  109. # Split into a list
  110. fields = [field.strip() for field in fields_str.split(',') if field.strip()]
  111. else:
  112. fields = None
  113. try:
  114. filtered_alerts = get_filtered_alerts(
  115. start_date=request.args.get('creation_start_date'),
  116. end_date=request.args.get('creation_end_date'),
  117. source_start_date=request.args.get('source_start_date'),
  118. source_end_date=request.args.get('source_end_date'),
  119. source_reference=request.args.get('source_reference'),
  120. title=request.args.get('alert_title'),
  121. description=request.args.get('alert_description'),
  122. status=request.args.get('alert_status_id', type=int),
  123. severity=request.args.get('alert_severity_id', type=int),
  124. owner=request.args.get('alert_owner_id', type=int),
  125. source=request.args.get('alert_source'),
  126. tags=request.args.get('alert_tags'),
  127. classification=request.args.get('alert_classification_id', type=int),
  128. client=request.args.get('alert_customer_id'),
  129. case_id=request.args.get('case_id', type=int),
  130. alert_ids=alert_ids,
  131. page=page,
  132. per_page=per_page,
  133. sort=request.args.get('sort', 'desc', type=str),
  134. custom_conditions=request.args.get('custom_conditions'),
  135. assets=alert_assets,
  136. iocs=alert_iocs,
  137. resolution_status=request.args.get('alert_resolution_id', type=int),
  138. current_user_id=current_user.id
  139. )
  140. except Exception as e:
  141. app.app.logger.exception(e)
  142. return response_error(str(e))
  143. if filtered_alerts is None:
  144. return response_error('Filtering error')
  145. # If fields are provided, use them in the schema
  146. if fields:
  147. try:
  148. alert_schema = AlertSchema(only=fields)
  149. except Exception as e:
  150. app.app.logger.exception(f"Error selecting fields in AlertSchema: {str(e)}")
  151. alert_schema = AlertSchema()
  152. else:
  153. alert_schema = AlertSchema()
  154. filtered_data = {
  155. 'total': filtered_alerts.total,
  156. 'alerts': alert_schema.dump(filtered_alerts, many=True),
  157. 'last_page': filtered_alerts.pages,
  158. 'current_page': filtered_alerts.page,
  159. 'next_page': filtered_alerts.next_num if filtered_alerts.has_next else None,
  160. }
  161. return response_success(data=filtered_data)
  162. @alerts_rest_blueprint.route('/alerts/add', methods=['POST'])
  163. @ac_api_requires(Permissions.alerts_write)
  164. def alerts_add_route() -> Response:
  165. """
  166. Add a new alert to the database
  167. args:
  168. caseid (str): The case id
  169. returns:
  170. Response: The response
  171. """
  172. if not request.json:
  173. return response_error('No JSON data provided')
  174. alert_schema = AlertSchema()
  175. ioc_schema = IocSchema()
  176. asset_schema = CaseAssetsSchema()
  177. try:
  178. # Load the JSON data from the request
  179. data = request.get_json()
  180. iocs_list = data.pop('alert_iocs', [])
  181. assets_list = data.pop('alert_assets', [])
  182. iocs = ioc_schema.load(iocs_list, many=True, partial=True)
  183. assets = asset_schema.load(assets_list, many=True, partial=True)
  184. # Deserialize the JSON data into an Alert object
  185. new_alert = alert_schema.load(data)
  186. # Verify the user is entitled to create an alert for the client
  187. if not user_has_client_access(current_user.id, new_alert.alert_customer_id):
  188. return response_error('User not entitled to create alerts for the client')
  189. new_alert.alert_creation_time = datetime.utcnow()
  190. new_alert.iocs = iocs
  191. new_alert.assets = assets
  192. # Add the new alert to the session and commit it
  193. db.session.add(new_alert)
  194. db.session.commit()
  195. # Add history entry
  196. add_obj_history_entry(new_alert, 'Alert created')
  197. # Cache the alert for similarities check
  198. cache_similar_alert(new_alert.alert_customer_id, assets=assets_list,
  199. iocs=iocs_list, alert_id=new_alert.alert_id,
  200. creation_date=new_alert.alert_source_event_time)
  201. #register_related_alerts(new_alert, assets_list=assets, iocs_list=iocs)
  202. new_alert = call_modules_hook('on_postload_alert_create', data=new_alert)
  203. track_activity(f"created alert #{new_alert.alert_id} - {new_alert.alert_title}", ctx_less=True)
  204. # Emit a socket io event
  205. app.socket_io.emit('new_alert', json.dumps({
  206. 'alert_id': new_alert.alert_id
  207. }), namespace='/alerts')
  208. # Return the newly created alert as JSON
  209. return response_success(data=alert_schema.dump(new_alert))
  210. except Exception as e:
  211. app.app.logger.exception(e)
  212. # Handle any errors during deserialization or DB operations
  213. return response_error(str(e))
  214. @alerts_rest_blueprint.route('/alerts/<int:alert_id>', methods=['GET'])
  215. @ac_api_requires(Permissions.alerts_read)
  216. def alerts_get_route(alert_id) -> Response:
  217. """
  218. Get an alert from the database
  219. args:
  220. caseid (str): The case id
  221. alert_id (int): The alert id
  222. returns:
  223. Response: The response
  224. """
  225. alert_schema = AlertSchema()
  226. # Get the alert from the database
  227. alert = get_alert_by_id(alert_id)
  228. # Return the alert as JSON
  229. if alert is None:
  230. return response_error('Alert not found')
  231. if not user_has_client_access(current_user.id, alert.alert_customer_id):
  232. return response_error('Alert not found')
  233. alert_dump = alert_schema.dump(alert)
  234. # Get similar alerts
  235. similar_alerts = get_related_alerts(alert.alert_customer_id, alert.assets, alert.iocs)
  236. alert_dump['related_alerts'] = similar_alerts
  237. return response_success(data=alert_dump)
  238. @alerts_rest_blueprint.route('/alerts/similarities/<int:alert_id>', methods=['GET'])
  239. @ac_api_requires(Permissions.alerts_read)
  240. def alerts_similarities_route(alert_id) -> Response:
  241. """
  242. Get an alert and similarities from the database
  243. args:
  244. caseid (str): The case id
  245. alert_id (int): The alert id
  246. returns:
  247. Response: The response
  248. """
  249. # Get the alert from the database
  250. alert = get_alert_by_id(alert_id)
  251. # Return the alert as JSON
  252. if alert is None:
  253. return response_error('Alert not found')
  254. if not user_has_client_access(current_user.id, alert.alert_customer_id):
  255. return response_error('Alert not found')
  256. open_alerts = request.args.get('open-alerts', 'false').lower() == 'true'
  257. open_cases = request.args.get('open-cases', 'false').lower() == 'true'
  258. closed_cases = request.args.get('closed-cases', 'false').lower() == 'true'
  259. closed_alerts = request.args.get('closed-alerts', 'false').lower() == 'true'
  260. days_back = request.args.get('days-back', 180, type=int)
  261. number_of_results = request.args.get('number-of-nodes', 100, type=int)
  262. if number_of_results < 0:
  263. number_of_results = 100
  264. if days_back < 0:
  265. days_back = 180
  266. # Get similar alerts
  267. similar_alerts = get_related_alerts_details(alert.alert_customer_id, alert.assets, alert.iocs,
  268. open_alerts=open_alerts, open_cases=open_cases,
  269. closed_cases=closed_cases, closed_alerts=closed_alerts,
  270. days_back=days_back, number_of_results=number_of_results)
  271. return response_success(data=similar_alerts)
  272. @alerts_rest_blueprint.route('/alerts/update/<int:alert_id>', methods=['POST'])
  273. @ac_api_requires(Permissions.alerts_write)
  274. def alerts_update_route(alert_id) -> Response:
  275. """
  276. Update an alert in the database
  277. args:
  278. caseid (str): The case id
  279. alert_id (int): The alert id
  280. returns:
  281. Response: The response
  282. """
  283. if not request.json:
  284. return response_error('No JSON data provided')
  285. alert = get_alert_by_id(alert_id)
  286. if not alert:
  287. return response_error('Alert not found')
  288. if not user_has_client_access(current_user.id, alert.alert_customer_id):
  289. return response_error('User not entitled to update alerts for the client', status=403)
  290. alert_schema = AlertSchema()
  291. do_resolution_hook = False
  292. do_status_hook = False
  293. try:
  294. # Load the JSON data from the request
  295. data = request.get_json()
  296. activity_data = []
  297. for key, value in data.items():
  298. old_value = getattr(alert, key, None)
  299. if type(old_value) is int:
  300. old_value = str(old_value)
  301. if type(value) is int:
  302. value = str(value)
  303. if old_value != value:
  304. if key == "alert_resolution_status_id":
  305. do_resolution_hook = True
  306. if key == 'alert_status_id':
  307. do_status_hook = True
  308. if key not in ["alert_content", "alert_note"]:
  309. activity_data.append(f"\"{key}\" from \"{old_value}\" to \"{value}\"")
  310. else:
  311. activity_data.append(f"\"{key}\"")
  312. # Deserialize the JSON data into an Alert object
  313. updated_alert = alert_schema.load(data, instance=alert, partial=True)
  314. if data.get('alert_owner_id') is None and updated_alert.alert_owner_id is None:
  315. updated_alert.alert_owner_id = current_user.id
  316. if data.get('alert_owner_id') == "-1" or data.get('alert_owner_id') == -1:
  317. updated_alert.alert_owner_id = None
  318. # Save the changes
  319. db.session.commit()
  320. updated_alert = call_modules_hook('on_postload_alert_update', data=updated_alert)
  321. if do_resolution_hook:
  322. updated_alert = call_modules_hook('on_postload_alert_resolution_update', data=updated_alert)
  323. if do_status_hook:
  324. updated_alert = call_modules_hook('on_postload_alert_status_update', data=updated_alert)
  325. if activity_data:
  326. activity_data_as_string = ','.join(activity_data)
  327. track_activity(f'updated alert #{alert_id}: {activity_data_as_string}', ctx_less=True)
  328. add_obj_history_entry(updated_alert, f'updated alert: {activity_data_as_string}')
  329. else:
  330. track_activity(f'updated alert #{alert_id}', ctx_less=True)
  331. add_obj_history_entry(updated_alert, 'updated alert')
  332. db.session.commit()
  333. # Return the updated alert as JSON
  334. return response_success(data=alert_schema.dump(updated_alert))
  335. except Exception as e:
  336. # Handle any errors during deserialization or DB operations
  337. return response_error(str(e))
  338. @alerts_rest_blueprint.route('/alerts/batch/update', methods=['POST'])
  339. @ac_api_requires(Permissions.alerts_write)
  340. def alerts_batch_update_route() -> Response:
  341. """
  342. Update multiple alerts in the database
  343. args:
  344. caseid (int): The case id
  345. returns:
  346. Response: The response
  347. """
  348. if not request.json:
  349. return response_error('No JSON data provided')
  350. # Load the JSON data from the request
  351. data = request.get_json()
  352. # Get the list of alert IDs and updates from the request data
  353. alert_ids: List[int] = data.get('alert_ids', [])
  354. updates = data.get('updates', {})
  355. if not updates.get('alert_tags'):
  356. updates.pop('alert_tags', None)
  357. if not alert_ids:
  358. return response_error('No alert IDs provided')
  359. alert_schema = AlertSchema()
  360. # Process each alert ID
  361. for alert_id in alert_ids:
  362. alert = get_alert_by_id(alert_id)
  363. if not alert:
  364. return response_error(f'Alert with ID {alert_id} not found')
  365. try:
  366. activity_data = []
  367. for key, value in updates.items():
  368. old_value = getattr(alert, key, None)
  369. if old_value != value:
  370. activity_data.append(f"\"{key}\"")
  371. # Check if the user has access to the client
  372. if not user_has_client_access(current_user.id, alert.alert_customer_id):
  373. return response_error('User not entitled to update alerts for the client', status=403)
  374. if getattr(alert, 'alert_owner_id') is None:
  375. updates['alert_owner_id'] = current_user.id
  376. if data.get('alert_owner_id') == "-1" or data.get('alert_owner_id') == -1:
  377. updates['alert_owner_id'] = None
  378. # Deserialize the JSON data into an Alert object
  379. alert_schema.load(updates, instance=alert, partial=True)
  380. db.session.commit()
  381. alert = call_modules_hook('on_postload_alert_update', data=alert)
  382. if activity_data:
  383. track_activity(f"updated alert #{alert_id}: {','.join(activity_data)}", ctx_less=True)
  384. add_obj_history_entry(alert, f"updated alert: {','.join(activity_data)}")
  385. db.session.commit()
  386. except Exception as e:
  387. # Handle any errors during deserialization or DB operations
  388. return response_error(str(e))
  389. # Return a success response
  390. return response_success(msg='Batch update successful')
  391. @alerts_rest_blueprint.route('/alerts/batch/delete', methods=['POST'])
  392. @ac_api_requires(Permissions.alerts_delete)
  393. def alerts_batch_delete_route() -> Response:
  394. """
  395. Delete multiple alerts from the database
  396. args:
  397. caseid (int): The case id
  398. returns:
  399. Response: The response
  400. """
  401. if not request.json:
  402. return response_error('No JSON data provided')
  403. # Load the JSON data from the request
  404. data = request.get_json()
  405. # Get the list of alert IDs and updates from the request data
  406. alert_ids: List[int] = data.get('alert_ids', [])
  407. if not alert_ids:
  408. return response_error('No alert IDs provided')
  409. # Check if the user has access to the client
  410. for alert_id in alert_ids:
  411. alert = get_alert_by_id(alert_id)
  412. if not alert:
  413. return response_error(f'Alert with ID {alert_id} not found')
  414. if not user_has_client_access(current_user.id, alert.alert_customer_id):
  415. return response_error('User not entitled to delete alerts for the client', status=403)
  416. success, logs = delete_alerts(alert_ids)
  417. if not success:
  418. return response_error(logs)
  419. alert = call_modules_hook('on_postload_alert_delete', data={"alert_ids": alert_ids})
  420. track_activity(f"deleted alerts #{','.join(str(alert_id) for alert_id in alert_ids)}", ctx_less=True)
  421. return response_success(msg='Batch delete successful')
  422. @alerts_rest_blueprint.route('/alerts/delete/<int:alert_id>', methods=['POST'])
  423. @ac_api_requires(Permissions.alerts_delete)
  424. def alerts_delete_route(alert_id) -> Response:
  425. """
  426. Delete an alert from the database
  427. args:
  428. caseid (str): The case id
  429. alert_id (int): The alert id
  430. returns:
  431. Response: The response
  432. """
  433. alert = get_alert_by_id(alert_id)
  434. if not alert:
  435. return response_error('Alert not found')
  436. try:
  437. # Check if the user has access to the client
  438. if not user_has_client_access(current_user.id, alert.alert_customer_id):
  439. return response_error('User not entitled to delete alerts for the client', status=403)
  440. # Delete the case association
  441. delete_similar_alert_cache(alert_id=alert_id)
  442. # Delete the similarity entries
  443. delete_related_alerts_cache([alert_id])
  444. # Delete the alert from the database
  445. db.session.delete(alert)
  446. db.session.commit()
  447. alert = call_modules_hook('on_postload_alert_delete', data=alert_id)
  448. track_activity(f"delete alert #{alert_id}", ctx_less=True)
  449. # Return the deleted alert as JSON
  450. return response_success(data={'alert_id': alert_id})
  451. except Exception as e:
  452. # Handle any errors during deserialization or DB operations
  453. return response_error(str(e))
  454. @alerts_rest_blueprint.route('/alerts/escalate/<int:alert_id>', methods=['POST'])
  455. @ac_api_requires(Permissions.alerts_write)
  456. def alerts_escalate_route(alert_id) -> Response:
  457. """
  458. Escalate an alert
  459. args:
  460. caseid (str): The case id
  461. alert_id (int): The alert id
  462. returns:
  463. Response: The response
  464. """
  465. alert = get_alert_by_id(alert_id)
  466. if not alert:
  467. return response_error('Alert not found')
  468. if request.json is None:
  469. return response_error('No JSON data provided')
  470. data = request.get_json()
  471. iocs_import_list: List[str] = data.get('iocs_import_list')
  472. assets_import_list: List[str] = data.get('assets_import_list')
  473. note: str = data.get('note')
  474. import_as_event: bool = data.get('import_as_event')
  475. case_tags: str = data.get('case_tags')
  476. case_title: str = data.get('case_title')
  477. case_template_id: int = data.get('case_template_id', None)
  478. try:
  479. # Check if the user has access to the client
  480. if not user_has_client_access(current_user.id, alert.alert_customer_id):
  481. return response_error('User not entitled to escalate alerts for the client', status=403)
  482. # Escalate the alert to a case
  483. alert.alert_status_id = AlertStatus.query.filter_by(status_name='Escalated').first().status_id
  484. db.session.commit()
  485. # Create a new case from the alert
  486. case = create_case_from_alert(alert, iocs_list=iocs_import_list, assets_list=assets_import_list, note=note,
  487. import_as_event=import_as_event, case_tags=case_tags, case_title=case_title,
  488. template_id=case_template_id)
  489. if not case:
  490. return response_error('Failed to create case from alert')
  491. ac_set_new_case_access(None, case.case_id, case.client_id)
  492. case = call_modules_hook('on_postload_case_create', data=case)
  493. add_obj_history_entry(case, 'created')
  494. track_activity("new case {case_name} created from alert".format(case_name=case.name),
  495. ctx_less=True)
  496. add_obj_history_entry(alert, f"Alert escalated to case #{case.case_id}")
  497. alert = call_modules_hook('on_postload_alert_escalate', data=alert)
  498. # Return the updated alert as JSON
  499. return response_success(data=CaseSchema().dump(case))
  500. except Exception as e:
  501. app.logger.exception(e)
  502. # Handle any errors during deserialization or DB operations
  503. return response_error(str(e))
  504. @alerts_rest_blueprint.route('/alerts/merge/<int:alert_id>', methods=['POST'])
  505. @ac_api_requires(Permissions.alerts_write)
  506. def alerts_merge_route(alert_id) -> Response:
  507. """
  508. Merge an alert into an existing case
  509. args:
  510. caseid (str): The case id
  511. alert_id (int): The alert id
  512. returns:
  513. Response: The response
  514. """
  515. if request.json is None:
  516. return response_error('No JSON data provided')
  517. data = request.get_json()
  518. target_case_id = data.get('target_case_id')
  519. if target_case_id is None:
  520. return response_error('No target case id provided')
  521. alert = get_alert_by_id(alert_id)
  522. if not alert:
  523. return response_error('Alert not found')
  524. case = get_case(target_case_id)
  525. if not case:
  526. return response_error('Target case not found')
  527. iocs_import_list: List[str] = data.get('iocs_import_list')
  528. assets_import_list: List[str] = data.get('assets_import_list')
  529. note: str = data.get('note')
  530. import_as_event: bool = data.get('import_as_event')
  531. case_tags = data.get('case_tags')
  532. try:
  533. # Check if the user has access to the client
  534. if not user_has_client_access(current_user.id, alert.alert_customer_id):
  535. return response_error('User not entitled to merge alerts for the client', status=403)
  536. # Check if the user has access to the case
  537. if not check_ua_case_client(current_user.id, target_case_id):
  538. return response_error('User not entitled to merge alerts for the case', status=403)
  539. # Merge the alert into a case
  540. alert.alert_status_id = AlertStatus.query.filter_by(status_name='Merged').first().status_id
  541. db.session.commit()
  542. # Merge alert in the case
  543. merge_alert_in_case(alert, case,
  544. iocs_list=iocs_import_list, assets_list=assets_import_list, note=note,
  545. import_as_event=import_as_event, case_tags=case_tags)
  546. alert = call_modules_hook('on_postload_alert_merge', data=alert, caseid=target_case_id)
  547. track_activity(f"merge alert #{alert_id} into existing case #{target_case_id}", caseid=target_case_id)
  548. add_obj_history_entry(alert, f"Alert merged into existing case #{target_case_id}")
  549. # Return the updated alert as JSON
  550. return response_success(data=CaseSchema().dump(case))
  551. except Exception as e:
  552. app.app.logger.exception(e)
  553. # Handle any errors during deserialization or DB operations
  554. return response_error(str(e))
  555. @alerts_rest_blueprint.route('/alerts/unmerge/<int:alert_id>', methods=['POST'])
  556. @ac_api_requires(Permissions.alerts_write)
  557. def alerts_unmerge_route(alert_id) -> Response:
  558. """
  559. Unmerge an alert from a case
  560. args:
  561. caseid (str): The case id
  562. alert_id (int): The alert id
  563. returns:
  564. Response: The response
  565. """
  566. if request.json is None:
  567. return response_error('No JSON data provided')
  568. target_case_id = request.json.get('target_case_id')
  569. if target_case_id is None:
  570. return response_error('No target case id provided')
  571. alert = get_alert_by_id(alert_id)
  572. if not alert:
  573. return response_error('Alert not found')
  574. case = get_case(target_case_id)
  575. if not case:
  576. return response_error('Target case not found')
  577. try:
  578. # Check if the user has access to the client
  579. if not user_has_client_access(current_user.id, alert.alert_customer_id):
  580. return response_error('User not entitled to unmerge alerts for the client', status=403)
  581. # Check if the user has access to the case
  582. if not check_ua_case_client(current_user.id, target_case_id):
  583. return response_error('User not entitled to unmerge alerts for the case', status=403)
  584. # Unmerge alert from the case
  585. success, message = unmerge_alert_from_case(alert, case)
  586. if success is False:
  587. return response_error(message)
  588. track_activity(f"unmerge alert #{alert_id} from case #{target_case_id}", caseid=target_case_id)
  589. add_obj_history_entry(alert, f"Alert unmerged from case #{target_case_id}")
  590. alert = call_modules_hook('on_postload_alert_unmerge', data=alert)
  591. # Return the updated case as JSON
  592. return response_success(data=AlertSchema().dump(alert), msg=message)
  593. except Exception as e:
  594. # Handle any errors during deserialization or DB operations
  595. return response_error(str(e))
  596. @alerts_rest_blueprint.route('/alerts/batch/merge', methods=['POST'])
  597. @ac_api_requires(Permissions.alerts_write)
  598. def alerts_batch_merge_route() -> Response:
  599. """
  600. Merge multiple alerts into a case
  601. args:
  602. caseid (str): The case id
  603. returns:
  604. Response: The response
  605. """
  606. if request.json is None:
  607. return response_error('No JSON data provided')
  608. data = request.get_json()
  609. target_case_id = data.get('target_case_id')
  610. if target_case_id is None:
  611. return response_error('No target case id provided')
  612. alert_ids = data.get('alert_ids')
  613. if not alert_ids:
  614. return response_error('No alert ids provided')
  615. case = get_case(target_case_id)
  616. if not case:
  617. return response_error('Target case not found')
  618. iocs_import_list: List[str] = data.get('iocs_import_list')
  619. assets_import_list: List[str] = data.get('assets_import_list')
  620. note: str = data.get('note')
  621. import_as_event: bool = data.get('import_as_event')
  622. case_tags = data.get('case_tags')
  623. # Check if the user has access to the case
  624. if not check_ua_case_client(current_user.id, target_case_id):
  625. return response_error('User not entitled to merge alerts for the case', status=403)
  626. try:
  627. # Merge the alerts into a case
  628. for alert_id in parse_comma_separated_identifiers(alert_ids):
  629. alert = get_alert_by_id(alert_id)
  630. if not alert:
  631. continue
  632. # Check if the user has access to the client
  633. if not user_has_client_access(current_user.id, alert.alert_customer_id):
  634. return response_error('User not entitled to merge alerts for the client', status=403)
  635. alert.alert_status_id = AlertStatus.query.filter_by(status_name='Merged').first().status_id
  636. db.session.commit()
  637. # Merge alert in the case
  638. merge_alert_in_case(alert, case, iocs_list=iocs_import_list, assets_list=assets_import_list, note=None,
  639. import_as_event=import_as_event, case_tags=case_tags)
  640. add_obj_history_entry(alert, f"Alert merged into existing case #{target_case_id}")
  641. alert = call_modules_hook('on_postload_alert_merge', data=alert)
  642. if note:
  643. case.description += f"\n\n### Escalation note\n\n{note}\n\n" if case.description else f"\n\n{note}\n\n"
  644. db.session.commit()
  645. track_activity(f"batched merge alerts {alert_ids} into existing case #{target_case_id}",
  646. caseid=target_case_id)
  647. # Return the updated case as JSON
  648. return response_success(data=CaseSchema().dump(case))
  649. except Exception as e:
  650. app.app.logger.exception(e)
  651. # Handle any errors during deserialization or DB operations
  652. return response_error(str(e))
  653. @alerts_rest_blueprint.route('/alerts/batch/escalate', methods=['POST'])
  654. @ac_api_requires(Permissions.alerts_write)
  655. def alerts_batch_escalate_route() -> Response:
  656. """
  657. Escalate multiple alerts into a case
  658. args:
  659. caseid (str): The case id
  660. returns:
  661. Response: The response
  662. """
  663. if request.json is None:
  664. return response_error('No JSON data provided')
  665. data = request.get_json()
  666. alert_ids = data.get('alert_ids')
  667. if not alert_ids:
  668. return response_error('No alert ids provided')
  669. iocs_import_list: List[str] = data.get('iocs_import_list')
  670. assets_import_list: List[str] = data.get('assets_import_list')
  671. note: str = data.get('note')
  672. import_as_event: bool = data.get('import_as_event')
  673. case_tags = data.get('case_tags')
  674. case_title = data.get('case_title')
  675. alerts_list = []
  676. case_template_id: int = data.get('case_template_id', None)
  677. try:
  678. # Merge the alerts into a case
  679. for alert_id in parse_comma_separated_identifiers(alert_ids):
  680. alert = get_alert_by_id(alert_id)
  681. if not alert:
  682. continue
  683. # Check if the user has access to the client
  684. if not user_has_client_access(current_user.id, alert.alert_customer_id):
  685. return response_error('User not entitled to escalate alerts for the client', status=403)
  686. alert.alert_status_id = AlertStatus.query.filter_by(status_name='Merged').first().status_id
  687. db.session.commit()
  688. alert = call_modules_hook('on_postload_alert_escalate', data=alert)
  689. alerts_list.append(alert)
  690. # Merge alerts in the case
  691. case = create_case_from_alerts(alerts_list, iocs_list=iocs_import_list, assets_list=assets_import_list,
  692. note=note, import_as_event=import_as_event, case_tags=case_tags,
  693. case_title=case_title, template_id=case_template_id)
  694. if not case:
  695. return response_error('Failed to create case from alert')
  696. ac_set_new_case_access(None, case.case_id, case.client_id)
  697. case = call_modules_hook('on_postload_case_create', data=case)
  698. add_obj_history_entry(case, 'created')
  699. track_activity("new case {case_name} created from alerts".format(case_name=case.name),
  700. caseid=case.case_id)
  701. for alert in alerts_list:
  702. add_obj_history_entry(alert, f"Alert escalated into new case #{case.case_id}")
  703. # Return the updated case as JSON
  704. return response_success(data=CaseSchema().dump(case))
  705. except Exception as e:
  706. app.app.logger.exception(e)
  707. # Handle any errors during deserialization or DB operations
  708. return response_error(str(e))
  709. @alerts_rest_blueprint.route('/alerts/<int:alert_id>/comments/list', methods=['GET'])
  710. @ac_api_requires(Permissions.alerts_read)
  711. def alert_comments_get(alert_id):
  712. """
  713. Get the comments for an alert
  714. args:
  715. alert_id (int): The alert id
  716. caseid (str): The case id
  717. returns:
  718. Response: The response
  719. """
  720. # Check if the user has access to the client
  721. alert = get_alert_by_id(alert_id)
  722. if not alert:
  723. return response_error('Invalid alert ID')
  724. if not user_has_client_access(current_user.id, alert.alert_customer_id):
  725. return response_error('User not entitled to read alerts for the client', status=403)
  726. alert_comments = get_alert_comments(alert_id)
  727. if alert_comments is None:
  728. return response_error('Invalid alert ID')
  729. return response_success(data=CommentSchema(many=True).dump(alert_comments))
  730. @alerts_rest_blueprint.route('/alerts/<int:alert_id>/comments/<int:com_id>/delete', methods=['POST'])
  731. @ac_api_requires(Permissions.alerts_write)
  732. def alert_comment_delete(alert_id, com_id):
  733. """
  734. Delete a comment for an alert
  735. args:
  736. alert_id (int): The alert id
  737. com_id (int): The comment id
  738. caseid (str): The case id
  739. returns:
  740. Response: The response
  741. """
  742. # Check if the user has access to the client
  743. alert = get_alert_by_id(alert_id)
  744. if not alert:
  745. return response_error('Invalid alert ID')
  746. if not user_has_client_access(current_user.id, alert.alert_customer_id):
  747. return response_error('User not entitled to read alerts for the client', status=403)
  748. success, msg = delete_alert_comment(comment_id=com_id, alert_id=alert_id)
  749. if not success:
  750. return response_error(msg)
  751. call_modules_hook('on_postload_alert_comment_delete', data=com_id)
  752. track_activity(f"comment {com_id} on alert {alert_id} deleted", ctx_less=True)
  753. return response_success(msg)
  754. @alerts_rest_blueprint.route('/alerts/<int:alert_id>/comments/<int:com_id>', methods=['GET'])
  755. @ac_api_requires(Permissions.alerts_read)
  756. def alert_comment_get(alert_id, com_id):
  757. """
  758. Get a comment for an alert
  759. args:
  760. cur_id (int): The alert id
  761. com_id (int): The comment id
  762. caseid (str): The case id
  763. returns:
  764. Response: The response
  765. """
  766. # Check if the user has access to the client
  767. alert = get_alert_by_id(alert_id)
  768. if not alert:
  769. return response_error('Invalid alert ID')
  770. if not user_has_client_access(current_user.id, alert.alert_customer_id):
  771. return response_error('User not entitled to read alerts for the client', status=403)
  772. comment = get_alert_comment(alert_id, com_id)
  773. if not comment:
  774. return response_error("Invalid comment ID")
  775. return response_success(data=CommentSchema().dump(comment))
  776. @alerts_rest_blueprint.route('/alerts/<int:alert_id>/comments/<int:com_id>/edit', methods=['POST'])
  777. @ac_api_requires(Permissions.alerts_write)
  778. def alert_comment_edit(alert_id, com_id):
  779. """
  780. Edit a comment for an alert
  781. args:
  782. alert_id (int): The alert id
  783. com_id (int): The comment id
  784. caseid (str): The case id
  785. returns:
  786. Response: The response
  787. """
  788. alert = get_alert_by_id(alert_id)
  789. if not alert:
  790. return response_error('Invalid alert ID')
  791. if not user_has_client_access(current_user.id, alert.alert_customer_id):
  792. return response_error('User not entitled to read alerts for the client', status=403)
  793. return case_comment_update(com_id, 'events', None)
  794. @alerts_rest_blueprint.route('/alerts/<int:alert_id>/comments/add', methods=['POST'])
  795. @ac_api_requires(Permissions.alerts_write)
  796. def case_comment_add(alert_id):
  797. """
  798. Add a comment to an alert
  799. args:
  800. alert_id (int): The alert id
  801. caseid (str): The case id
  802. returns:
  803. Response: The response
  804. """
  805. try:
  806. alert = get_alert_by_id(alert_id=alert_id)
  807. if not alert:
  808. return response_error('Invalid alert ID')
  809. if not user_has_client_access(current_user.id, alert.alert_customer_id):
  810. return response_error('User not entitled to read alerts for the client', status=403)
  811. comment_schema = CommentSchema()
  812. comment = comment_schema.load(request.get_json())
  813. comment.comment_alert_id = alert_id
  814. comment.comment_user_id = current_user.id
  815. comment.comment_date = datetime.now()
  816. comment.comment_update_date = datetime.now()
  817. db.session.add(comment)
  818. db.session.commit()
  819. add_obj_history_entry(alert, 'commented')
  820. db.session.commit()
  821. hook_data = {
  822. "comment": comment_schema.dump(comment),
  823. "alert": AlertSchema().dump(alert)
  824. }
  825. call_modules_hook('on_postload_alert_commented', data=hook_data)
  826. track_activity(f"alert \"{alert.alert_id}\" commented", ctx_less=True)
  827. return response_success("Alert commented", data=comment_schema.dump(comment))
  828. except marshmallow.exceptions.ValidationError as e:
  829. return response_error(msg="Data error", data=e.normalized_messages())