# 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/', 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/', 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/', 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/', 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/', 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/', 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/', 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/', 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/', 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)