| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387 |
- # IRIS Source Code
- # Copyright (C) 2024 - DFIR-IRIS
- # contact@dfir-iris.org
- #
- # This program is free software; you can redistribute it and/or
- # modify it under the terms of the GNU Lesser General Public
- # License as published by the Free Software Foundation; either
- # version 3 of the License, or (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- # Lesser General Public License for more details.
- #
- # You should have received a copy of the GNU Lesser General Public License
- # along with this program; if not, write to the Free Software Foundation,
- # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- import base64
- import datetime
- import json
- import marshmallow.exceptions
- import urllib.parse
- from flask import Blueprint
- from flask import request
- from flask import send_file
- from flask_login import current_user
- from pathlib import Path
- import app
- from app import db
- from app.datamgmt.datastore.datastore_db import datastore_add_child_node
- from app.datamgmt.datastore.datastore_db import datastore_add_file_as_evidence
- from app.datamgmt.datastore.datastore_db import datastore_add_file_as_ioc
- from app.datamgmt.datastore.datastore_db import datastore_delete_file
- from app.datamgmt.datastore.datastore_db import datastore_delete_node
- from app.datamgmt.datastore.datastore_db import datastore_filter_tree
- from app.datamgmt.datastore.datastore_db import datastore_get_file
- from app.datamgmt.datastore.datastore_db import datastore_get_interactive_path_node
- from app.datamgmt.datastore.datastore_db import datastore_get_local_file_path
- from app.datamgmt.datastore.datastore_db import datastore_get_path_node
- from app.datamgmt.datastore.datastore_db import datastore_get_standard_path
- from app.datamgmt.datastore.datastore_db import datastore_rename_node
- from app.datamgmt.datastore.datastore_db import ds_list_tree
- from app.iris_engine.utils.tracker import track_activity
- from app.models.authorization import CaseAccessLevel
- from app.schema.marshables import DSFileSchema
- from app.schema.marshables import DSPathSchema
- from app.blueprints.access_controls import ac_requires_case_identifier
- from app.blueprints.access_controls import ac_api_requires
- from app.util import add_obj_history_entry
- from app.blueprints.responses import response_error
- from app.blueprints.responses import response_success
- datastore_rest_blueprint = Blueprint('datastore_rest', __name__)
- logger = app.logger
- @datastore_rest_blueprint.route('/datastore/list/tree', methods=['GET'])
- @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
- @ac_api_requires()
- def datastore_list_tree(caseid):
- data = ds_list_tree(caseid)
- return response_success("", data=data)
- @datastore_rest_blueprint.route('/datastore/list/filter', methods=['GET'])
- @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
- @ac_api_requires()
- def datastore_list_filter(caseid):
- args = request.args.to_dict()
- query_filter = args.get('q')
- try:
- filter_d = dict(json.loads(urllib.parse.unquote_plus(query_filter)))
- except Exception as e:
- logger.error('Error parsing filter: {}'.format(query_filter))
- logger.error(e)
- return response_error('Invalid query')
- data, log = datastore_filter_tree(filter_d, caseid)
- if data is None:
- logger.error('Error parsing filter: {}'.format(query_filter))
- logger.error(log)
- return response_error('Invalid query')
- return response_success("", data=data)
- @datastore_rest_blueprint.route('/datastore/file/info/<int:cur_id>', methods=['GET'])
- @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
- @ac_api_requires()
- def datastore_info_file(cur_id: int, caseid: int):
- file = datastore_get_file(cur_id, caseid)
- if not file:
- return response_error('Invalid file ID for this case')
- file_schema = DSFileSchema()
- file = file_schema.dump(file)
- del file['file_local_name']
- return response_success("", data=file)
- @datastore_rest_blueprint.route('/datastore/file/update/<int:cur_id>', methods=['POST'])
- @ac_requires_case_identifier(CaseAccessLevel.full_access)
- @ac_api_requires()
- def datastore_update_file(cur_id: int, caseid: int):
- dsf = datastore_get_file(cur_id, caseid)
- if not dsf:
- return response_error('Invalid file ID for this case')
- dsf_schema = DSFileSchema()
- try:
- dsf_sc = dsf_schema.load(request.form, instance=dsf, partial=True)
- add_obj_history_entry(dsf_sc, 'updated')
- dsf.file_is_ioc = request.form.get('file_is_ioc') is not None or request.form.get('file_is_ioc') is True
- dsf.file_is_evidence = request.form.get('file_is_evidence') is not None or request.form.get('file_is_evidence') is True
- db.session.commit()
- if request.files.get('file_content'):
- ds_location = datastore_get_standard_path(dsf_sc, caseid)
- dsf_sc.file_local_name, dsf_sc.file_size, dsf_sc.file_sha256 = dsf_schema.ds_store_file(
- request.files.get('file_content'),
- ds_location,
- dsf_sc.file_is_ioc,
- dsf_sc.file_password)
- if dsf_sc.file_is_ioc and not dsf_sc.file_password:
- dsf_sc.file_password = 'infected'
- db.session.commit()
- msg_added_as = ''
- if dsf.file_is_ioc:
- datastore_add_file_as_ioc(dsf, caseid)
- msg_added_as += 'and added in IOC'
- if dsf.file_is_evidence:
- datastore_add_file_as_evidence(dsf, caseid)
- msg_added_as += ' and evidence' if len(msg_added_as) > 0 else 'and added in evidence'
- track_activity(f'File \"{dsf.file_original_name}\" updated in DS', caseid=caseid)
- return response_success('File updated in datastore', data=dsf_schema.dump(dsf_sc))
- except marshmallow.exceptions.ValidationError as e:
- return response_error(msg="Data error", data=e.messages)
- @datastore_rest_blueprint.route('/datastore/file/move/<int:cur_id>', methods=['POST'])
- @ac_requires_case_identifier(CaseAccessLevel.full_access)
- @ac_api_requires()
- def datastore_move_file(cur_id: int, caseid: int):
- if not request.json:
- return response_error("Invalid data")
- dsf = datastore_get_file(cur_id, caseid)
- if not dsf:
- return response_error('Invalid file ID for this case')
- dsp = datastore_get_path_node(request.json.get('destination-node'), caseid)
- if not dsp:
- return response_error('Invalid destination node ID for this case')
- dsf.file_parent_id = dsp.path_id
- db.session.commit()
- track_activity(f'File \"{dsf.file_original_name}\" moved to \"{dsp.path_name}\" in DS', caseid=caseid)
- return response_success(f"File successfully moved to {dsp.path_name}")
- @datastore_rest_blueprint.route('/datastore/folder/move/<int:cur_id>', methods=['POST'])
- @ac_requires_case_identifier(CaseAccessLevel.full_access)
- @ac_api_requires()
- def datastore_move_folder(cur_id: int, caseid: int):
- if not request.json:
- return response_error("Invalid data")
- dsp = datastore_get_path_node(cur_id, caseid)
- if not dsp:
- return response_error('Invalid file ID for this case')
- dsp_dst = datastore_get_path_node(request.json.get('destination-node'), caseid)
- if not dsp_dst:
- return response_error('Invalid destination node ID for this case')
- if dsp.path_id == dsp_dst.path_id:
- return response_error("If that's true, then I've made a mistake, and you should kill me now.")
- dsp.path_parent_id = dsp_dst.path_id
- db.session.commit()
- dsf_folder_schema = DSPathSchema()
- msg = f"Folder \"{dsp.path_name}\" successfully moved to \"{dsp_dst.path_name}\""
- track_activity(msg, caseid=caseid)
- return response_success(msg, data=dsf_folder_schema.dump(dsp))
- @datastore_rest_blueprint.route('/datastore/file/view/<int:cur_id>', methods=['GET'])
- @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
- @ac_api_requires()
- def datastore_view_file(cur_id: int, caseid: int):
- has_error, dsf = datastore_get_local_file_path(cur_id, caseid)
- if has_error:
- return response_error('Unable to get requested file ID', data=dsf)
- if dsf.file_is_ioc or dsf.file_password:
- destination_name = dsf.file_original_name + ".zip"
- else:
- destination_name = dsf.file_original_name
- if not Path(dsf.file_local_name).is_file():
- return response_error(f'File {dsf.file_local_name} does not exists on the server. '
- f'Update or delete virtual entry')
- resp = send_file(dsf.file_local_name, as_attachment=False,
- download_name=destination_name)
- track_activity(f"File \"{destination_name}\" downloaded", caseid=caseid, display_in_ui=False)
- return resp
- @datastore_rest_blueprint.route('/datastore/file/add/<int:cur_id>', methods=['POST'])
- @ac_requires_case_identifier(CaseAccessLevel.full_access)
- @ac_api_requires()
- def datastore_add_file(cur_id: int, caseid: int):
- dsp = datastore_get_path_node(cur_id, caseid)
- if not dsp:
- return response_error('Invalid path node for this case')
- dsf_schema = DSFileSchema()
- try:
- dsf_sc = dsf_schema.load(request.form, partial=True)
- dsf_sc.file_parent_id = dsp.path_id
- dsf_sc.added_by_user_id = current_user.id
- dsf_sc.file_date_added = datetime.datetime.now()
- dsf_sc.file_local_name = 'tmp_xc'
- dsf_sc.file_case_id = caseid
- add_obj_history_entry(dsf_sc, 'created')
- if dsf_sc.file_is_ioc and not dsf_sc.file_password:
- dsf_sc.file_password = 'infected'
- db.session.add(dsf_sc)
- db.session.commit()
- ds_location = datastore_get_standard_path(dsf_sc, caseid)
- dsf_sc.file_local_name, dsf_sc.file_size, dsf_sc.file_sha256 = dsf_schema.ds_store_file(
- request.files.get('file_content'),
- ds_location,
- dsf_sc.file_is_ioc,
- dsf_sc.file_password)
- db.session.commit()
- msg_added_as = ''
- if dsf_sc.file_is_ioc:
- datastore_add_file_as_ioc(dsf_sc, caseid)
- msg_added_as += 'and added in IOC'
- if dsf_sc.file_is_evidence:
- datastore_add_file_as_evidence(dsf_sc, caseid)
- msg_added_as += ' and evidence' if len(msg_added_as) > 0 else 'and added in evidence'
- track_activity(f"File \"{dsf_sc.file_original_name}\" added to DS", caseid=caseid)
- return response_success(f'File saved in datastore {msg_added_as}', data=dsf_schema.dump(dsf_sc))
- except marshmallow.exceptions.ValidationError as e:
- return response_error(msg="Data error", data=e.messages)
- @datastore_rest_blueprint.route('/datastore/file/add-interactive', methods=['POST'])
- @ac_requires_case_identifier(CaseAccessLevel.full_access)
- @ac_api_requires()
- def datastore_add_interactive_file(caseid: int):
- dsp = datastore_get_interactive_path_node(caseid)
- if not dsp:
- return response_error('Invalid path node for this case')
- dsf_schema = DSFileSchema()
- try:
- js_data = request.json
- try:
- file_content = base64.b64decode(js_data.get('file_content'))
- filename = js_data.get('file_original_name')
- except Exception as e:
- return response_error(msg=str(e))
- dsf_sc, existed = dsf_schema.ds_store_file_b64(filename, file_content, dsp, caseid)
- if not existed:
- msg = "File saved in datastore"
- else:
- msg = "File already existing in datastore. Using it."
- track_activity(f"File \"{dsf_sc.file_original_name}\" added to DS", caseid=caseid)
- return response_success(msg, data={"file_url": f"/datastore/file/view/{dsf_sc.file_id}"})
- except marshmallow.exceptions.ValidationError as e:
- return response_error(msg="Data error", data=e.messages)
- @datastore_rest_blueprint.route('/datastore/folder/add', methods=['POST'])
- @ac_requires_case_identifier(CaseAccessLevel.full_access)
- @ac_api_requires()
- def datastore_add_folder(caseid: int):
- data = request.json
- if not data:
- return response_error('Invalid data')
- parent_node = data.get('parent_node')
- folder_name = data.get('folder_name')
- if not parent_node or not folder_name:
- return response_error('Invalid data')
- has_error, logs, node = datastore_add_child_node(parent_node, folder_name, caseid)
- dsf_folder_schema = DSPathSchema()
- if not has_error:
- track_activity(f"Folder \"{folder_name}\" added to DS", caseid=caseid)
- return response_success(msg=logs, data=dsf_folder_schema.dump(node))
- return response_error(msg=logs)
- @datastore_rest_blueprint.route('/datastore/folder/rename/<int:cur_id>', methods=['POST'])
- @ac_requires_case_identifier(CaseAccessLevel.full_access)
- @ac_api_requires()
- def datastore_rename_folder(cur_id: int, caseid: int):
- data = request.json
- if not data:
- return response_error('Invalid data')
- parent_node = data.get('parent_node')
- folder_name = data.get('folder_name')
- if not parent_node or not folder_name:
- return response_error('Invalid data')
- if int(parent_node) != cur_id:
- return response_error('Invalid data')
- has_error, logs, dsp_base = datastore_rename_node(parent_node, folder_name, caseid)
- dsf_folder_schema = DSPathSchema()
- if has_error:
- return response_error(logs)
- track_activity(f"Folder \"{parent_node}\" renamed to \"{folder_name}\" in DS", caseid=caseid)
- return response_success(logs, data=dsf_folder_schema.dump(dsp_base))
- @datastore_rest_blueprint.route('/datastore/folder/delete/<int:cur_id>', methods=['POST'])
- @ac_requires_case_identifier(CaseAccessLevel.full_access)
- @ac_api_requires()
- def datastore_delete_folder_route(cur_id: int, caseid: int):
- has_error, logs = datastore_delete_node(cur_id, caseid)
- if has_error:
- return response_error(logs)
- track_activity(f"Folder \"{cur_id}\" deleted from DS", caseid=caseid)
- return response_success(logs)
- @datastore_rest_blueprint.route('/datastore/file/delete/<int:cur_id>', methods=['POST'])
- @ac_requires_case_identifier(CaseAccessLevel.full_access)
- @ac_api_requires()
- def datastore_delete_file_route(cur_id: int, caseid: int):
- has_error, logs = datastore_delete_file(cur_id, caseid)
- if has_error:
- return response_error(logs)
- track_activity(f"File \"{cur_id}\" deleted from DS", caseid=caseid)
- return response_success(logs)
|