Sin descripción

case_timeline_routes.py 38KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060
  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 csv
  19. import json
  20. import urllib.parse
  21. from datetime import datetime
  22. import marshmallow
  23. from flask import Blueprint
  24. from flask import request
  25. from flask_login import current_user
  26. from sqlalchemy import and_
  27. from app import db
  28. from app import app
  29. from app.blueprints.rest.case_comments import case_comment_update
  30. from app.datamgmt.case.case_assets_db import get_asset_by_name
  31. from app.datamgmt.case.case_events_db import add_comment_to_event
  32. from app.datamgmt.case.case_events_db import get_category_by_name
  33. from app.datamgmt.case.case_events_db import get_default_category
  34. from app.datamgmt.case.case_events_db import delete_event
  35. from app.datamgmt.case.case_events_db import delete_event_comment
  36. from app.datamgmt.case.case_events_db import get_case_event
  37. from app.datamgmt.case.case_events_db import get_case_event_comment
  38. from app.datamgmt.case.case_events_db import get_case_event_comments
  39. from app.datamgmt.case.case_events_db import get_case_events_comments_count
  40. from app.datamgmt.case.case_events_db import get_event_assets_ids
  41. from app.datamgmt.case.case_events_db import get_event_category
  42. from app.datamgmt.case.case_events_db import get_event_iocs_ids
  43. from app.datamgmt.case.case_events_db import get_events_categories
  44. from app.datamgmt.case.case_events_db import save_event_category
  45. from app.datamgmt.case.case_events_db import update_event_assets
  46. from app.datamgmt.case.case_events_db import update_event_iocs
  47. from app.datamgmt.case.case_iocs_db import get_ioc_by_value
  48. from app.datamgmt.states import get_timeline_state
  49. from app.datamgmt.states import update_timeline_state
  50. from app.iris_engine.module_handler.module_handler import call_modules_hook
  51. from app.iris_engine.utils.collab import collab_notify
  52. from app.iris_engine.utils.common import parse_bf_date_format
  53. from app.iris_engine.utils.tracker import track_activity
  54. from app.models.models import CompromiseStatus
  55. from app.models.authorization import CaseAccessLevel
  56. from app.models.authorization import User
  57. from app.models.cases import CasesEvent
  58. from app.models.models import AssetsType
  59. from app.models.models import CaseAssets
  60. from app.models.models import CaseEventsAssets
  61. from app.models.models import CaseEventsIoc
  62. from app.models.models import EventCategory
  63. from app.models.models import Ioc
  64. from app.schema.marshables import CommentSchema
  65. from app.schema.marshables import EventSchema
  66. from app.blueprints.access_controls import ac_requires_case_identifier
  67. from app.blueprints.access_controls import ac_api_requires
  68. from app.util import add_obj_history_entry
  69. from app.blueprints.responses import response_error
  70. from app.blueprints.responses import response_success
  71. case_timeline_rest_blueprint = Blueprint('case_timeline_rest', __name__)
  72. @case_timeline_rest_blueprint.route('/case/timeline/events/<int:cur_id>/comments/list', methods=['GET'])
  73. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  74. @ac_api_requires()
  75. def case_comments_get(cur_id, caseid):
  76. event_comments = get_case_event_comments(cur_id, caseid=caseid)
  77. if event_comments is None:
  78. return response_error('Invalid event ID')
  79. return response_success(data=CommentSchema(many=True).dump(event_comments))
  80. @case_timeline_rest_blueprint.route('/case/timeline/events/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
  81. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  82. @ac_api_requires()
  83. def case_comment_delete(cur_id, com_id, caseid):
  84. success, msg = delete_event_comment(cur_id, com_id)
  85. if not success:
  86. return response_error(msg)
  87. call_modules_hook('on_postload_event_comment_delete', data=com_id, caseid=caseid)
  88. track_activity(f"comment {com_id} on event {cur_id} deleted", caseid=caseid)
  89. return response_success(msg)
  90. @case_timeline_rest_blueprint.route('/case/timeline/events/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
  91. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  92. @ac_api_requires()
  93. def case_comment_get(cur_id, com_id, caseid):
  94. comment = get_case_event_comment(cur_id, com_id, caseid=caseid)
  95. if not comment:
  96. return response_error("Invalid comment ID")
  97. return response_success(data=comment._asdict())
  98. @case_timeline_rest_blueprint.route('/case/timeline/events/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
  99. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  100. @ac_api_requires()
  101. def case_comment_edit(cur_id, com_id, caseid):
  102. return case_comment_update(com_id, 'events', caseid)
  103. @case_timeline_rest_blueprint.route('/case/timeline/events/<int:cur_id>/comments/add', methods=['POST'])
  104. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  105. @ac_api_requires()
  106. def case_comment_add(cur_id, caseid):
  107. try:
  108. event = get_case_event(event_id=cur_id, caseid=caseid)
  109. if not event:
  110. return response_error('Invalid event ID')
  111. comment_schema = CommentSchema()
  112. comment = comment_schema.load(request.get_json())
  113. comment.comment_case_id = caseid
  114. comment.comment_user_id = current_user.id
  115. comment.comment_date = datetime.now()
  116. comment.comment_update_date = datetime.now()
  117. db.session.add(comment)
  118. db.session.commit()
  119. add_comment_to_event(event.event_id, comment.comment_id)
  120. add_obj_history_entry(event, 'commented')
  121. db.session.commit()
  122. hook_data = {
  123. "comment": comment_schema.dump(comment),
  124. "event": EventSchema().dump(event)
  125. }
  126. call_modules_hook('on_postload_event_commented', data=hook_data, caseid=caseid)
  127. track_activity(f"event \"{event.event_title}\" commented", caseid=caseid)
  128. return response_success("Event commented", data=comment_schema.dump(comment))
  129. except marshmallow.exceptions.ValidationError as e:
  130. return response_error(msg="Data error", data=e.normalized_messages())
  131. @case_timeline_rest_blueprint.route('/case/timeline/state', methods=['GET'])
  132. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  133. @ac_api_requires()
  134. def case_get_timeline_state(caseid):
  135. os = get_timeline_state(caseid=caseid)
  136. if os:
  137. return response_success(data=os)
  138. return response_error('No timeline state for this case. Add an event to begin')
  139. @case_timeline_rest_blueprint.route('/case/timeline/visualize/data/by-asset', methods=['GET'])
  140. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  141. @ac_api_requires()
  142. def case_getgraph_assets(caseid):
  143. assets_cache = CaseAssets.query.with_entities(
  144. CaseEventsAssets.event_id,
  145. CaseAssets.asset_name
  146. ).filter(
  147. CaseEventsAssets.case_id == caseid,
  148. ).join(CaseEventsAssets.asset).all()
  149. timeline = CasesEvent.query.filter(and_(
  150. CasesEvent.case_id == caseid,
  151. CasesEvent.event_in_summary
  152. )).order_by(
  153. CasesEvent.event_date
  154. ).all()
  155. tim = []
  156. for row in timeline:
  157. for asset in assets_cache:
  158. if asset.event_id == row.event_id:
  159. tmp = {'date': row.event_date, 'group': asset.asset_name, 'content': row.event_title,
  160. 'title': f"{row.event_date.strftime('%Y-%m-%dT%H:%M:%S')} - {row.event_content}"}
  161. if row.event_color:
  162. tmp['style'] = f'background-color: {row.event_color};'
  163. tmp['unique_id'] = row.event_id
  164. tim.append(tmp)
  165. res = {
  166. "events": tim
  167. }
  168. return response_success("", data=res)
  169. @case_timeline_rest_blueprint.route('/case/timeline/visualize/data/by-category', methods=['GET'])
  170. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  171. @ac_api_requires()
  172. def case_getgraph(caseid):
  173. timeline = CasesEvent.query.filter(and_(
  174. CasesEvent.case_id == caseid,
  175. CasesEvent.event_in_summary
  176. )).order_by(
  177. CasesEvent.event_date
  178. ).all()
  179. tim = []
  180. for row in timeline:
  181. tmp = {'date': row.event_date, 'group': row.category[0].name if row.category else 'Uncategorized', 'content': row.event_title}
  182. if row.event_content:
  183. content = row.event_content.replace('\n', '<br/>')
  184. else:
  185. content = ''
  186. tmp['title'] = f"<small>{row.event_date.strftime('%Y-%m-%dT%H:%M:%S')}</small><br/>{content}"
  187. if row.event_color:
  188. tmp['style'] = f'background-color: {row.event_color};'
  189. tmp['unique_id'] = row.event_id
  190. tim.append(tmp)
  191. res = {
  192. "events": tim
  193. }
  194. return response_success("", data=res)
  195. @case_timeline_rest_blueprint.route('/case/timeline/events/list', methods=['GET'])
  196. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  197. @ac_api_requires()
  198. def case_gettimeline_api_nofilter(caseid):
  199. return case_gettimeline_api(0)
  200. @case_timeline_rest_blueprint.route('/case/timeline/events/list/filter/<int:asset_id>', methods=['GET'])
  201. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  202. @ac_api_requires()
  203. def case_gettimeline_api(asset_id, caseid):
  204. if asset_id:
  205. condition = and_(
  206. CasesEvent.case_id == caseid,
  207. CaseEventsAssets.asset_id == asset_id,
  208. CaseEventsAssets.event_id == CasesEvent.event_id
  209. )
  210. else:
  211. condition = CasesEvent.case_id == caseid
  212. timeline = CasesEvent.query.with_entities(
  213. CasesEvent.event_id,
  214. CasesEvent.event_uuid,
  215. CasesEvent.event_date,
  216. CasesEvent.event_date_wtz,
  217. CasesEvent.event_tz,
  218. CasesEvent.event_title,
  219. CasesEvent.event_color,
  220. CasesEvent.event_tags,
  221. CasesEvent.event_content,
  222. CasesEvent.event_in_summary,
  223. CasesEvent.event_in_graph,
  224. EventCategory.name.label("category_name"),
  225. EventCategory.id.label("event_category_id")
  226. ).filter(condition).order_by(
  227. CasesEvent.event_date
  228. ).outerjoin(
  229. CasesEvent.category
  230. ).all()
  231. assets_cache = CaseAssets.query.with_entities(
  232. CaseAssets.asset_id,
  233. CaseAssets.asset_name,
  234. CaseEventsAssets.event_id
  235. ).filter(
  236. CaseEventsAssets.case_id == caseid,
  237. ).join(CaseEventsAssets.asset).all()
  238. iocs_cache = CaseEventsIoc.query.with_entities(
  239. Ioc.ioc_id,
  240. Ioc.ioc_value,
  241. CaseEventsIoc.event_id
  242. ).filter(
  243. CaseEventsIoc.case_id == caseid
  244. ).join(
  245. CaseEventsIoc.ioc
  246. ).all()
  247. tim = []
  248. for row in timeline:
  249. ras = row._asdict()
  250. ras['event_date'] = ras['event_date'].strftime('%Y-%m-%dT%H:%M:%S.%f')
  251. ras['event_date_wtz'] = ras['event_date_wtz'].strftime('%Y-%m-%dT%H:%M:%S.%f')
  252. alki = []
  253. cache = {}
  254. for asset in assets_cache:
  255. if asset.event_id == ras['event_id']:
  256. if asset.asset_id not in cache:
  257. cache[asset.asset_id] = asset.asset_name
  258. alki.append(asset._asdict())
  259. alki = []
  260. cache = {}
  261. for ioc in iocs_cache:
  262. if ioc.event_id == ras['event_id']:
  263. if ioc.ioc_id not in cache:
  264. cache[ioc.ioc_id] = ioc.ioc_value
  265. alki.append(ioc._asdict())
  266. ras['iocs'] = alki
  267. tim.append(ras)
  268. resp = {
  269. "timeline": tim,
  270. "state": get_timeline_state(caseid=caseid)
  271. }
  272. return response_success("", data=resp)
  273. @case_timeline_rest_blueprint.route('/case/timeline/advanced-filter', methods=['GET'])
  274. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  275. @ac_api_requires()
  276. def case_filter_timeline(caseid):
  277. args = request.args.to_dict()
  278. query_filter = args.get('q')
  279. try:
  280. filter_d = dict(json.loads(urllib.parse.unquote_plus(query_filter)))
  281. except Exception as _:
  282. return response_error('Invalid query string')
  283. assets = filter_d.get('asset')
  284. assets_id = filter_d.get('asset_id')
  285. event_ids = filter_d.get('event_id')
  286. iocs = filter_d.get('ioc')
  287. iocs_id = filter_d.get('ioc_id')
  288. tags = filter_d.get('tag')
  289. descriptions = filter_d.get('description')
  290. categories = filter_d.get('category')
  291. raws = filter_d.get('raw')
  292. start_date = filter_d.get('startDate')
  293. end_date = filter_d.get('endDate')
  294. titles = filter_d.get('title')
  295. sources = filter_d.get('source')
  296. flag = filter_d.get('flag')
  297. condition = (CasesEvent.case_id == caseid)
  298. if assets:
  299. assets = [asset.lower() for asset in assets]
  300. if assets_id:
  301. assets_id = [int(asset) for asset in assets_id]
  302. if flag:
  303. flags = (flag[0].lower() == 'true')
  304. condition = and_(condition, CasesEvent.event_is_flagged == flags)
  305. if iocs:
  306. iocs = [ioc.lower() for ioc in iocs]
  307. if iocs_id:
  308. iocs_id = [int(ioc) for ioc in iocs_id]
  309. if tags:
  310. for tag in tags:
  311. condition = and_(condition,
  312. CasesEvent.event_tags.ilike(f'%{tag}%'))
  313. if titles:
  314. for title in titles:
  315. condition = and_(condition,
  316. CasesEvent.event_title.ilike(f'%{title}%'))
  317. if sources:
  318. for source in sources:
  319. condition = and_(condition,
  320. CasesEvent.event_source.ilike(f'%{source}%'))
  321. if descriptions:
  322. for description in descriptions:
  323. condition = and_(condition,
  324. CasesEvent.event_content.ilike(f'%{description}%'))
  325. if raws:
  326. for raw in raws:
  327. condition = and_(condition,
  328. CasesEvent.event_raw.ilike(f'%{raw}%'))
  329. if start_date:
  330. try:
  331. parsed_start_date = parse_bf_date_format(start_date[0])
  332. condition = and_(condition,
  333. CasesEvent.event_date >= parsed_start_date)
  334. except Exception as e:
  335. print(e)
  336. if end_date:
  337. try:
  338. parsed_end_date = parse_bf_date_format(end_date[0])
  339. condition = and_(condition,
  340. CasesEvent.event_date <= parsed_end_date)
  341. except Exception as _:
  342. pass
  343. if categories:
  344. for category in categories:
  345. condition = and_(condition,
  346. EventCategory.name == category)
  347. if event_ids:
  348. try:
  349. event_ids = [int(event_id) for event_id in event_ids]
  350. except Exception as _:
  351. return response_error('Invalid event id')
  352. condition = and_(condition,
  353. CasesEvent.event_id.in_(event_ids))
  354. timeline = CasesEvent.query.with_entities(
  355. CasesEvent.event_id,
  356. CasesEvent.event_uuid,
  357. CasesEvent.event_date,
  358. CasesEvent.event_date_wtz,
  359. CasesEvent.event_tz,
  360. CasesEvent.event_title,
  361. CasesEvent.event_color,
  362. CasesEvent.event_tags,
  363. CasesEvent.event_content,
  364. CasesEvent.event_in_summary,
  365. CasesEvent.event_in_graph,
  366. CasesEvent.event_is_flagged,
  367. CasesEvent.parent_event_id,
  368. User.user,
  369. CasesEvent.event_added,
  370. EventCategory.name.label("category_name")
  371. ).filter(condition).order_by(
  372. CasesEvent.event_date
  373. ).outerjoin(
  374. CasesEvent.category
  375. ).join(
  376. CasesEvent.user
  377. ).all()
  378. assets_cache_condition = and_(
  379. CaseEventsAssets.case_id == caseid
  380. )
  381. if assets_id:
  382. assets_cache_condition = and_(
  383. assets_cache_condition,
  384. CaseEventsAssets.asset_id.in_(assets_id)
  385. )
  386. assets_cache = (CaseAssets.query.with_entities(
  387. CaseEventsAssets.event_id,
  388. CaseAssets.asset_id,
  389. CaseAssets.asset_name,
  390. AssetsType.asset_name.label('type'),
  391. CaseAssets.asset_ip,
  392. CaseAssets.asset_description,
  393. CaseAssets.asset_compromise_status_id
  394. ).filter(
  395. assets_cache_condition
  396. ).join(CaseEventsAssets.asset)
  397. .join(CaseAssets.asset_type).all())
  398. iocs_cache_condition = and_(
  399. CaseEventsIoc.case_id == caseid
  400. )
  401. if iocs_id:
  402. iocs_cache_condition = and_(
  403. iocs_cache_condition,
  404. CaseEventsIoc.ioc_id.in_(iocs_id)
  405. )
  406. iocs_cache = CaseEventsIoc.query.with_entities(
  407. CaseEventsIoc.event_id,
  408. CaseEventsIoc.ioc_id,
  409. Ioc.ioc_value,
  410. Ioc.ioc_description
  411. ).filter(
  412. iocs_cache_condition
  413. ).join(
  414. CaseEventsIoc.ioc
  415. ).all()
  416. assets_map = {}
  417. cache = {}
  418. for asset in assets_cache:
  419. if asset.asset_id not in cache:
  420. cache[asset.asset_id] = [asset.asset_name, asset.type]
  421. if (assets and asset.asset_name.lower() in assets) \
  422. or (assets_id and asset.asset_id in assets_id):
  423. if asset.event_id in assets_map:
  424. assets_map[asset.event_id] += 1
  425. else:
  426. assets_map[asset.event_id] = 1
  427. assets_filter = []
  428. len_assets = 0
  429. if assets:
  430. len_assets += len(assets)
  431. if assets_id:
  432. len_assets += len(assets_id)
  433. for event_id in assets_map:
  434. if assets_map[event_id] == len_assets:
  435. assets_filter.append(event_id)
  436. iocs_filter = []
  437. if iocs:
  438. for ioc in iocs_cache:
  439. if ioc.event_id not in iocs_filter and ioc.ioc_value.lower() in iocs:
  440. iocs_filter.append(ioc.event_id)
  441. tim = []
  442. events_list = []
  443. for row in timeline:
  444. if (assets is not None or assets_id is not None) and row.event_id not in assets_filter:
  445. continue
  446. if iocs is not None and row.event_id not in iocs_filter:
  447. continue
  448. ras = row._asdict()
  449. ras['event_date'] = ras['event_date'].strftime('%Y-%m-%dT%H:%M:%S.%f')
  450. ras['event_date_wtz'] = ras['event_date_wtz'].strftime('%Y-%m-%dT%H:%M:%S.%f') if ras[
  451. 'event_date_wtz'] else None
  452. ras['event_added'] = ras['event_added'].strftime('%Y-%m-%dT%H:%M:%S')
  453. if row.event_id not in events_list:
  454. events_list.append(row.event_id)
  455. alki = []
  456. for asset in assets_cache:
  457. if asset.event_id == ras['event_id']:
  458. alki.append(
  459. {
  460. "name": "{} ({})".format(asset.asset_name, asset.type),
  461. "ip": asset.asset_ip,
  462. "description": asset.asset_description,
  463. "compromised": asset.asset_compromise_status_id == CompromiseStatus.compromised.value
  464. }
  465. )
  466. ras['assets'] = alki
  467. alki = []
  468. for ioc in iocs_cache:
  469. if ioc.event_id == ras['event_id']:
  470. if ioc.ioc_id not in cache:
  471. cache[ioc.ioc_id] = [ioc.ioc_value]
  472. alki.append(
  473. {
  474. "name": "{}".format(ioc.ioc_value),
  475. "description": ioc.ioc_description
  476. }
  477. )
  478. ras['iocs'] = alki
  479. tim.append(ras)
  480. if request.cookies.get('session'):
  481. iocs = Ioc.query.with_entities(
  482. Ioc.ioc_id,
  483. Ioc.ioc_value,
  484. Ioc.ioc_description,
  485. ).filter(
  486. Ioc.case_id == caseid
  487. ).all()
  488. events_comments_map = {}
  489. events_comments_set = get_case_events_comments_count(events_list)
  490. for k, v in events_comments_set:
  491. events_comments_map.setdefault(k, []).append(v)
  492. resp = {
  493. "tim": tim,
  494. "comments_map": events_comments_map,
  495. "assets": cache,
  496. "iocs": [ioc._asdict() for ioc in iocs],
  497. "categories": [cat.name for cat in get_events_categories()],
  498. "state": get_timeline_state(caseid=caseid)
  499. }
  500. else:
  501. resp = {
  502. "timeline": tim,
  503. "state": get_timeline_state(caseid=caseid)
  504. }
  505. return response_success("ok", data=resp)
  506. @case_timeline_rest_blueprint.route('/case/timeline/events/delete/<int:cur_id>', methods=['POST'])
  507. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  508. @ac_api_requires()
  509. def case_delete_event(cur_id, caseid):
  510. call_modules_hook('on_preload_event_delete', data=cur_id, caseid=caseid)
  511. event = get_case_event(event_id=cur_id, caseid=caseid)
  512. if not event:
  513. return response_error('Not a valid event ID for this case')
  514. delete_event(event=event, caseid=caseid)
  515. call_modules_hook('on_postload_event_delete', data=cur_id, caseid=caseid)
  516. collab_notify(caseid, 'events', 'deletion', cur_id)
  517. track_activity(f"deleted event \"{event.event_title}\" in timeline", caseid)
  518. return response_success('Event ID {} deleted'.format(cur_id))
  519. @case_timeline_rest_blueprint.route('/case/timeline/events/flag/<int:cur_id>', methods=['GET'])
  520. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  521. @ac_api_requires()
  522. def event_flag(cur_id, caseid):
  523. event = get_case_event(cur_id, caseid)
  524. if not event:
  525. return response_error("Invalid event ID for this case")
  526. event.event_is_flagged = not event.event_is_flagged
  527. db.session.commit()
  528. collab_notify(caseid, 'events', 'flagged' if event.event_is_flagged else "un-flagged", cur_id)
  529. return response_success("Event flagged" if event.event_is_flagged else "Event unflagged", data=event)
  530. @case_timeline_rest_blueprint.route('/case/timeline/events/<int:cur_id>', methods=['GET'])
  531. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  532. @ac_api_requires()
  533. def event_view(cur_id, caseid):
  534. event = get_case_event(cur_id, caseid)
  535. if not event:
  536. return response_error("Invalid event ID for this case")
  537. event_schema = EventSchema()
  538. linked_assets = get_event_assets_ids(cur_id, caseid)
  539. linked_iocs = get_event_iocs_ids(cur_id, caseid)
  540. output = event_schema.dump(event)
  541. output['event_assets'] = linked_assets
  542. output['event_iocs'] = linked_iocs
  543. output['event_category_id'] = event.category[0].id if event.category else None
  544. output['event_comments_map'] = [c._asdict() for c in get_case_events_comments_count([cur_id])]
  545. return response_success(data=output)
  546. @case_timeline_rest_blueprint.route('/case/timeline/events/update/<int:cur_id>', methods=['POST'])
  547. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  548. @ac_api_requires()
  549. def case_edit_event(cur_id, caseid):
  550. try:
  551. event = get_case_event(cur_id, caseid)
  552. if not event:
  553. return response_error("Invalid event ID for this case")
  554. event_schema = EventSchema()
  555. request_data = call_modules_hook('on_preload_event_update', data=request.get_json(), caseid=caseid)
  556. request_data['event_id'] = cur_id
  557. event = event_schema.load(request_data, instance=event)
  558. event.event_date, event.event_date_wtz = event_schema.validate_date(
  559. request_data.get(u'event_date'),
  560. request_data.get(u'event_tz')
  561. )
  562. event.case_id = caseid
  563. add_obj_history_entry(event, 'updated')
  564. update_timeline_state(caseid=caseid)
  565. db.session.commit()
  566. save_event_category(event.event_id, request_data.get('event_category_id'))
  567. setattr(event, 'event_category_id', request_data.get('event_category_id'))
  568. success, log = update_event_assets(event_id=event.event_id,
  569. caseid=caseid,
  570. assets_list=request_data.get('event_assets'),
  571. iocs_list=request_data.get('event_iocs'),
  572. sync_iocs_assets=request_data.get('event_sync_iocs_assets'))
  573. if not success:
  574. return response_error('Error while saving linked assets', data=log)
  575. success, log = update_event_iocs(event_id=event.event_id,
  576. caseid=caseid,
  577. iocs_list=request_data.get('event_iocs'))
  578. if not success:
  579. return response_error('Error while saving linked iocs', data=log)
  580. event = call_modules_hook('on_postload_event_update', data=event, caseid=caseid)
  581. track_activity(f"updated event \"{event.event_title}\"", caseid=caseid)
  582. event_dump = event_schema.dump(event)
  583. collab_notify(case_id=caseid,
  584. object_type='events',
  585. action_type='updated',
  586. object_id=cur_id,
  587. object_data=event_dump)
  588. return response_success("Event updated", data=event_dump)
  589. except marshmallow.exceptions.ValidationError as e:
  590. return response_error(msg="Data error", data=e.normalized_messages())
  591. @case_timeline_rest_blueprint.route('/case/timeline/events/add', methods=['POST'])
  592. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  593. @ac_api_requires()
  594. def case_add_event(caseid):
  595. try:
  596. event_schema = EventSchema()
  597. request_data = call_modules_hook('on_preload_event_create', data=request.get_json(), caseid=caseid)
  598. event = event_schema.load(request_data)
  599. event.event_date, event.event_date_wtz = event_schema.validate_date(request_data.get(u'event_date'),
  600. request_data.get(u'event_tz'))
  601. event.case_id = caseid
  602. event.event_added = datetime.utcnow()
  603. event.user_id = current_user.id
  604. add_obj_history_entry(event, 'created')
  605. db.session.add(event)
  606. update_timeline_state(caseid=caseid)
  607. db.session.commit()
  608. save_event_category(event.event_id, request_data.get('event_category_id'))
  609. setattr(event, 'event_category_id', request_data.get('event_category_id'))
  610. sync_iocs_assets = request_data.get('event_sync_iocs_assets') if request_data.get(
  611. 'event_sync_iocs_assets') else False
  612. success, log = update_event_assets(event_id=event.event_id,
  613. caseid=caseid,
  614. assets_list=request_data.get('event_assets'),
  615. iocs_list=request_data.get('event_iocs'),
  616. sync_iocs_assets=sync_iocs_assets)
  617. if not success:
  618. return response_error('Error while saving linked assets', data=log)
  619. success, log = update_event_iocs(event_id=event.event_id,
  620. caseid=caseid,
  621. iocs_list=request_data.get('event_iocs'))
  622. if not success:
  623. return response_error('Error while saving linked iocs', data=log)
  624. setattr(event, 'event_category_id', request_data.get('event_category_id'))
  625. event = call_modules_hook('on_postload_event_create', data=event, caseid=caseid)
  626. track_activity(f"added event \"{event.event_title}\"", caseid=caseid)
  627. return response_success("Event added", data=event_schema.dump(event))
  628. except marshmallow.exceptions.ValidationError as e:
  629. return response_error(msg="Data error", data=e.normalized_messages())
  630. @case_timeline_rest_blueprint.route('/case/timeline/events/duplicate/<int:cur_id>', methods=['GET'])
  631. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  632. @ac_api_requires()
  633. def case_duplicate_event(cur_id, caseid):
  634. call_modules_hook('on_preload_event_duplicate', data=cur_id, caseid=caseid)
  635. try:
  636. event_schema = EventSchema()
  637. old_event = get_case_event(event_id=cur_id, caseid=caseid)
  638. if not old_event:
  639. return response_error("Invalid event ID for this case")
  640. # Create new Event
  641. event = CasesEvent()
  642. orig_event_id = event.event_id
  643. # Transfer duplicated event's attributes to new event
  644. for key in dir(old_event):
  645. if not key.startswith('_') and key not in ['query', 'query_class', 'registry', 'metadata']:
  646. setattr(event, key, getattr(old_event, key))
  647. event.event_id = orig_event_id
  648. # Override event_added and user_id
  649. event.event_added = datetime.utcnow()
  650. event.user_id = current_user.id
  651. if event.event_title.startswith("[DUPLICATED] - ") is False:
  652. event.event_title = f"[DUPLICATED] - {event.event_title}"
  653. db.session.add(event)
  654. update_timeline_state(caseid=caseid)
  655. db.session.commit()
  656. # Update category
  657. old_event_category = get_event_category(old_event.event_id)
  658. if old_event_category is not None:
  659. save_event_category(event.event_id, old_event_category.category_id)
  660. iocs_list = get_event_iocs_ids(old_event.event_id, caseid)
  661. # Update assets mapping
  662. assets_list = get_event_assets_ids(old_event.event_id, caseid)
  663. success, log = update_event_assets(event_id=event.event_id,
  664. caseid=caseid,
  665. assets_list=assets_list,
  666. iocs_list=iocs_list,
  667. sync_iocs_assets=False)
  668. if not success:
  669. return response_error('Error while saving linked assets', data=log)
  670. # Update iocs mapping
  671. success, log = update_event_iocs(event_id=event.event_id,
  672. caseid=caseid,
  673. iocs_list=iocs_list)
  674. if not success:
  675. return response_error('Error while saving linked iocs', data=log)
  676. event = call_modules_hook('on_postload_event_create', data=event, caseid=caseid)
  677. track_activity(f"added event \"{event.event_title}\"", caseid=caseid)
  678. return response_success("Event duplicated", data=event_schema.dump(event))
  679. except marshmallow.exceptions.ValidationError as e:
  680. return response_error(msg="Data error", data=e.normalized_messages())
  681. @case_timeline_rest_blueprint.route('/case/timeline/events/convert-date', methods=['POST'])
  682. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  683. @ac_api_requires()
  684. def case_event_date_convert(caseid):
  685. jsdata = request.get_json()
  686. date_value = jsdata.get('date_value')
  687. if not date_value:
  688. return response_error("Invalid request")
  689. parsed_date = parse_bf_date_format(date_value)
  690. if parsed_date:
  691. tz = parsed_date.strftime("%z")
  692. data = {
  693. "date": parsed_date.strftime("%Y-%m-%d"),
  694. "time": parsed_date.strftime("%H:%M:%S.%f")[:-3],
  695. "tz": tz if tz else "+00:00"
  696. }
  697. return response_success("Date parsed", data=data)
  698. return response_error("Unable to find a matching date format")
  699. # BEGIN_RS_CODE
  700. @case_timeline_rest_blueprint.route('/case/timeline/events/csv_upload', methods=['POST'])
  701. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  702. @ac_api_requires()
  703. def case_events_upload_csv(caseid):
  704. event_schema = EventSchema()
  705. jsdata = request.get_json()
  706. app.logger.info("Starting CSV import")
  707. event_fields = [
  708. "event_date",
  709. "event_tz",
  710. "event_title",
  711. "event_category",
  712. "event_content",
  713. "event_raw",
  714. "event_source",
  715. "event_assets",
  716. "event_iocs",
  717. "event_tags"
  718. ]
  719. csv_lines = jsdata["CSVData"].splitlines()
  720. csv_options = jsdata.get('CSVOptions') if jsdata.get('CSVOptions') else {}
  721. event_sync_iocs_assets = csv_options.get('event_sync_iocs_assets') if csv_options.get(
  722. 'event_sync_iocs_assets') else False
  723. event_in_summary = csv_options.get('event_in_summary') if csv_options.get('event_in_summary') else False
  724. event_in_graph = csv_options.get('event_in_graph') if csv_options.get('event_in_graph') else True
  725. event_source = csv_options.get('event_source') if csv_options.get('event_source') else ''
  726. csv_data = list(csv.DictReader(csv_lines, delimiter=','))
  727. missing_fields = []
  728. row0 = csv_data[0]
  729. for fld in event_fields:
  730. if row0.get(fld) is None:
  731. missing_fields.append(fld)
  732. if len(missing_fields) > 0:
  733. csv_fields = list(row0.keys())
  734. msg = f"Bad SCV Fields Mapping. Fields missing: [{','.join(missing_fields)}]"
  735. data = {"error_code": "BAD_FIELDS_MAPPING", "expected": ','.join(event_fields), "found": ','.join(csv_fields),
  736. "missing": ','.join(missing_fields)}
  737. app.logger.warning(data)
  738. return response_error(msg=msg, data=data)
  739. DEFAULT_CAT_ID = get_default_category().id
  740. # ========================== checking data validity (assets, ioc, categories, etc... ) ==========================
  741. line = 0
  742. csv_lines = []
  743. try:
  744. for row in csv_data:
  745. event_title = row.get('event_title')
  746. event_assets = row.get('event_assets')
  747. event_iocs = row.get('event_iocs')
  748. event_tags = row.get('event_tags')
  749. event_category_name = row.pop('event_category')
  750. line += 1
  751. if len(event_title) == 0:
  752. return response_error(msg="Data error",
  753. data={"Error": f"Event Title can not be empty.\nrow number: {line}"})
  754. assets = []
  755. for asset_name in event_assets.split(";"):
  756. if asset_name == '':
  757. continue
  758. asset = get_asset_by_name(asset_name, caseid)
  759. if asset:
  760. assets.append(asset.asset_id)
  761. else:
  762. return response_error(msg="Data error", data={
  763. "Error": f"Asset not recognized : {asset_name}.\nrow number: {line}"})
  764. row['event_assets'] = assets
  765. iocs = []
  766. for ioc_value in event_iocs.split("|"):
  767. if ioc_value == '':
  768. continue
  769. ioc = get_ioc_by_value(ioc_value, caseid)
  770. if ioc:
  771. iocs.append(ioc.ioc_id)
  772. else:
  773. return response_error(msg="Data error",
  774. data={"Error": f"IoC not recognized : {ioc_value}.\nrow number: {line}"})
  775. row['event_iocs'] = iocs
  776. if (event_category_name is not None) and (event_category_name != ''):
  777. event_category = get_category_by_name(event_category_name)
  778. if event_category:
  779. row['event_category_id'] = event_category.id
  780. else:
  781. return response_error(msg="Data error", data={
  782. "Error": f"event_category not recognized : {event_category}.\nrow number: {line}"})
  783. else:
  784. row['event_category_id'] = DEFAULT_CAT_ID
  785. if event_tags:
  786. row['event_tags'] = ','.join(event_tags.split('|'))
  787. row['event_in_summary'] = event_in_summary
  788. row['event_in_graph'] = event_in_graph
  789. row['event_source'] = event_source
  790. csv_lines.append(row)
  791. except Exception as e:
  792. return response_error(msg="Data error", data={"Exception": f"Unhandled error {e}.\nrow number: {line}"})
  793. # ========================== begin saving data ============================
  794. session = db.session.begin_nested()
  795. line = 0
  796. try:
  797. for row in csv_lines:
  798. if row is None:
  799. continue
  800. line += 1
  801. request_data = call_modules_hook('on_preload_event_create', data=row, caseid=caseid)
  802. event = event_schema.load(request_data)
  803. event.event_date, event.event_date_wtz = event_schema.validate_date(request_data.get(u'event_date'),
  804. request_data.get(u'event_tz'))
  805. event.case_id = caseid
  806. event.event_added = datetime.utcnow()
  807. event.user_id = current_user.id
  808. add_obj_history_entry(event, 'created')
  809. db.session.add(event)
  810. update_timeline_state(caseid=caseid)
  811. save_event_category(event.event_id, request_data.get('event_category_id'))
  812. setattr(event, 'event_category_id', request_data.get('event_category_id'))
  813. success, log = update_event_assets(event_id=event.event_id,
  814. caseid=caseid,
  815. assets_list=request_data.get('event_assets'),
  816. iocs_list=request_data.get('event_iocs'),
  817. sync_iocs_assets=event_sync_iocs_assets)
  818. if not success:
  819. raise Exception(f'Error while saving linked assets\nlog:{log}')
  820. success, log = update_event_iocs(event_id=event.event_id,
  821. caseid=caseid,
  822. iocs_list=request_data.get('event_iocs'))
  823. if not success:
  824. raise Exception(f'Error while saving linked iocs\nlog:{log}')
  825. setattr(event, 'event_category_id', request_data.get('event_category_id'))
  826. event = call_modules_hook('on_postload_event_create', data=event, caseid=caseid)
  827. track_activity("added event {}".format(event.event_id), caseid=caseid)
  828. except marshmallow.exceptions.ValidationError as e:
  829. return response_error(msg="Data error", data=e.normalized_messages())
  830. except Exception as e:
  831. return response_error(msg="Data error", data={"Error": f"{e}"})
  832. try:
  833. session.commit()
  834. except:
  835. pass
  836. app.logger.info("======================== END_CSV_IMPORT ==========================================")
  837. return response_success(msg="Events added (CSV File)")