Keine Beschreibung

manage_cases_db.py 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. # IRIS Source Code
  2. # Copyright (C) 2021 - Airbus CyberSecurity (SAS)
  3. # ir@cyberactionlab.net
  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 datetime import datetime
  19. from datetime import date
  20. from datetime import timedelta
  21. from pathlib import Path
  22. from sqlalchemy import and_
  23. from sqlalchemy.orm import aliased
  24. from functools import reduce
  25. from app import db
  26. from app import app
  27. from app.datamgmt.alerts.alerts_db import search_alert_resolution_by_name
  28. from app.datamgmt.case.case_db import get_case_tags
  29. from app.datamgmt.manage.manage_case_state_db import get_case_state_by_name
  30. from app.datamgmt.conversions import convert_sort_direction
  31. from app.datamgmt.authorization import has_deny_all_access_level
  32. from app.datamgmt.states import delete_case_states
  33. from app.models.models import CaseAssets
  34. from app.models.models import NoteRevisions
  35. from app.models.models import CaseClassification
  36. from app.models.models import alert_assets_association
  37. from app.models.models import CaseStatus
  38. from app.models.models import TaskAssignee
  39. from app.models.models import NoteDirectory
  40. from app.models.models import Tags
  41. from app.models.models import CaseEventCategory
  42. from app.models.models import CaseEventsAssets
  43. from app.models.models import CaseEventsIoc
  44. from app.models.models import CaseReceivedFile
  45. from app.models.models import CaseTasks
  46. from app.models.cases import Cases
  47. from app.models.cases import CasesEvent
  48. from app.models.models import Client
  49. from app.models.models import DataStoreFile
  50. from app.models.models import DataStorePath
  51. from app.models.models import IocAssetLink
  52. from app.models.models import Notes
  53. from app.models.models import NotesGroup
  54. from app.models.models import NotesGroupLink
  55. from app.models.models import UserActivity
  56. from app.models.alerts import AlertCaseAssociation
  57. from app.models.authorization import CaseAccessLevel
  58. from app.models.authorization import GroupCaseAccess
  59. from app.models.authorization import OrganisationCaseAccess
  60. from app.models.authorization import User
  61. from app.models.authorization import UserCaseAccess
  62. from app.models.authorization import UserCaseEffectiveAccess
  63. from app.models.models import Ioc
  64. from app.models.cases import CaseProtagonist
  65. from app.models.cases import CaseTags
  66. from app.models.cases import CaseState
  67. from app.models.pagination_parameters import PaginationParameters
  68. def list_cases_id():
  69. res = Cases.query.with_entities(
  70. Cases.case_id
  71. ).all()
  72. return [r.case_id for r in res]
  73. def list_cases_dict_unrestricted():
  74. owner_alias = aliased(User)
  75. user_alias = aliased(User)
  76. res = Cases.query.with_entities(
  77. Cases.name.label('case_name'),
  78. Cases.description.label('case_description'),
  79. Client.name.label('client_name'),
  80. Cases.open_date.label('case_open_date'),
  81. Cases.close_date.label('case_close_date'),
  82. Cases.soc_id.label('case_soc_id'),
  83. Cases.user_id.label('opened_by_user_id'),
  84. user_alias.user.label('opened_by'),
  85. Cases.owner_id,
  86. owner_alias.name.label('owner'),
  87. Cases.case_id
  88. ).join(
  89. Cases.client
  90. ).join(
  91. user_alias, and_(Cases.user_id == user_alias.id)
  92. ).join(
  93. owner_alias, and_(Cases.owner_id == owner_alias.id)
  94. ).order_by(
  95. Cases.open_date
  96. ).all()
  97. data = []
  98. for row in res:
  99. row = row._asdict()
  100. row['case_open_date'] = row['case_open_date'].strftime("%m/%d/%Y")
  101. row['case_close_date'] = row['case_close_date'].strftime("%m/%d/%Y") if row["case_close_date"] else ""
  102. data.append(row)
  103. return data
  104. def list_cases_dict(user_id):
  105. owner_alias = aliased(User)
  106. user_alias = aliased(User)
  107. res = UserCaseEffectiveAccess.query.with_entities(
  108. Cases.name.label('case_name'),
  109. Cases.description.label('case_description'),
  110. Client.name.label('client_name'),
  111. Cases.open_date.label('case_open_date'),
  112. Cases.close_date.label('case_close_date'),
  113. Cases.soc_id.label('case_soc_id'),
  114. Cases.user_id.label('opened_by_user_id'),
  115. user_alias.user.label('opened_by'),
  116. Cases.owner_id,
  117. owner_alias.name.label('owner'),
  118. Cases.case_id,
  119. Cases.case_uuid,
  120. Cases.classification_id,
  121. CaseClassification.name.label('classification'),
  122. Cases.state_id,
  123. CaseState.state_name,
  124. UserCaseEffectiveAccess.access_level
  125. ).join(
  126. UserCaseEffectiveAccess.case
  127. ).join(
  128. Cases.client
  129. ).join(
  130. Cases.user
  131. ).outerjoin(
  132. Cases.classification
  133. ).outerjoin(
  134. Cases.state
  135. ).join(
  136. user_alias, and_(Cases.user_id == user_alias.id)
  137. ).join(
  138. owner_alias, and_(Cases.owner_id == owner_alias.id)
  139. ).filter(
  140. UserCaseEffectiveAccess.user_id == user_id
  141. ).order_by(
  142. Cases.open_date
  143. ).all()
  144. data = []
  145. for row in res:
  146. if has_deny_all_access_level(row):
  147. continue
  148. row = row._asdict()
  149. row['case_open_date'] = row['case_open_date'].strftime("%m/%d/%Y")
  150. row['case_close_date'] = row['case_close_date'].strftime("%m/%d/%Y") if row["case_close_date"] else ""
  151. data.append(row)
  152. return data
  153. def user_list_cases_view(user_id):
  154. res = UserCaseEffectiveAccess.query.with_entities(
  155. UserCaseEffectiveAccess.case_id
  156. ).filter(and_(
  157. UserCaseEffectiveAccess.user_id == user_id,
  158. UserCaseEffectiveAccess.access_level != CaseAccessLevel.deny_all.value
  159. )).all()
  160. return [r.case_id for r in res]
  161. def close_case(case_id):
  162. res = Cases.query.filter(
  163. Cases.case_id == case_id
  164. ).first()
  165. if res:
  166. res.close_date = datetime.utcnow()
  167. res.state_id = get_case_state_by_name('Closed').state_id
  168. db.session.commit()
  169. return res
  170. return None
  171. def map_alert_resolution_to_case_status(case_status_id):
  172. if case_status_id == CaseStatus.false_positive.value:
  173. ares = search_alert_resolution_by_name('False Positive', exact_match=True)
  174. elif case_status_id == CaseStatus.true_positive_with_impact.value:
  175. ares = search_alert_resolution_by_name('True Positive With Impact', exact_match=True)
  176. elif case_status_id == CaseStatus.true_positive_without_impact.value:
  177. ares = search_alert_resolution_by_name('True Positive Without Impact', exact_match=True)
  178. elif case_status_id == CaseStatus.legitimate.value:
  179. ares = search_alert_resolution_by_name('Legitimate', exact_match=True)
  180. elif case_status_id == CaseStatus.unknown.value:
  181. ares = search_alert_resolution_by_name('Unknown', exact_match=True)
  182. else:
  183. ares = search_alert_resolution_by_name('Not Applicable', exact_match=True)
  184. if ares:
  185. return ares.resolution_status_id
  186. return None
  187. def reopen_case(case_id):
  188. res = Cases.query.filter(
  189. Cases.case_id == case_id
  190. ).first()
  191. if res:
  192. res.close_date = None
  193. res.state_id = get_case_state_by_name('Open').state_id
  194. db.session.commit()
  195. return res
  196. return None
  197. def get_case_protagonists(case_id):
  198. protagonists = CaseProtagonist.query.with_entities(
  199. CaseProtagonist.role,
  200. CaseProtagonist.name,
  201. CaseProtagonist.contact,
  202. User.name.label('user_name'),
  203. User.user.label('user_login')
  204. ).filter(
  205. CaseProtagonist.case_id == case_id
  206. ).outerjoin(
  207. CaseProtagonist.user
  208. ).all()
  209. return protagonists
  210. def get_case_details_rt(case_id):
  211. case = Cases.query.filter(Cases.case_id == case_id).first()
  212. if case:
  213. owner_alias = aliased(User)
  214. user_alias = aliased(User)
  215. review_alias = aliased(User)
  216. res = db.session.query(Cases, Client, user_alias, owner_alias).with_entities(
  217. Cases.name.label('case_name'),
  218. Cases.description.label('case_description'),
  219. Cases.open_date, Cases.close_date,
  220. Cases.soc_id.label('case_soc_id'),
  221. Cases.case_id,
  222. Cases.case_uuid,
  223. Client.name.label('customer_name'),
  224. Cases.client_id.label('customer_id'),
  225. Cases.user_id.label('open_by_user_id'),
  226. user_alias.user.label('open_by_user'),
  227. Cases.owner_id,
  228. owner_alias.name.label('owner'),
  229. Cases.status_id,
  230. Cases.state_id,
  231. CaseState.state_name,
  232. Cases.custom_attributes,
  233. Cases.modification_history,
  234. Cases.initial_date,
  235. Cases.classification_id,
  236. CaseClassification.name.label('classification'),
  237. Cases.reviewer_id,
  238. review_alias.name.label('reviewer'),
  239. ).filter(and_(
  240. Cases.case_id == case_id
  241. )).join(
  242. user_alias, and_(Cases.user_id == user_alias.id)
  243. ).outerjoin(
  244. owner_alias, and_(Cases.owner_id == owner_alias.id)
  245. ).outerjoin(
  246. review_alias, and_(Cases.reviewer_id == review_alias.id)
  247. ).join(
  248. Cases.client,
  249. ).outerjoin(
  250. Cases.classification
  251. ).outerjoin(
  252. Cases.state
  253. ).first()
  254. if res is None:
  255. return None
  256. res = res._asdict()
  257. res['case_tags'] = ",".join(get_case_tags(case_id))
  258. res['status_name'] = CaseStatus(res['status_id']).name.replace("_", " ").title()
  259. res['protagonists'] = [r._asdict() for r in get_case_protagonists(case_id)]
  260. else:
  261. res = None
  262. return res
  263. def delete_case(case_id):
  264. if not Cases.query.filter(Cases.case_id == case_id).first():
  265. return False
  266. delete_case_states(caseid=case_id)
  267. UserActivity.query.filter(UserActivity.case_id == case_id).delete()
  268. CaseReceivedFile.query.filter(CaseReceivedFile.case_id == case_id).delete()
  269. Ioc.query.filter(Ioc.case_id == case_id).delete()
  270. CaseTags.query.filter(CaseTags.case_id == case_id).delete()
  271. CaseProtagonist.query.filter(CaseProtagonist.case_id == case_id).delete()
  272. AlertCaseAssociation.query.filter(AlertCaseAssociation.case_id == case_id).delete()
  273. dsf_list = DataStoreFile.query.filter(DataStoreFile.file_case_id == case_id).all()
  274. for dsf_list_item in dsf_list:
  275. fln = Path(dsf_list_item.file_local_name)
  276. if fln.is_file():
  277. fln.unlink(missing_ok=True)
  278. db.session.delete(dsf_list_item)
  279. db.session.commit()
  280. DataStorePath.query.filter(DataStorePath.path_case_id == case_id).delete()
  281. da = CaseAssets.query.with_entities(CaseAssets.asset_id).filter(CaseAssets.case_id == case_id).all()
  282. for asset in da:
  283. IocAssetLink.query.filter(asset.asset_id == asset.asset_id).delete()
  284. CaseEventsAssets.query.filter(CaseEventsAssets.case_id == case_id).delete()
  285. CaseEventsIoc.query.filter(CaseEventsIoc.case_id == case_id).delete()
  286. CaseAssetsAlias = aliased(CaseAssets)
  287. # Query for CaseAssets that are not referenced in alerts and match the case_id
  288. assets_to_delete = db.session.query(CaseAssets).filter(
  289. and_(
  290. CaseAssets.case_id == case_id,
  291. ~db.session.query(alert_assets_association).filter(
  292. alert_assets_association.c.asset_id == CaseAssetsAlias.asset_id
  293. ).exists()
  294. )
  295. )
  296. # Delete the assets
  297. assets_to_delete.delete(synchronize_session='fetch')
  298. # Get all alerts associated with assets in the case
  299. alerts_to_update = db.session.query(CaseAssets).filter(CaseAssets.case_id == case_id)
  300. # Update case_id for the alerts
  301. alerts_to_update.update({CaseAssets.case_id: None}, synchronize_session='fetch')
  302. db.session.commit()
  303. # Legacy code
  304. NotesGroupLink.query.filter(NotesGroupLink.case_id == case_id).delete()
  305. NotesGroup.query.filter(NotesGroup.group_case_id == case_id).delete()
  306. NoteRevisions.query.filter(
  307. and_(
  308. Notes.note_case_id == case_id,
  309. NoteRevisions.note_id == Notes.note_id
  310. )
  311. ).delete()
  312. Notes.query.filter(Notes.note_case_id == case_id).delete()
  313. NoteDirectory.query.filter(NoteDirectory.case_id == case_id).delete()
  314. tasks = CaseTasks.query.filter(CaseTasks.task_case_id == case_id).all()
  315. for task in tasks:
  316. TaskAssignee.query.filter(TaskAssignee.task_id == task.id).delete()
  317. CaseTasks.query.filter(CaseTasks.id == task.id).delete()
  318. da = CasesEvent.query.with_entities(CasesEvent.event_id).filter(CasesEvent.case_id == case_id).all()
  319. for event in da:
  320. CaseEventCategory.query.filter(CaseEventCategory.event_id == event.event_id).delete()
  321. CasesEvent.query.filter(CasesEvent.case_id == case_id).delete()
  322. UserCaseAccess.query.filter(UserCaseAccess.case_id == case_id).delete()
  323. UserCaseEffectiveAccess.query.filter(UserCaseEffectiveAccess.case_id == case_id).delete()
  324. GroupCaseAccess.query.filter(GroupCaseAccess.case_id == case_id).delete()
  325. OrganisationCaseAccess.query.filter(OrganisationCaseAccess.case_id == case_id).delete()
  326. Cases.query.filter(Cases.case_id == case_id).delete()
  327. db.session.commit()
  328. return True
  329. # TODO is it really necessary to have both case_name and search_value
  330. # as of now, it seems case_name does a case.name.ilike, whereas search_value does a case.name.like
  331. def build_filter_case_query(current_user_id,
  332. start_open_date: str = None,
  333. end_open_date: str = None,
  334. case_customer_id: int = None,
  335. case_ids: list = None,
  336. case_name: str = None,
  337. case_description: str = None,
  338. case_classification_id: int = None,
  339. case_owner_id: int = None,
  340. case_opening_user_id: int = None,
  341. case_severity_id: int = None,
  342. case_state_id: int = None,
  343. case_soc_id: str = None,
  344. case_tags: str = None,
  345. case_open_since: int = None,
  346. search_value=None,
  347. sort_by=None,
  348. sort_dir='asc',
  349. is_open: bool=None
  350. ):
  351. """
  352. Get a list of cases from the database, filtered by the given parameters
  353. """
  354. conditions = []
  355. if start_open_date is not None and end_open_date is not None:
  356. conditions.append(Cases.open_date.between(start_open_date, end_open_date))
  357. if case_customer_id is not None:
  358. conditions.append(Cases.client_id == case_customer_id)
  359. if case_ids is not None:
  360. conditions.append(Cases.case_id.in_(case_ids))
  361. if case_name is not None:
  362. conditions.append(Cases.name.ilike(f'%{case_name}%'))
  363. if case_description is not None:
  364. conditions.append(Cases.description.ilike(f'%{case_description}%'))
  365. if case_classification_id is not None:
  366. conditions.append(Cases.classification_id == case_classification_id)
  367. if case_owner_id is not None:
  368. conditions.append(Cases.owner_id == case_owner_id)
  369. if case_opening_user_id is not None:
  370. conditions.append(Cases.user_id == case_opening_user_id)
  371. if case_severity_id is not None:
  372. conditions.append(Cases.severity_id == case_severity_id)
  373. if case_state_id is not None:
  374. conditions.append(Cases.state_id == case_state_id)
  375. if case_soc_id is not None:
  376. conditions.append(Cases.soc_id == case_soc_id)
  377. if search_value is not None:
  378. conditions.append(Cases.name.like(f"%{search_value}%"))
  379. if case_open_since is not None:
  380. result = date.today() - timedelta(case_open_since)
  381. conditions.append(Cases.open_date == result)
  382. if is_open is not None:
  383. if is_open:
  384. conditions.append(Cases.close_date.is_(None))
  385. else:
  386. conditions.append(Cases.close_date.is_not(None))
  387. if len(conditions) > 1:
  388. conditions = [reduce(and_, conditions)]
  389. conditions.append(Cases.case_id.in_(user_list_cases_view(current_user_id)))
  390. query = Cases.query.filter(*conditions)
  391. if case_tags is not None:
  392. return query.join(Tags, Tags.tag_title.ilike(f'%{case_tags}%')).filter(CaseTags.case_id == Cases.case_id)
  393. if sort_by is not None:
  394. order_func = convert_sort_direction(sort_dir)
  395. if sort_by == 'owner':
  396. query = query.join(User, Cases.owner_id == User.id).order_by(order_func(User.name))
  397. elif sort_by == 'opened_by':
  398. query = query.join(User, Cases.user_id == User.id).order_by(order_func(User.name))
  399. elif sort_by == 'customer_name':
  400. query = query.join(Client, Cases.client_id == Client.client_id).order_by(order_func(Client.name))
  401. elif sort_by == 'state':
  402. query = query.join(CaseState, Cases.state_id == CaseState.state_id).order_by(order_func(CaseState.state_name))
  403. elif hasattr(Cases, sort_by):
  404. query = query.order_by(order_func(getattr(Cases, sort_by)))
  405. return query
  406. def get_filtered_cases(current_user_id,
  407. pagination_parameters: PaginationParameters,
  408. start_open_date: str = None,
  409. end_open_date: str = None,
  410. case_customer_id: int = None,
  411. case_ids: list = None,
  412. case_name: str = None,
  413. case_description: str = None,
  414. case_classification_id: int = None,
  415. case_owner_id: int = None,
  416. case_opening_user_id: int = None,
  417. case_severity_id: int = None,
  418. case_state_id: int = None,
  419. case_soc_id: str = None,
  420. case_open_since: int = None,
  421. search_value=None,
  422. is_open: bool = None
  423. ):
  424. data = build_filter_case_query(case_classification_id=case_classification_id, case_customer_id=case_customer_id, case_description=case_description,
  425. case_ids=case_ids, case_name=case_name, case_opening_user_id=case_opening_user_id, case_owner_id=case_owner_id,
  426. case_severity_id=case_severity_id, case_soc_id=case_soc_id, case_open_since=case_open_since,
  427. case_state_id=case_state_id, current_user_id=current_user_id, end_open_date=end_open_date,
  428. search_value=search_value, start_open_date=start_open_date, is_open=is_open,
  429. sort_by=pagination_parameters.get_order_by(), sort_dir=pagination_parameters.get_direction())
  430. try:
  431. filtered_cases = data.paginate(page=pagination_parameters.get_page(), per_page=pagination_parameters.get_per_page(), error_out=False)
  432. except Exception as e:
  433. app.logger.exception(f'Error getting cases: {str(e)}')
  434. return None
  435. return filtered_cases