# 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 binascii import marshmallow import traceback from flask import Blueprint from flask import request from flask_login import current_user from sqlalchemy import and_ from sqlalchemy import desc from app import app from app import db from app import socket_io from app.blueprints.rest.endpoints import endpoint_deprecated from app.business.cases import cases_exists from app.datamgmt.case.case_db import get_review_id_from_name from app.datamgmt.case.case_db import case_get_desc_crc from app.datamgmt.case.case_db import get_case 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_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.business.cases import cases_export_to_json 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_case_access_for_users from app.iris_engine.utils.tracker import track_activity from app.models.models import CaseStatus from app.models.models import ReviewStatusList from app.models.models import UserActivity from app.models.authorization import CaseAccessLevel from app.models.authorization import User from app.schema.marshables import TaskLogSchema from app.schema.marshables import CaseSchema from app.schema.marshables import CaseDetailsSchema 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 case_rest_blueprint = Blueprint('case_rest', __name__) log = app.logger @case_rest_blueprint.route('/case/exists', methods=['GET']) @endpoint_deprecated('GET', '/api/v2/cases/') @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access) @ac_api_requires() def case_routes_exists(caseid): if cases_exists(caseid): return response_success('Case exists') return response_error('Case does not exist', 404) @case_rest_blueprint.route('/case/summary/update', methods=['POST']) @ac_requires_case_identifier(CaseAccessLevel.full_access) @ac_api_requires() 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_rest_blueprint.route('/case/summary/fetch', methods=['GET']) @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access) @ac_api_requires() 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_rest_blueprint.route('/case/activities/list', methods=['GET']) @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access) @ac_api_requires() 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_rest_blueprint.route('/case/export', methods=['GET']) @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access) @ac_api_requires() def export_case(caseid): return response_success('', data=cases_export_to_json(caseid)) @case_rest_blueprint.route('/case/meta', methods=['GET']) @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access) @ac_api_requires() def meta_case(caseid): case_details = get_case(caseid) return response_success('', data=CaseDetailsSchema().dump(case_details)) @case_rest_blueprint.route('/case/tasklog/add', methods=['POST']) @ac_requires_case_identifier(CaseAccessLevel.full_access) @ac_api_requires() 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) return response_success('Log saved', data=ua) @case_rest_blueprint.route('/case/users/list', methods=['GET']) @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access) @ac_api_requires() def case_get_users(caseid): users = get_users_list_restricted_from_case(caseid) return response_success(data=users) @case_rest_blueprint.route('/case/access/set-group', methods=['POST']) @ac_requires_case_identifier(CaseAccessLevel.full_access) @ac_api_requires() 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_rest_blueprint.route('/case/access/set-user', methods=['POST']) @ac_requires_case_identifier(CaseAccessLevel.full_access) @ac_api_requires() 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_rest_blueprint.route('/case/update-status', methods=['POST']) @ac_requires_case_identifier(CaseAccessLevel.full_access) @ac_api_requires() 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 = {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_rest_blueprint.route('/case/review/update', methods=['POST']) @ac_requires_case_identifier(CaseAccessLevel.full_access) @ac_api_requires() 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 in ('cancel', '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))