hcornet 506716e703
Some checks failed
Deployment Verification / deploy-and-test (push) Failing after 29s
first sync
2025-03-04 07:59:21 +01:00

569 lines
23 KiB
Python

#!/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/<int:cur_id>', 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/<int:cur_id>', 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/<int:cur_id>', 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/<int:cur_id>', 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/<int:cur_id>', 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/<int:cur_id>', 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/<int:cur_id>', 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)