Geen omschrijving

reporter.py 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. # IRIS Source Code
  2. # Copyright (C) 2022 - DFIR IRIS Team
  3. # contact@dfir-iris.org
  4. # Copyright (C) 2021 - Airbus CyberSecurity (SAS)
  5. # ir@cyberactionlab.net
  6. #
  7. # This program is free software; you can redistribute it and/or
  8. # modify it under the terms of the GNU Lesser General Public
  9. # License as published by the Free Software Foundation; either
  10. # version 3 of the License, or (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  15. # Lesser General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Lesser General Public License
  18. # along with this program; if not, write to the Free Software Foundation,
  19. # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  20. import logging as log
  21. import os
  22. from datetime import datetime
  23. from docx_generator.docx_generator import DocxGenerator
  24. from docx_generator.exceptions import rendering_error
  25. from flask_login import current_user
  26. from sqlalchemy import desc
  27. from app import app
  28. from app.business.cases import cases_export_to_report_json
  29. from app.business.cases import cases_export_to_json
  30. from app.datamgmt.activities.activities_db import get_auto_activities
  31. from app.datamgmt.activities.activities_db import get_manual_activities
  32. from app.datamgmt.case.case_db import case_get_desc_crc
  33. from app.models.models import AssetsType
  34. from app.models.models import CaseAssets
  35. from app.models.models import CaseEventsAssets
  36. from app.models.models import CaseReceivedFile
  37. from app.models.models import CaseTemplateReport
  38. from app.models.cases import CasesEvent
  39. from app.models.models import Ioc
  40. from app.models.models import IocAssetLink
  41. from app.iris_engine.reporter.ImageHandler import ImageHandler
  42. from app.iris_engine.utils.common import IrisJinjaEnv
  43. LOG_FORMAT = '%(asctime)s :: %(levelname)s :: %(module)s :: %(funcName)s :: %(message)s'
  44. log.basicConfig(level=log.INFO, format=LOG_FORMAT)
  45. class IrisReportMaker(object):
  46. """
  47. IRIS generical report maker
  48. """
  49. def __init__(self, tmp_dir, report_id, caseid, safe_mode=False):
  50. self._tmp = tmp_dir
  51. self._report_id = report_id
  52. self._case_info = {}
  53. self._caseid = caseid
  54. self.safe_mode = safe_mode
  55. def get_case_info(self, doc_type):
  56. """Returns case information
  57. Args:
  58. doc_type (_type_): Investigation or Activities report
  59. Returns:
  60. _type_: case info
  61. """
  62. if doc_type == 'Investigation':
  63. case_info = self._get_case_info()
  64. elif doc_type == 'Activities':
  65. case_info = self._get_activity_info()
  66. else:
  67. log.error("Unknown report type")
  68. return None
  69. return case_info
  70. def _get_activity_info(self):
  71. auto_activities = get_auto_activities(self._caseid)
  72. manual_activities = get_manual_activities(self._caseid)
  73. case_info_in = self._get_case_info()
  74. # Format information and generate the activity report #
  75. doc_id = "{}".format(datetime.utcnow().strftime("%y%m%d_%H%M"))
  76. case_info = {
  77. 'auto_activities': auto_activities,
  78. 'manual_activities': manual_activities,
  79. 'date': datetime.utcnow(),
  80. 'gen_user': current_user.name,
  81. 'case': {'name': case_info_in['case'].get('name'),
  82. 'open_date': case_info_in['case'].get('open_date'),
  83. 'for_customer': case_info_in['case'].get('client').get('customer_name'),
  84. 'client': case_info_in['case'].get('client')
  85. },
  86. 'doc_id': doc_id
  87. }
  88. return case_info
  89. def _get_case_info(self):
  90. """
  91. Retrieve information of the case
  92. :return:
  93. """
  94. case_info = cases_export_to_json(self._caseid)
  95. # Get customer, user and case title
  96. case_info['doc_id'] = IrisReportMaker.get_docid()
  97. case_info['user'] = current_user.name
  98. # Set date
  99. case_info['date'] = datetime.utcnow().strftime("%Y-%m-%d")
  100. return case_info
  101. @staticmethod
  102. def get_case_summary(caseid):
  103. """
  104. Retrieve the case summary from thehive
  105. :return:
  106. """
  107. _crc32, descr = case_get_desc_crc(caseid)
  108. return descr
  109. @staticmethod
  110. def get_case_files(caseid):
  111. """
  112. Retrieve the list of files with their hashes
  113. :return:
  114. """
  115. files = CaseReceivedFile.query.filter(
  116. CaseReceivedFile.case_id == caseid
  117. ).with_entities(
  118. CaseReceivedFile.filename,
  119. CaseReceivedFile.date_added,
  120. CaseReceivedFile.file_hash,
  121. CaseReceivedFile.custom_attributes
  122. ).order_by(
  123. CaseReceivedFile.date_added
  124. ).all()
  125. if files:
  126. return [row._asdict() for row in files]
  127. else:
  128. return []
  129. @staticmethod
  130. def get_case_timeline(caseid):
  131. """
  132. Retrieve the case timeline
  133. :return:
  134. """
  135. timeline = CasesEvent.query.filter(
  136. CasesEvent.case_id == caseid
  137. ).order_by(
  138. CasesEvent.event_date
  139. ).all()
  140. cache_id = {}
  141. ras = {}
  142. tim = []
  143. for row in timeline:
  144. ras = row
  145. setattr(ras, 'asset', None)
  146. as_list = CaseEventsAssets.query.with_entities(
  147. CaseAssets.asset_id,
  148. CaseAssets.asset_name,
  149. AssetsType.asset_name.label('type')
  150. ).filter(
  151. CaseEventsAssets.event_id == row.event_id
  152. ).join(CaseEventsAssets.asset, CaseAssets.asset_type).all()
  153. alki = []
  154. for asset in as_list:
  155. alki.append("{} ({})".format(asset.asset_name, asset.type))
  156. setattr(ras, 'asset', "\r\n".join(alki))
  157. tim.append(ras)
  158. return tim
  159. @staticmethod
  160. def get_case_ioc(caseid):
  161. """
  162. Retrieve the list of IOC linked to the case
  163. :return:
  164. """
  165. res = Ioc.query.distinct().with_entities(
  166. Ioc.ioc_value,
  167. Ioc.ioc_type,
  168. Ioc.ioc_description,
  169. Ioc.ioc_tags,
  170. Ioc.custom_attributes
  171. ).filter(
  172. Ioc.case_id == caseid
  173. ).order_by(Ioc.ioc_type).all()
  174. if res:
  175. return [row._asdict() for row in res]
  176. else:
  177. return []
  178. @staticmethod
  179. def get_case_assets(caseid):
  180. """
  181. Retrieve the assets linked ot the case
  182. :return:
  183. """
  184. ret = []
  185. res = CaseAssets.query.distinct().with_entities(
  186. CaseAssets.asset_id,
  187. CaseAssets.asset_name,
  188. CaseAssets.asset_description,
  189. CaseAssets.asset_compromised.label('compromised'),
  190. AssetsType.asset_name.label("type"),
  191. CaseAssets.custom_attributes,
  192. CaseAssets.asset_tags
  193. ).filter(
  194. CaseAssets.case_id == caseid
  195. ).join(
  196. CaseAssets.asset_type
  197. ).order_by(desc(CaseAssets.asset_compromised)).all()
  198. for row in res:
  199. row = row._asdict()
  200. row['light_asset_description'] = row['asset_description']
  201. ial = IocAssetLink.query.with_entities(
  202. Ioc.ioc_value,
  203. Ioc.ioc_type,
  204. Ioc.ioc_description
  205. ).filter(
  206. IocAssetLink.asset_id == row['asset_id']
  207. ).join(
  208. IocAssetLink.ioc
  209. ).all()
  210. if ial:
  211. row['asset_ioc'] = [row._asdict() for row in ial]
  212. else:
  213. row['asset_ioc'] = []
  214. ret.append(row)
  215. return ret
  216. @staticmethod
  217. def get_docid():
  218. return "{}".format(
  219. datetime.utcnow().strftime("%y%m%d_%H%M"))
  220. @staticmethod
  221. def markdown_to_text(markdown_string):
  222. """
  223. Converts a markdown string to plaintext
  224. """
  225. return markdown_string.replace('\n', '</w:t></w:r><w:r/></w:p><w:p><w:r><w:t xml:space="preserve">').replace(
  226. '#', '')
  227. class IrisMakeDocReport(IrisReportMaker):
  228. """
  229. Generates a DOCX report for the case
  230. """
  231. def __init__(self, tmp_dir, report_id, caseid, safe_mode=False):
  232. self._tmp = tmp_dir
  233. self._report_id = report_id
  234. self._case_info = {}
  235. self._caseid = caseid
  236. self._safe_mode = safe_mode
  237. def generate_doc_report(self, doc_type):
  238. """
  239. Actually generates the report
  240. :return:
  241. """
  242. if doc_type == 'Investigation':
  243. case_info = self._get_case_info()
  244. elif doc_type == 'Activities':
  245. case_info = self._get_activity_info()
  246. else:
  247. log.error("Unknown report type")
  248. return None
  249. report = CaseTemplateReport.query.filter(CaseTemplateReport.id == self._report_id).first()
  250. name = "{}".format("{}.docx".format(report.naming_format))
  251. name = name.replace("%code_name%", case_info['doc_id'])
  252. name = name.replace('%customer%', case_info['case']['client']['customer_name'])
  253. name = name.replace('%case_name%', case_info['case'].get('name'))
  254. name = name.replace('%date%', datetime.utcnow().strftime("%Y-%m-%d"))
  255. output_file_path = os.path.join(self._tmp, name)
  256. try:
  257. if not self._safe_mode:
  258. image_handler = ImageHandler(template=None, base_path='/')
  259. else:
  260. image_handler = None
  261. generator = DocxGenerator(image_handler=image_handler)
  262. generator.generate_docx("/",
  263. os.path.join(app.config['TEMPLATES_PATH'], report.internal_reference),
  264. case_info,
  265. output_file_path
  266. )
  267. return output_file_path, ""
  268. except rendering_error.RenderingError as e:
  269. return None, e.__str__()
  270. def _get_activity_info(self):
  271. auto_activities = get_auto_activities(self._caseid)
  272. manual_activities = get_manual_activities(self._caseid)
  273. case_info_in = self._get_case_info()
  274. # Format information and generate the activity report #
  275. doc_id = "{}".format(datetime.utcnow().strftime("%y%m%d_%H%M"))
  276. case_info = {
  277. 'auto_activities': auto_activities,
  278. 'manual_activities': manual_activities,
  279. 'date': datetime.utcnow(),
  280. 'gen_user': current_user.name,
  281. 'case': {'name': case_info_in['case'].get('name'),
  282. 'open_date': case_info_in['case'].get('open_date'),
  283. 'for_customer': case_info_in['case'].get('for_customer'),
  284. 'client': case_info_in['case'].get('client')
  285. },
  286. 'doc_id': doc_id
  287. }
  288. return case_info
  289. def _get_case_info(self):
  290. """
  291. Retrieve information of the case
  292. :return:
  293. """
  294. case_info = cases_export_to_report_json(self._caseid)
  295. # Get customer, user and case title
  296. case_info['doc_id'] = IrisMakeDocReport.get_docid()
  297. case_info['user'] = current_user.name
  298. # Set date
  299. case_info['date'] = datetime.utcnow().strftime("%Y-%m-%d")
  300. return case_info
  301. @staticmethod
  302. def get_case_summary(caseid):
  303. """
  304. Retrieve the case summary from thehive
  305. :return:
  306. """
  307. _crc32, descr = case_get_desc_crc(caseid)
  308. return descr
  309. @staticmethod
  310. def get_case_files(caseid):
  311. """
  312. Retrieve the list of files with their hashes
  313. :return:
  314. """
  315. files = CaseReceivedFile.query.filter(
  316. CaseReceivedFile.case_id == caseid
  317. ).with_entities(
  318. CaseReceivedFile.filename,
  319. CaseReceivedFile.date_added,
  320. CaseReceivedFile.file_hash,
  321. CaseReceivedFile.custom_attributes
  322. ).order_by(
  323. CaseReceivedFile.date_added
  324. ).all()
  325. if files:
  326. return [row._asdict() for row in files]
  327. else:
  328. return []
  329. @staticmethod
  330. def get_case_timeline(caseid):
  331. """
  332. Retrieve the case timeline
  333. :return:
  334. """
  335. timeline = CasesEvent.query.filter(
  336. CasesEvent.case_id == caseid
  337. ).order_by(
  338. CasesEvent.event_date
  339. ).all()
  340. cache_id = {}
  341. ras = {}
  342. tim = []
  343. for row in timeline:
  344. ras = row
  345. setattr(ras, 'asset', None)
  346. as_list = CaseEventsAssets.query.with_entities(
  347. CaseAssets.asset_id,
  348. CaseAssets.asset_name,
  349. AssetsType.asset_name.label('type')
  350. ).filter(
  351. CaseEventsAssets.event_id == row.event_id
  352. ).join(CaseEventsAssets.asset, CaseAssets.asset_type).all()
  353. alki = []
  354. for asset in as_list:
  355. alki.append("{} ({})".format(asset.asset_name, asset.type))
  356. setattr(ras, 'asset', "\r\n".join(alki))
  357. tim.append(ras)
  358. return tim
  359. @staticmethod
  360. def get_case_ioc(caseid):
  361. """
  362. Retrieve the list of IOC linked to the case
  363. :return:
  364. """
  365. res = Ioc.query.distinct().with_entities(
  366. Ioc.ioc_value,
  367. Ioc.ioc_type,
  368. Ioc.ioc_description,
  369. Ioc.ioc_tags,
  370. Ioc.custom_attributes
  371. ).filter(
  372. Ioc.case_id == caseid
  373. ).order_by(Ioc.ioc_type).all()
  374. if res:
  375. return [row._asdict() for row in res]
  376. else:
  377. return []
  378. @staticmethod
  379. def get_case_assets(caseid):
  380. """
  381. Retrieve the assets linked ot the case
  382. :return:
  383. """
  384. ret = []
  385. res = CaseAssets.query.distinct().with_entities(
  386. CaseAssets.asset_id,
  387. CaseAssets.asset_name,
  388. CaseAssets.asset_description,
  389. CaseAssets.asset_compromise_status_id.label('compromise_status'),
  390. AssetsType.asset_name.label("type"),
  391. CaseAssets.custom_attributes,
  392. CaseAssets.asset_tags
  393. ).filter(
  394. CaseAssets.case_id == caseid
  395. ).join(
  396. CaseAssets.asset_type
  397. ).order_by(desc(CaseAssets.asset_compromise_status_id)).all()
  398. for row in res:
  399. row = row._asdict()
  400. row['light_asset_description'] = row['asset_description']
  401. ial = IocAssetLink.query.with_entities(
  402. Ioc.ioc_value,
  403. Ioc.ioc_type,
  404. Ioc.ioc_description
  405. ).filter(
  406. IocAssetLink.asset_id == row['asset_id']
  407. ).join(
  408. IocAssetLink.ioc
  409. ).all()
  410. if ial:
  411. row['asset_ioc'] = [row._asdict() for row in ial]
  412. else:
  413. row['asset_ioc'] = []
  414. ret.append(row)
  415. return ret
  416. @staticmethod
  417. def get_docid():
  418. return "{}".format(
  419. datetime.utcnow().strftime("%y%m%d_%H%M"))
  420. @staticmethod
  421. def markdown_to_text(markdown_string):
  422. """
  423. Converts a markdown string to plaintext
  424. """
  425. return markdown_string.replace('\n', '</w:t></w:r><w:r/></w:p><w:p><w:r><w:t xml:space="preserve">').replace(
  426. '#', '')
  427. class IrisMakeMdReport(IrisReportMaker):
  428. """
  429. Generates a MD report for the case
  430. """
  431. def __init__(self, tmp_dir, report_id, caseid, safe_mode=False):
  432. self._tmp = tmp_dir
  433. self._report_id = report_id
  434. self._case_info = {}
  435. self._caseid = caseid
  436. self.safe_mode = safe_mode
  437. def generate_md_report(self, doc_type):
  438. """
  439. Generate report file
  440. """
  441. case_info = self.get_case_info(doc_type)
  442. if case_info is None:
  443. return None
  444. # Get file extension
  445. report = CaseTemplateReport.query.filter(
  446. CaseTemplateReport.id == self._report_id).first()
  447. _, report_format = os.path.splitext(report.internal_reference)
  448. case_info['case']['for_customer'] = f"{case_info['case'].get('client').get('customer_name')} (legacy::use client.customer_name)"
  449. # Prepare report name
  450. name = "{}".format(("{}" + str(report_format)).format(report.naming_format))
  451. name = name.replace("%code_name%", case_info['doc_id'])
  452. name = name.replace(
  453. '%customer%', case_info['case'].get('client').get('customer_name'))
  454. name = name.replace('%case_name%', case_info['case'].get('name'))
  455. name = name.replace('%date%', datetime.utcnow().strftime("%Y-%m-%d"))
  456. # Build output file
  457. output_file_path = os.path.join(self._tmp, name)
  458. try:
  459. env = IrisJinjaEnv()
  460. env.filters = app.jinja_env.filters
  461. template_path = os.path.join(app.config['TEMPLATES_PATH'], report.internal_reference)
  462. with open(template_path, 'r', encoding="utf-8") as template_file:
  463. template = env.from_string(template_file.read())
  464. output_text = template.render(case_info)
  465. with open(output_file_path, 'w', encoding="utf-8") as html_file:
  466. html_file.write(output_text)
  467. except Exception as e:
  468. log.exception("Error while generating report: {}".format(e))
  469. return None, e.__str__()
  470. return output_file_path, 'Report generated'
  471. class QueuingHandler(log.Handler):
  472. """A thread safe logging.Handler that writes messages into a queue object.
  473. Designed to work with LoggingWidget so log messages from multiple
  474. threads can be shown together in a single ttk.Frame.
  475. The standard logging.QueueHandler/logging.QueueListener can not be used
  476. for this because the QueueListener runs in a private thread, not the
  477. main thread.
  478. Warning: If multiple threads are writing into this Handler, all threads
  479. must be joined before calling logging.shutdown() or any other log
  480. destinations will be corrupted.
  481. """
  482. def __init__(self, *args, task_self, message_queue, **kwargs):
  483. """Initialize by copying the queue and sending everything else to superclass."""
  484. log.Handler.__init__(self, *args, **kwargs)
  485. self.message_queue = message_queue
  486. self.task_self = task_self
  487. def emit(self, record):
  488. """Add the formatted log message (sans newlines) to the queue."""
  489. self.message_queue.append(self.format(record).rstrip('\n'))
  490. self.task_self.update_state(state='PROGRESS',
  491. meta=list(self.message_queue))