#!/usr/bin/env python3 # # IRIS Source Code # Copyright (C) 2021 - Airbus CyberSecurity (SAS) - DFIR-IRIS Team # ir@cyberactionlab.net - 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 binascii import marshmallow # IMPORTS ------------------------------------------------ import traceback 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_socketio import emit from flask_socketio import join_room from flask_wtf import FlaskForm from sqlalchemy import and_ from sqlalchemy import desc from app import app from app import db from app import socket_io from app.blueprints.case.case_assets_routes import case_assets_blueprint from app.blueprints.case.case_graphs_routes import case_graph_blueprint from app.blueprints.case.case_ioc_routes import case_ioc_blueprint from app.blueprints.case.case_notes_routes import case_notes_blueprint from app.blueprints.case.case_rfiles_routes import case_rfiles_blueprint from app.blueprints.case.case_tasks_routes import case_tasks_blueprint from app.blueprints.case.case_timeline_routes import case_timeline_blueprint from app.datamgmt.case.case_db import case_exists, get_review_id_from_name from app.datamgmt.case.case_db import case_get_desc_crc from app.datamgmt.case.case_db import get_activities_report_template from app.datamgmt.case.case_db import get_case from app.datamgmt.case.case_db import get_case_report_template from app.datamgmt.case.case_db import get_case_tags from app.datamgmt.manage.manage_groups_db import add_case_access_to_group from app.datamgmt.manage.manage_groups_db import get_group_with_members from app.datamgmt.manage.manage_groups_db import get_groups_list from app.datamgmt.manage.manage_users_db import get_user from app.datamgmt.manage.manage_users_db import get_users_list_restricted_from_case from app.datamgmt.manage.manage_users_db import set_user_case_access from app.datamgmt.reporter.report_db import export_case_json from app.forms import PipelinesCaseForm from app.iris_engine.access_control.utils import ac_get_all_access_level, ac_fast_check_current_user_has_case_access, \ ac_fast_check_user_has_case_access from app.iris_engine.access_control.utils import ac_set_case_access_for_users from app.iris_engine.module_handler.module_handler import list_available_pipelines from app.iris_engine.utils.tracker import track_activity from app.models import CaseStatus, ReviewStatusList from app.models import UserActivity from app.models.authorization import CaseAccessLevel from app.models.authorization import User from app.schema.marshables import TaskLogSchema, CaseSchema from app.util import ac_api_case_requires, add_obj_history_entry from app.util import ac_case_requires from app.util import ac_socket_requires from app.util import response_error from app.util import response_success app.register_blueprint(case_timeline_blueprint) app.register_blueprint(case_notes_blueprint) app.register_blueprint(case_assets_blueprint) app.register_blueprint(case_ioc_blueprint) app.register_blueprint(case_rfiles_blueprint) app.register_blueprint(case_graph_blueprint) app.register_blueprint(case_tasks_blueprint) case_blueprint = Blueprint('case', __name__, template_folder='templates') event_tags = ["Network", "Server", "ActiveDirectory", "Computer", "Malware", "User Interaction"] log = app.logger # CONTENT ------------------------------------------------ @case_blueprint.route('/case', methods=['GET']) @ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access) def case_r(caseid, url_redir): if url_redir: return redirect(url_for('case.case_r', cid=caseid, redirect=True)) case = get_case(caseid) setattr(case, 'case_tags', get_case_tags(caseid)) form = FlaskForm() reports = get_case_report_template() reports = [row for row in reports] reports_act = get_activities_report_template() reports_act = [row for row in reports_act] if not case: return render_template('select_case.html') desc_crc32, description = case_get_desc_crc(caseid) setattr(case, 'status_name', CaseStatus(case.status_id).name.replace('_', ' ').title()) return render_template('case.html', case=case, desc=description, crc=desc_crc32, reports=reports, reports_act=reports_act, form=form) @case_blueprint.route('/case/exists', methods=['GET']) @ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access) def case_exists_r(caseid): if case_exists(caseid): return response_success('Case exists') else: return response_error('Case does not exist', 404) @case_blueprint.route('/case/pipelines-modal', methods=['GET']) @ac_case_requires(CaseAccessLevel.full_access) def case_pipelines_modal(caseid, url_redir): if url_redir: return redirect(url_for('case.case_r', cid=caseid, redirect=True)) case = get_case(caseid) form = PipelinesCaseForm() pl = list_available_pipelines() form.pipeline.choices = [("{}-{}".format(ap[0], ap[1]['pipeline_internal_name']), ap[1]['pipeline_human_name'])for ap in pl] # Return default page of case management pipeline_args = [("{}-{}".format(ap[0], ap[1]['pipeline_internal_name']), ap[1]['pipeline_human_name'], ap[1]['pipeline_args'])for ap in pl] return render_template('modal_case_pipelines.html', case=case, form=form, pipeline_args=pipeline_args) @socket_io.on('change') @ac_socket_requires(CaseAccessLevel.full_access) def socket_summary_onchange(data): data['last_change'] = current_user.user emit('change', data, to=data['channel'], skip_sid=request.sid) @socket_io.on('save') @ac_socket_requires(CaseAccessLevel.full_access) def socket_summary_onsave(data): data['last_saved'] = current_user.user emit('save', data, to=data['channel'], skip_sid=request.sid) @socket_io.on('clear_buffer') @ac_socket_requires(CaseAccessLevel.full_access) def socket_summary_onchange(message): emit('clear_buffer', message) @socket_io.on('join') @ac_socket_requires(CaseAccessLevel.full_access) def get_message(data): room = data['channel'] join_room(room=room) emit('join', {'message': f"{current_user.user} just joined"}, room=room) @case_blueprint.route('/case/summary/update', methods=['POST']) @ac_api_case_requires(CaseAccessLevel.full_access) def desc_fetch(caseid): js_data = request.get_json() case = get_case(caseid) if not case: return response_error('Invalid case ID') case.description = js_data.get('case_description') crc = binascii.crc32(case.description.encode('utf-8')) db.session.commit() track_activity("updated summary", caseid) if not request.cookies.get('session'): # API call so we propagate the message to everyone data = { "case_description": case.description, "last_saved": current_user.user } socket_io.emit('save', data, to=f"case-{caseid}") return response_success("Summary updated", data=crc) @case_blueprint.route('/case/summary/fetch', methods=['GET']) @ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access) def summary_fetch(caseid): desc_crc32, description = case_get_desc_crc(caseid) return response_success("Summary fetch", data={'case_description': description, 'crc32': desc_crc32}) @case_blueprint.route('/case/activities/list', methods=['GET']) @ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access) def activity_fetch(caseid): ua = UserActivity.query.with_entities( UserActivity.activity_date, User.name, UserActivity.activity_desc, UserActivity.is_from_api ).filter(and_( UserActivity.case_id == caseid, UserActivity.display_in_ui == True )).join( UserActivity.user ).order_by( desc(UserActivity.activity_date) ).limit(40).all() output = [a._asdict() for a in ua] return response_success("", data=output) @case_blueprint.route("/case/export", methods=['GET']) @ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access) def export_case(caseid): return response_success('', data=export_case_json(caseid)) @case_blueprint.route('/case/tasklog/add', methods=['POST']) @ac_api_case_requires(CaseAccessLevel.full_access) def case_add_tasklog(caseid): log_schema = TaskLogSchema() try: log_data = log_schema.load(request.get_json()) ua = track_activity(log_data.get('log_content'), caseid, user_input=True) except marshmallow.exceptions.ValidationError as e: return response_error(msg="Data error", data=e.messages, status=400) return response_success("Log saved", data=ua) @case_blueprint.route('/case/users/list', methods=['GET']) @ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access) def case_get_users(caseid): users = get_users_list_restricted_from_case(caseid) return response_success(data=users) @case_blueprint.route('/case/groups/access/modal', methods=['GET']) @ac_case_requires(CaseAccessLevel.full_access) def groups_cac_view(caseid, url_redir): if url_redir: return redirect(url_for('case.case_r', cid=caseid, redirect=True)) groups = get_groups_list() access_levels = ac_get_all_access_level() return render_template('modal_cac_to_groups.html', groups=groups, access_levels=access_levels, caseid=caseid) @case_blueprint.route('/case/access/set-group', methods=['POST']) @ac_api_case_requires(CaseAccessLevel.full_access) def group_cac_set_case(caseid): data = request.get_json() if not data: return response_error("Invalid request") if data.get('case_id') != caseid: return response_error("Inconsistent case ID") case = get_case(caseid) if not case: return response_error("Invalid case ID") group_id = data.get('group_id') access_level = data.get('access_level') group = get_group_with_members(group_id) try: success, logs = add_case_access_to_group(group, [data.get('case_id')], access_level) if success: success, logs = ac_set_case_access_for_users(group.group_members, caseid, access_level) except Exception as e: log.error("Error while setting case access for group: {}".format(e)) log.error(traceback.format_exc()) return response_error(msg=str(e)) if success: track_activity("case access set to {} for group {}".format(data.get('access_level'), group_id), caseid) add_obj_history_entry(case, "access changed to {} for group {}".format(data.get('access_level'), group_id), commit=True) return response_success(msg=logs) return response_error(msg=logs) @case_blueprint.route('/case/access/set-user', methods=['POST']) @ac_api_case_requires(CaseAccessLevel.full_access) def user_cac_set_case(caseid): data = request.get_json() if not data: return response_error("Invalid request") if data.get('user_id') == current_user.id: return response_error("I can't let you do that, Dave") user = get_user(data.get('user_id')) if not user: return response_error("Invalid user ID") if data.get('case_id') != caseid: return response_error("Inconsistent case ID") case = get_case(caseid) if not case: return response_error('Invalid case ID') try: success, logs = set_user_case_access(user.id, data.get('case_id'), data.get('access_level')) track_activity("case access set to {} for user {}".format(data.get('access_level'), user.name), caseid) add_obj_history_entry(case, "access changed to {} for user {}".format(data.get('access_level'), user.name)) db.session.commit() except Exception as e: log.error("Error while setting case access for user: {}".format(e)) log.error(traceback.format_exc()) return response_error(msg=str(e)) if success: return response_success(msg=logs) return response_error(msg=logs) @case_blueprint.route('/case/update-status', methods=['POST']) @ac_api_case_requires(CaseAccessLevel.full_access) def case_update_status(caseid): case = get_case(caseid) if not case: return response_error('Invalid case ID') status = request.get_json().get('status_id') case_status = set(item.value for item in CaseStatus) try: status = int(status) except ValueError: return response_error('Invalid status') except TypeError: return response_error('Invalid status. Expected int') if status not in case_status: return response_error('Invalid status') case.status_id = status add_obj_history_entry(case, f'status updated to {CaseStatus(status).name}') db.session.commit() return response_success("Case status updated", data=case.status_id) @case_blueprint.route('/case/md-helper', methods=['GET']) @ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access) def case_md_helper(caseid, url_redir): return render_template('case_md_helper.html') @case_blueprint.route('/case/review/update', methods=['POST']) @ac_api_case_requires(CaseAccessLevel.full_access) def case_review(caseid): case = get_case(caseid) if not case: return response_error('Invalid case ID') action = request.get_json().get('action') reviewer_id = request.get_json().get('reviewer_id') if action == 'start': review_name = ReviewStatusList.review_in_progress elif action == 'cancel' or action == 'request': review_name = ReviewStatusList.pending_review elif action == 'no_review': review_name = ReviewStatusList.no_review_required elif action == 'to_review': review_name = ReviewStatusList.not_reviewed elif action == 'done': review_name = ReviewStatusList.reviewed else: return response_error('Invalid action') case.review_status_id = get_review_id_from_name(review_name) if reviewer_id: try: reviewer_id = int(reviewer_id) except ValueError: return response_error('Invalid reviewer ID') if not ac_fast_check_user_has_case_access(reviewer_id, caseid, [CaseAccessLevel.full_access]): return response_error('Invalid reviewer ID') case.reviewer_id = reviewer_id db.session.commit() add_obj_history_entry(case, f'review status updated to {review_name}') track_activity(f'review status updated to {review_name}', caseid) db.session.commit() return response_success("Case review updated", data=CaseSchema().dump(case))