Няма описание

datastore_routes.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  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 base64
  19. import datetime
  20. import json
  21. import marshmallow.exceptions
  22. import urllib.parse
  23. from flask import Blueprint
  24. from flask import request
  25. from flask import send_file
  26. from flask_login import current_user
  27. from pathlib import Path
  28. import app
  29. from app import db
  30. from app.datamgmt.datastore.datastore_db import datastore_add_child_node
  31. from app.datamgmt.datastore.datastore_db import datastore_add_file_as_evidence
  32. from app.datamgmt.datastore.datastore_db import datastore_add_file_as_ioc
  33. from app.datamgmt.datastore.datastore_db import datastore_delete_file
  34. from app.datamgmt.datastore.datastore_db import datastore_delete_node
  35. from app.datamgmt.datastore.datastore_db import datastore_filter_tree
  36. from app.datamgmt.datastore.datastore_db import datastore_get_file
  37. from app.datamgmt.datastore.datastore_db import datastore_get_interactive_path_node
  38. from app.datamgmt.datastore.datastore_db import datastore_get_local_file_path
  39. from app.datamgmt.datastore.datastore_db import datastore_get_path_node
  40. from app.datamgmt.datastore.datastore_db import datastore_get_standard_path
  41. from app.datamgmt.datastore.datastore_db import datastore_rename_node
  42. from app.datamgmt.datastore.datastore_db import ds_list_tree
  43. from app.iris_engine.utils.tracker import track_activity
  44. from app.models.authorization import CaseAccessLevel
  45. from app.schema.marshables import DSFileSchema
  46. from app.schema.marshables import DSPathSchema
  47. from app.blueprints.access_controls import ac_requires_case_identifier
  48. from app.blueprints.access_controls import ac_api_requires
  49. from app.util import add_obj_history_entry
  50. from app.blueprints.responses import response_error
  51. from app.blueprints.responses import response_success
  52. datastore_rest_blueprint = Blueprint('datastore_rest', __name__)
  53. logger = app.logger
  54. @datastore_rest_blueprint.route('/datastore/list/tree', methods=['GET'])
  55. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  56. @ac_api_requires()
  57. def datastore_list_tree(caseid):
  58. data = ds_list_tree(caseid)
  59. return response_success("", data=data)
  60. @datastore_rest_blueprint.route('/datastore/list/filter', methods=['GET'])
  61. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  62. @ac_api_requires()
  63. def datastore_list_filter(caseid):
  64. args = request.args.to_dict()
  65. query_filter = args.get('q')
  66. try:
  67. filter_d = dict(json.loads(urllib.parse.unquote_plus(query_filter)))
  68. except Exception as e:
  69. logger.error('Error parsing filter: {}'.format(query_filter))
  70. logger.error(e)
  71. return response_error('Invalid query')
  72. data, log = datastore_filter_tree(filter_d, caseid)
  73. if data is None:
  74. logger.error('Error parsing filter: {}'.format(query_filter))
  75. logger.error(log)
  76. return response_error('Invalid query')
  77. return response_success("", data=data)
  78. @datastore_rest_blueprint.route('/datastore/file/info/<int:cur_id>', methods=['GET'])
  79. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  80. @ac_api_requires()
  81. def datastore_info_file(cur_id: int, caseid: int):
  82. file = datastore_get_file(cur_id, caseid)
  83. if not file:
  84. return response_error('Invalid file ID for this case')
  85. file_schema = DSFileSchema()
  86. file = file_schema.dump(file)
  87. del file['file_local_name']
  88. return response_success("", data=file)
  89. @datastore_rest_blueprint.route('/datastore/file/update/<int:cur_id>', methods=['POST'])
  90. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  91. @ac_api_requires()
  92. def datastore_update_file(cur_id: int, caseid: int):
  93. dsf = datastore_get_file(cur_id, caseid)
  94. if not dsf:
  95. return response_error('Invalid file ID for this case')
  96. dsf_schema = DSFileSchema()
  97. try:
  98. dsf_sc = dsf_schema.load(request.form, instance=dsf, partial=True)
  99. add_obj_history_entry(dsf_sc, 'updated')
  100. dsf.file_is_ioc = request.form.get('file_is_ioc') is not None or request.form.get('file_is_ioc') is True
  101. dsf.file_is_evidence = request.form.get('file_is_evidence') is not None or request.form.get('file_is_evidence') is True
  102. db.session.commit()
  103. if request.files.get('file_content'):
  104. ds_location = datastore_get_standard_path(dsf_sc, caseid)
  105. dsf_sc.file_local_name, dsf_sc.file_size, dsf_sc.file_sha256 = dsf_schema.ds_store_file(
  106. request.files.get('file_content'),
  107. ds_location,
  108. dsf_sc.file_is_ioc,
  109. dsf_sc.file_password)
  110. if dsf_sc.file_is_ioc and not dsf_sc.file_password:
  111. dsf_sc.file_password = 'infected'
  112. db.session.commit()
  113. msg_added_as = ''
  114. if dsf.file_is_ioc:
  115. datastore_add_file_as_ioc(dsf, caseid)
  116. msg_added_as += 'and added in IOC'
  117. if dsf.file_is_evidence:
  118. datastore_add_file_as_evidence(dsf, caseid)
  119. msg_added_as += ' and evidence' if len(msg_added_as) > 0 else 'and added in evidence'
  120. track_activity(f'File \"{dsf.file_original_name}\" updated in DS', caseid=caseid)
  121. return response_success('File updated in datastore', data=dsf_schema.dump(dsf_sc))
  122. except marshmallow.exceptions.ValidationError as e:
  123. return response_error(msg="Data error", data=e.messages)
  124. @datastore_rest_blueprint.route('/datastore/file/move/<int:cur_id>', methods=['POST'])
  125. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  126. @ac_api_requires()
  127. def datastore_move_file(cur_id: int, caseid: int):
  128. if not request.json:
  129. return response_error("Invalid data")
  130. dsf = datastore_get_file(cur_id, caseid)
  131. if not dsf:
  132. return response_error('Invalid file ID for this case')
  133. dsp = datastore_get_path_node(request.json.get('destination-node'), caseid)
  134. if not dsp:
  135. return response_error('Invalid destination node ID for this case')
  136. dsf.file_parent_id = dsp.path_id
  137. db.session.commit()
  138. track_activity(f'File \"{dsf.file_original_name}\" moved to \"{dsp.path_name}\" in DS', caseid=caseid)
  139. return response_success(f"File successfully moved to {dsp.path_name}")
  140. @datastore_rest_blueprint.route('/datastore/folder/move/<int:cur_id>', methods=['POST'])
  141. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  142. @ac_api_requires()
  143. def datastore_move_folder(cur_id: int, caseid: int):
  144. if not request.json:
  145. return response_error("Invalid data")
  146. dsp = datastore_get_path_node(cur_id, caseid)
  147. if not dsp:
  148. return response_error('Invalid file ID for this case')
  149. dsp_dst = datastore_get_path_node(request.json.get('destination-node'), caseid)
  150. if not dsp_dst:
  151. return response_error('Invalid destination node ID for this case')
  152. if dsp.path_id == dsp_dst.path_id:
  153. return response_error("If that's true, then I've made a mistake, and you should kill me now.")
  154. dsp.path_parent_id = dsp_dst.path_id
  155. db.session.commit()
  156. dsf_folder_schema = DSPathSchema()
  157. msg = f"Folder \"{dsp.path_name}\" successfully moved to \"{dsp_dst.path_name}\""
  158. track_activity(msg, caseid=caseid)
  159. return response_success(msg, data=dsf_folder_schema.dump(dsp))
  160. @datastore_rest_blueprint.route('/datastore/file/view/<int:cur_id>', methods=['GET'])
  161. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  162. @ac_api_requires()
  163. def datastore_view_file(cur_id: int, caseid: int):
  164. has_error, dsf = datastore_get_local_file_path(cur_id, caseid)
  165. if has_error:
  166. return response_error('Unable to get requested file ID', data=dsf)
  167. if dsf.file_is_ioc or dsf.file_password:
  168. destination_name = dsf.file_original_name + ".zip"
  169. else:
  170. destination_name = dsf.file_original_name
  171. if not Path(dsf.file_local_name).is_file():
  172. return response_error(f'File {dsf.file_local_name} does not exists on the server. '
  173. f'Update or delete virtual entry')
  174. resp = send_file(dsf.file_local_name, as_attachment=False,
  175. download_name=destination_name)
  176. track_activity(f"File \"{destination_name}\" downloaded", caseid=caseid, display_in_ui=False)
  177. return resp
  178. @datastore_rest_blueprint.route('/datastore/file/add/<int:cur_id>', methods=['POST'])
  179. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  180. @ac_api_requires()
  181. def datastore_add_file(cur_id: int, caseid: int):
  182. dsp = datastore_get_path_node(cur_id, caseid)
  183. if not dsp:
  184. return response_error('Invalid path node for this case')
  185. dsf_schema = DSFileSchema()
  186. try:
  187. dsf_sc = dsf_schema.load(request.form, partial=True)
  188. dsf_sc.file_parent_id = dsp.path_id
  189. dsf_sc.added_by_user_id = current_user.id
  190. dsf_sc.file_date_added = datetime.datetime.now()
  191. dsf_sc.file_local_name = 'tmp_xc'
  192. dsf_sc.file_case_id = caseid
  193. add_obj_history_entry(dsf_sc, 'created')
  194. if dsf_sc.file_is_ioc and not dsf_sc.file_password:
  195. dsf_sc.file_password = 'infected'
  196. db.session.add(dsf_sc)
  197. db.session.commit()
  198. ds_location = datastore_get_standard_path(dsf_sc, caseid)
  199. dsf_sc.file_local_name, dsf_sc.file_size, dsf_sc.file_sha256 = dsf_schema.ds_store_file(
  200. request.files.get('file_content'),
  201. ds_location,
  202. dsf_sc.file_is_ioc,
  203. dsf_sc.file_password)
  204. db.session.commit()
  205. msg_added_as = ''
  206. if dsf_sc.file_is_ioc:
  207. datastore_add_file_as_ioc(dsf_sc, caseid)
  208. msg_added_as += 'and added in IOC'
  209. if dsf_sc.file_is_evidence:
  210. datastore_add_file_as_evidence(dsf_sc, caseid)
  211. msg_added_as += ' and evidence' if len(msg_added_as) > 0 else 'and added in evidence'
  212. track_activity(f"File \"{dsf_sc.file_original_name}\" added to DS", caseid=caseid)
  213. return response_success(f'File saved in datastore {msg_added_as}', data=dsf_schema.dump(dsf_sc))
  214. except marshmallow.exceptions.ValidationError as e:
  215. return response_error(msg="Data error", data=e.messages)
  216. @datastore_rest_blueprint.route('/datastore/file/add-interactive', methods=['POST'])
  217. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  218. @ac_api_requires()
  219. def datastore_add_interactive_file(caseid: int):
  220. dsp = datastore_get_interactive_path_node(caseid)
  221. if not dsp:
  222. return response_error('Invalid path node for this case')
  223. dsf_schema = DSFileSchema()
  224. try:
  225. js_data = request.json
  226. try:
  227. file_content = base64.b64decode(js_data.get('file_content'))
  228. filename = js_data.get('file_original_name')
  229. except Exception as e:
  230. return response_error(msg=str(e))
  231. dsf_sc, existed = dsf_schema.ds_store_file_b64(filename, file_content, dsp, caseid)
  232. if not existed:
  233. msg = "File saved in datastore"
  234. else:
  235. msg = "File already existing in datastore. Using it."
  236. track_activity(f"File \"{dsf_sc.file_original_name}\" added to DS", caseid=caseid)
  237. return response_success(msg, data={"file_url": f"/datastore/file/view/{dsf_sc.file_id}"})
  238. except marshmallow.exceptions.ValidationError as e:
  239. return response_error(msg="Data error", data=e.messages)
  240. @datastore_rest_blueprint.route('/datastore/folder/add', methods=['POST'])
  241. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  242. @ac_api_requires()
  243. def datastore_add_folder(caseid: int):
  244. data = request.json
  245. if not data:
  246. return response_error('Invalid data')
  247. parent_node = data.get('parent_node')
  248. folder_name = data.get('folder_name')
  249. if not parent_node or not folder_name:
  250. return response_error('Invalid data')
  251. has_error, logs, node = datastore_add_child_node(parent_node, folder_name, caseid)
  252. dsf_folder_schema = DSPathSchema()
  253. if not has_error:
  254. track_activity(f"Folder \"{folder_name}\" added to DS", caseid=caseid)
  255. return response_success(msg=logs, data=dsf_folder_schema.dump(node))
  256. return response_error(msg=logs)
  257. @datastore_rest_blueprint.route('/datastore/folder/rename/<int:cur_id>', methods=['POST'])
  258. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  259. @ac_api_requires()
  260. def datastore_rename_folder(cur_id: int, caseid: int):
  261. data = request.json
  262. if not data:
  263. return response_error('Invalid data')
  264. parent_node = data.get('parent_node')
  265. folder_name = data.get('folder_name')
  266. if not parent_node or not folder_name:
  267. return response_error('Invalid data')
  268. if int(parent_node) != cur_id:
  269. return response_error('Invalid data')
  270. has_error, logs, dsp_base = datastore_rename_node(parent_node, folder_name, caseid)
  271. dsf_folder_schema = DSPathSchema()
  272. if has_error:
  273. return response_error(logs)
  274. track_activity(f"Folder \"{parent_node}\" renamed to \"{folder_name}\" in DS", caseid=caseid)
  275. return response_success(logs, data=dsf_folder_schema.dump(dsp_base))
  276. @datastore_rest_blueprint.route('/datastore/folder/delete/<int:cur_id>', methods=['POST'])
  277. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  278. @ac_api_requires()
  279. def datastore_delete_folder_route(cur_id: int, caseid: int):
  280. has_error, logs = datastore_delete_node(cur_id, caseid)
  281. if has_error:
  282. return response_error(logs)
  283. track_activity(f"Folder \"{cur_id}\" deleted from DS", caseid=caseid)
  284. return response_success(logs)
  285. @datastore_rest_blueprint.route('/datastore/file/delete/<int:cur_id>', methods=['POST'])
  286. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  287. @ac_api_requires()
  288. def datastore_delete_file_route(cur_id: int, caseid: int):
  289. has_error, logs = datastore_delete_file(cur_id, caseid)
  290. if has_error:
  291. return response_error(logs)
  292. track_activity(f"File \"{cur_id}\" deleted from DS", caseid=caseid)
  293. return response_success(logs)