#!/usr/bin/env python3 # # IRIS Source Code # 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. from typing import Union import logging as log # IMPORTS ------------------------------------------------ import os import traceback import urllib.parse import marshmallow from flask import Blueprint from flask import redirect from flask import render_template from flask import request from flask import url_for from flask_login import current_user from flask_wtf import FlaskForm from werkzeug import Response import app from app import db from app.datamgmt.alerts.alerts_db import get_alert_status_by_name from app.datamgmt.case.case_db import get_case, get_review_id_from_name from app.datamgmt.case.case_db import register_case_protagonists from app.datamgmt.case.case_db import save_case_tags from app.datamgmt.client.client_db import get_client_list from app.datamgmt.iris_engine.modules_db import get_pipelines_args_from_name from app.datamgmt.iris_engine.modules_db import iris_module_exists from app.datamgmt.manage.manage_attribute_db import get_default_custom_attributes from app.datamgmt.manage.manage_case_classifications_db import get_case_classifications_list from app.datamgmt.manage.manage_case_state_db import get_case_states_list, get_case_state_by_name from app.datamgmt.manage.manage_case_templates_db import get_case_templates_list, case_template_pre_modifier, \ case_template_post_modifier from app.datamgmt.manage.manage_cases_db import close_case, map_alert_resolution_to_case_status from app.datamgmt.manage.manage_cases_db import delete_case from app.datamgmt.manage.manage_cases_db import get_case_details_rt from app.datamgmt.manage.manage_cases_db import get_case_protagonists from app.datamgmt.manage.manage_cases_db import list_cases_dict from app.datamgmt.manage.manage_cases_db import reopen_case from app.datamgmt.manage.manage_users_db import get_user_organisations from app.forms import AddCaseForm from app.iris_engine.access_control.utils import ac_fast_check_current_user_has_case_access from app.iris_engine.access_control.utils import ac_fast_check_user_has_case_access from app.iris_engine.access_control.utils import ac_set_new_case_access from app.iris_engine.module_handler.module_handler import call_modules_hook from app.iris_engine.module_handler.module_handler import configure_module_on_init from app.iris_engine.module_handler.module_handler import instantiate_module_from_name from app.iris_engine.tasker.tasks import task_case_update from app.iris_engine.utils.common import build_upload_path from app.iris_engine.utils.tracker import track_activity from app.models.alerts import AlertStatus from app.models.authorization import CaseAccessLevel from app.models.authorization import Permissions from app.models.models import Client, ReviewStatusList from app.schema.marshables import CaseSchema from app.util import ac_api_case_requires, add_obj_history_entry from app.util import ac_api_requires from app.util import ac_api_return_access_denied from app.util import ac_case_requires from app.util import ac_requires from app.util import response_error from app.util import response_success manage_cases_blueprint = Blueprint('manage_case', __name__, template_folder='templates') # CONTENT ------------------------------------------------ @manage_cases_blueprint.route('/manage/cases', methods=['GET']) @ac_requires(Permissions.standard_user, no_cid_required=True) def manage_index_cases(caseid, url_redir): if url_redir: return redirect(url_for('manage_case.manage_index_cases', cid=caseid)) form = AddCaseForm() # Fill select form field customer with the available customers in DB form.case_customer.choices = [(c.client_id, c.name) for c in Client.query.order_by(Client.name)] form.case_organisations.choices = [(org['org_id'], org['org_name']) for org in get_user_organisations(current_user.id)] form.classification_id.choices = [(clc['id'], clc['name_expanded']) for clc in get_case_classifications_list()] form.case_template_id.choices = [(ctp['id'], ctp['display_name']) for ctp in get_case_templates_list()] attributes = get_default_custom_attributes('case') return render_template('manage_cases.html', form=form, attributes=attributes) @manage_cases_blueprint.route('/manage/cases/details/', methods=['GET']) @ac_requires(no_cid_required=True) def details_case(cur_id: int, caseid: int, url_redir: bool) -> Union[Response, str]: """ Get case details Args: cur_id (int): case id caseid (int): case id url_redir (bool): url redirection Returns: Union[str, Response]: The case details """ if url_redir: return response_error("Invalid request") if not ac_fast_check_user_has_case_access(current_user.id, cur_id, [CaseAccessLevel.read_only, CaseAccessLevel.full_access]): return ac_api_return_access_denied(caseid=cur_id) res = get_case_details_rt(cur_id) case_classifications = get_case_classifications_list() case_states = get_case_states_list() customers = get_client_list() form = FlaskForm() if res: return render_template("modal_case_info_from_case.html", data=res, form=form, protagnists=None, case_classifications=case_classifications, case_states=case_states, customers=customers) else: return response_error("Unknown case") @manage_cases_blueprint.route('/case/details/', methods=['GET']) @ac_requires(no_cid_required=True) def details_case_from_case_modal(cur_id: int, caseid: int, url_redir: bool) -> Union[str, Response]: """ Returns the case details modal for a case from a case Args: cur_id (int): The case id caseid (int): The case id url_redir (bool): If the request is a url redirect Returns: Union[str, Response]: The case details modal """ if url_redir: return response_error("Invalid request") if not ac_fast_check_current_user_has_case_access(cur_id, [CaseAccessLevel.read_only, CaseAccessLevel.full_access]): return ac_api_return_access_denied(caseid=cur_id) res = get_case_details_rt(cur_id) case_classifications = get_case_classifications_list() case_states = get_case_states_list() customers = get_client_list() protagonists = get_case_protagonists(cur_id) form = FlaskForm() if res: return render_template("modal_case_info_from_case.html", data=res, form=form, protagonists=protagonists, case_classifications=case_classifications, case_states=case_states, customers=customers) else: return response_error("Unknown case") @manage_cases_blueprint.route('/manage/cases/', methods=['GET']) @ac_api_requires(no_cid_required=True) def get_case_api(cur_id, caseid): if not ac_fast_check_current_user_has_case_access(cur_id, [CaseAccessLevel.read_only, CaseAccessLevel.full_access]): return ac_api_return_access_denied(caseid=cur_id) res = get_case_details_rt(cur_id) if res: return response_success(data=res) return response_error(f'Case ID {cur_id} not found') @manage_cases_blueprint.route('/manage/cases/delete/', methods=['POST']) @ac_api_requires(Permissions.standard_user, no_cid_required=True) def api_delete_case(cur_id, caseid): if not ac_fast_check_current_user_has_case_access(cur_id, [CaseAccessLevel.full_access]): return ac_api_return_access_denied(caseid=cur_id) if cur_id == 1: track_activity("tried to delete case {}, but case is the primary case".format(cur_id), caseid=caseid, ctx_less=True) return response_error("Cannot delete a primary case to keep consistency") else: try: call_modules_hook('on_preload_case_delete', data=cur_id, caseid=caseid) if delete_case(case_id=cur_id): call_modules_hook('on_postload_case_delete', data=cur_id, caseid=caseid) track_activity("case {} deleted successfully".format(cur_id), ctx_less=True) return response_success("Case successfully deleted") else: track_activity("tried to delete case {}, but it doesn't exist".format(cur_id), caseid=caseid, ctx_less=True) return response_error("Tried to delete a non-existing case") except Exception as e: app.app.logger.exception(e) return response_error("Cannot delete the case. Please check server logs for additional informations") @manage_cases_blueprint.route('/manage/cases/reopen/', methods=['POST']) @ac_api_requires(Permissions.standard_user, no_cid_required=True) def api_reopen_case(cur_id, caseid): if not ac_fast_check_current_user_has_case_access(cur_id, [CaseAccessLevel.full_access]): return ac_api_return_access_denied(caseid=cur_id) if not cur_id: return response_error("No case ID provided") case = get_case(cur_id) if not case: return response_error("Tried to reopen an non-existing case") res = reopen_case(cur_id) if not res: return response_error("Tried to reopen an non-existing case") # Reopen the related alerts if case.alerts: merged_status = get_alert_status_by_name('Merged') for alert in case.alerts: if alert.alert_status_id != merged_status.status_id: alert.alert_status_id = merged_status.status_id track_activity(f"alert ID {alert.alert_id} status updated to merged due to case #{caseid} being reopen", caseid=caseid, ctx_less=False) db.session.add(alert) case = call_modules_hook('on_postload_case_update', data=case, caseid=caseid) add_obj_history_entry(case, 'case reopen') track_activity("reopen case ID {}".format(cur_id), caseid=caseid) case_schema = CaseSchema() return response_success("Case reopen successfully", data=case_schema.dump(res)) @manage_cases_blueprint.route('/manage/cases/close/', methods=['POST']) @ac_api_requires(Permissions.standard_user, no_cid_required=True) def api_case_close(cur_id, caseid): if not ac_fast_check_current_user_has_case_access(cur_id, [CaseAccessLevel.full_access]): return ac_api_return_access_denied(caseid=cur_id) if not cur_id: return response_error("No case ID provided") case = get_case(cur_id) if not case: return response_error("Tried to close an non-existing case") res = close_case(cur_id) if not res: return response_error("Tried to close an non-existing case") # Close the related alerts if case.alerts: close_status = get_alert_status_by_name('Closed') case_status_id_mapped = map_alert_resolution_to_case_status(case.status_id) for alert in case.alerts: if alert.alert_status_id != close_status.status_id: alert.alert_status_id = close_status.status_id alert = call_modules_hook('on_postload_alert_update', data=alert, caseid=caseid) if alert.alert_resolution_status_id != case_status_id_mapped: alert.alert_resolution_status_id = case_status_id_mapped alert = call_modules_hook('on_postload_alert_resolution_update', data=alert, caseid=caseid) track_activity(f"closing alert ID {alert.alert_id} due to case #{caseid} being closed", caseid=caseid, ctx_less=False) db.session.add(alert) case = call_modules_hook('on_postload_case_update', data=case, caseid=caseid) add_obj_history_entry(case, 'case closed') track_activity("closed case ID {}".format(cur_id), caseid=caseid, ctx_less=False) case_schema = CaseSchema() return response_success("Case closed successfully", data=case_schema.dump(res)) @manage_cases_blueprint.route('/manage/cases/add', methods=['POST']) @ac_api_requires(Permissions.standard_user, no_cid_required=True) def api_add_case(caseid): case_schema = CaseSchema() try: request_data = call_modules_hook('on_preload_case_create', data=request.get_json(), caseid=caseid) case_template_id = request_data.pop("case_template_id", None) case = case_schema.load(request_data) case.owner_id = current_user.id if case_template_id and len(case_template_id) > 0: case = case_template_pre_modifier(case, case_template_id) if case is None: return response_error(msg=f"Invalid Case template ID {case_template_id}", status=400) case.state_id = get_case_state_by_name('Open').state_id case.save() if case_template_id and len(case_template_id) > 0: try: case, logs = case_template_post_modifier(case, case_template_id) if len(logs) > 0: return response_error(msg=f"Could not update new case with {case_template_id}", data=logs, status=400) except Exception as e: log.error(e.__str__()) return response_error(msg=f"Unexpected error when loading template {case_template_id} to new case.", status=400) ac_set_new_case_access(None, case.case_id) case = call_modules_hook('on_postload_case_create', data=case, caseid=caseid) add_obj_history_entry(case, 'created') track_activity("new case {case_name} created".format(case_name=case.name), caseid=case.case_id, ctx_less=False) except marshmallow.exceptions.ValidationError as e: return response_error(msg="Data error", data=e.messages, status=400) except Exception as e: log.error(e.__str__()) log.error(traceback.format_exc()) return response_error(msg="Error creating case - check server logs", status=400) return response_success(msg='Case created', data=case_schema.dump(case)) @manage_cases_blueprint.route('/manage/cases/list', methods=['GET']) @ac_api_requires(Permissions.standard_user, no_cid_required=True) def api_list_case(caseid): data = list_cases_dict(current_user.id) return response_success("", data=data) @manage_cases_blueprint.route('/manage/cases/update/', methods=['POST']) @ac_api_requires(Permissions.standard_user, no_cid_required=True) def update_case_info(cur_id, caseid): if not ac_fast_check_current_user_has_case_access(cur_id, [CaseAccessLevel.full_access]): return ac_api_return_access_denied(caseid=cur_id) case_schema = CaseSchema() case_i = get_case(cur_id) if not case_i: return response_error("Case not found") try: request_data = request.get_json() previous_case_state = case_i.state_id case_previous_reviewer_id = case_i.reviewer_id closed_state_id = get_case_state_by_name('Closed').state_id request_data['case_name'] = f"#{case_i.case_id} - {request_data.get('case_name').replace(f'#{case_i.case_id} - ', '')}" request_data['case_customer'] = case_i.client_id if request_data.get('case_customer') is None else request_data.get('case_customer') request_data['reviewer_id'] = None if request_data.get('reviewer_id') == "" else request_data.get('reviewer_id') case = case_schema.load(request_data, instance=case_i, partial=True) db.session.commit() if previous_case_state != case.state_id: if case.state_id == closed_state_id: track_activity("case closed", caseid=cur_id) res = close_case(cur_id) if not res: return response_error("Tried to close an non-existing case") # Close the related alerts if case.alerts: close_status = get_alert_status_by_name('Closed') case_status_id_mapped = map_alert_resolution_to_case_status(case.status_id) for alert in case.alerts: if alert.alert_status_id != close_status.status_id: alert.alert_status_id = close_status.status_id alert = call_modules_hook('on_postload_alert_update', data=alert, caseid=caseid) if alert.alert_resolution_status_id != case_status_id_mapped: alert.alert_resolution_status_id = case_status_id_mapped alert = call_modules_hook('on_postload_alert_resolution_update', data=alert, caseid=caseid) track_activity(f"closing alert ID {alert.alert_id} due to case #{caseid} being closed", caseid=caseid, ctx_less=False) db.session.add(alert) elif previous_case_state == closed_state_id and case.state_id != closed_state_id: track_activity("case re-opened", caseid=cur_id) res = reopen_case(cur_id) if not res: return response_error("Tried to re-open an non-existing case") if case_previous_reviewer_id != case.reviewer_id: if case.reviewer_id is None: track_activity("case reviewer removed", caseid=cur_id) case.review_status_id = get_review_id_from_name(ReviewStatusList.not_reviewed) else: track_activity("case reviewer changed", caseid=cur_id) register_case_protagonists(case.case_id, request_data.get('protagonists')) save_case_tags(request_data.get('case_tags'), case_i) case = call_modules_hook('on_postload_case_update', data=case, caseid=caseid) add_obj_history_entry(case_i, 'case info updated') track_activity("case updated {case_name}".format(case_name=case.name), caseid=cur_id) except marshmallow.exceptions.ValidationError as e: return response_error(msg="Data error", data=e.messages, status=400) except Exception as e: log.error(e.__str__()) log.error(traceback.format_exc()) return response_error(msg="Error updating case - check server logs", status=400) return response_success(msg='Case updated', data=case_schema.dump(case)) @manage_cases_blueprint.route('/manage/cases/trigger-pipeline', methods=['POST']) @ac_api_requires(Permissions.standard_user) def update_case_files(caseid): if not ac_fast_check_current_user_has_case_access(caseid, [CaseAccessLevel.full_access]): return ac_api_return_access_denied(caseid=caseid) # case update request. The files should have already arrived with the request upload_files try: # Create the update task jsdata = request.get_json() if not jsdata: return response_error('Not a JSON content', status=400) pipeline = jsdata.get('pipeline') try: pipeline_mod = pipeline.split("-")[0] pipeline_name = pipeline.split("-")[1] except Exception as e: log.error(e.__str__()) return response_error('Malformed request', status=400) ppl_config = get_pipelines_args_from_name(pipeline_mod) if not ppl_config: return response_error('Malformed request', status=400) pl_args = ppl_config['pipeline_args'] pipeline_args = {} for argi in pl_args: arg = argi[0] fetch_arg = jsdata.get('args_' + arg) if argi[1] == 'required' and (not fetch_arg or fetch_arg == ""): return response_error("Required arguments are not set") if fetch_arg: pipeline_args[arg] = fetch_arg else: pipeline_args[arg] = None status = task_case_update( module=pipeline_mod, pipeline=pipeline_name, pipeline_args=pipeline_args, caseid=caseid) if status.is_success(): # The job has been created, so return. The progress can be followed on the dashboard return response_success("Case task created") else: # We got some errors and cannot continue return response_error(status.get_message(), data=status.get_data()) except Exception as e: traceback.print_exc() return response_error('Fail to update case', data=traceback.print_exc()) @manage_cases_blueprint.route('/manage/cases/upload_files', methods=['POST']) @ac_api_requires(Permissions.standard_user) def manage_cases_uploadfiles(caseid): """ Handles the entire the case management, i.e creation, update, list and files imports :param path: Path within the URL :return: Depends on the path, either a page a JSON """ if not ac_fast_check_current_user_has_case_access(caseid, [CaseAccessLevel.full_access]): return ac_api_return_access_denied(caseid=caseid) # Files uploads of a case. Get the files, create the folder tree # The request "add" will start the check + import of the files. f = request.files.get('file') is_update = request.form.get('is_update', type=str) pipeline = request.form.get('pipeline', '', type=str) try: pipeline_mod = pipeline.split("-")[0] except Exception as e: log.error(e.__str__()) return response_error('Malformed request', status=400) if not iris_module_exists(pipeline_mod): return response_error('Missing pipeline', status=400) mod, _ = instantiate_module_from_name(pipeline_mod) status = configure_module_on_init(mod) if status.is_failure(): return response_error("Path for upload {} is not built ! Unreachable pipeline".format( os.path.join(f.filename)), status=400) case_customer = None case_name = None if is_update == "true": case = get_case(caseid) if case: case_name = case.name case_customer = case.client.name else: case_name = urllib.parse.quote(request.form.get('case_name', '', type=str), safe='') case_customer = request.form.get('case_customer', type=str) fpath = build_upload_path(case_customer=case_customer, case_name=urllib.parse.unquote(case_name), module=pipeline_mod, create=is_update ) status = mod.pipeline_files_upload(fpath, f, case_customer, case_name, is_update) if status.is_success(): return response_success(status.get_message()) return response_error(status.get_message(), status=400)