#!/usr/bin/env python3 # # # IRIS Source Code # Copyright (C) 2021 - Airbus CyberSecurity (SAS) # ir@cyberactionlab.net # # 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. # IMPORTS ------------------------------------------------ import json import os import pickle from flask import Blueprint from flask import redirect from flask import render_template from flask import request from flask import url_for from flask_wtf import FlaskForm from sqlalchemy import desc import app from app.iris_engine.module_handler.module_handler import call_modules_hook from app.models import CaseAssets from app.models import CaseReceivedFile from app.models import CaseTasks from app.models import Cases from app.models import CasesEvent from app.models import CeleryTaskMeta from app.models import GlobalTasks from app.models import Ioc from app.models import IrisHook from app.models import IrisModule from app.models import IrisModuleHook from app.models import Notes from app.models.alerts import Alert from app.models.authorization import CaseAccessLevel from app.models.authorization import Permissions from app.util import ac_api_case_requires from app.util import ac_api_requires 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 from iris_interface.IrisInterfaceStatus import IIStatus dim_tasks_blueprint = Blueprint( 'dim_tasks', __name__, template_folder='templates' ) basedir = os.path.abspath(os.path.dirname(app.__file__)) # CONTENT ------------------------------------------------ @dim_tasks_blueprint.route('/dim/tasks', methods=['GET']) @ac_requires(Permissions.standard_user) def dim_index(caseid: int, url_redir): if url_redir: return redirect(url_for('dim.dim_index', cid=caseid)) form = FlaskForm() return render_template('dim_tasks.html', form=form) @dim_tasks_blueprint.route('/dim/hooks/options//list', methods=['GET']) @ac_api_requires() def list_dim_hook_options_ioc(hook_type, caseid): mods_options = IrisModuleHook.query.with_entities( IrisModuleHook.manual_hook_ui_name, IrisHook.hook_name, IrisModule.module_name ).filter( IrisHook.hook_name == f"on_manual_trigger_{hook_type}", IrisModule.is_active == True ).join( IrisModuleHook.module, IrisModuleHook.hook ).all() data = [options._asdict() for options in mods_options] return response_success("", data=data) @dim_tasks_blueprint.route('/dim/hooks/call', methods=['POST']) @ac_api_case_requires(CaseAccessLevel.full_access) def dim_hooks_call(caseid): logs = [] js_data = request.json if not js_data: return response_error('Invalid data') hook_name = js_data.get('hook_name') if not hook_name: return response_error('Missing hook_name') hook_ui_name = js_data.get('hook_ui_name') targets = js_data.get('targets') if not targets: return response_error('Missing targets') data_type = js_data.get('type') if not data_type: return response_error('Missing data type') module_name = js_data.get('module_name') index = 0 obj_targets = [] for target in js_data.get('targets'): if type(target) == str: try: target = int(target) except ValueError: return response_error('Invalid target') elif type(target) != int: return response_error('Invalid target') if data_type == 'ioc': obj = Ioc.query.filter(Ioc.ioc_id == target).first() elif data_type == "case": obj = Cases.query.filter(Cases.case_id == caseid).first() elif data_type == "asset": obj = CaseAssets.query.filter( CaseAssets.asset_id == target, CaseAssets.case_id == caseid ).first() elif data_type == "note": obj = Notes.query.filter( Notes.note_id == target, Notes.note_case_id == caseid ).first() elif data_type == "event": obj = CasesEvent.query.filter( CasesEvent.event_id == target, CasesEvent.case_id == caseid ).first() elif data_type == "task": obj = CaseTasks.query.filter( CaseTasks.id == target, CaseTasks.task_case_id == caseid ).first() elif data_type == "evidence": obj = CaseReceivedFile.query.filter( CaseReceivedFile.id == target, CaseReceivedFile.case_id == caseid ).first() elif data_type == "global_task": obj = GlobalTasks.query.filter( GlobalTasks.id == target ).first() elif data_type == 'alert': obj = Alert.query.filter( Alert.alert_id == target ).first() else: logs.append(f'Data type {data_type} not supported') continue if not obj: logs.append(f'Object ID {target} not found') continue obj_targets.append(obj) # Call to queue task index += 1 if len(obj_targets) > 0: call_modules_hook(hook_name=hook_name, hook_ui_name=hook_ui_name, data=obj_targets, caseid=caseid, module_name=module_name) if len(logs) > 0: return response_error(f"Errors encountered during processing of data. Queued task with {index} objects", data=logs) return response_success(f'Queued task with {index} objects') @dim_tasks_blueprint.route('/dim/tasks/list/', methods=['GET']) @ac_api_requires() def list_dim_tasks(count, caseid): tasks = CeleryTaskMeta.query.filter( ~ CeleryTaskMeta.name.like('app.iris_engine.updater.updater.%') ).order_by(desc(CeleryTaskMeta.date_done)).limit(count).all() data = [] for row in tasks: tkp = {} tkp['state'] = row.status tkp['case'] = "Unknown" tkp['module'] = row.name tkp['task_id'] = row.task_id tkp['date_done'] = row.date_done tkp['user'] = "Unknown" try: tinfo = row.result except AttributeError: # Legacy task data.append(tkp) continue if row.name is not None and 'task_hook_wrapper' in row.name: task_name = f"{row.kwargs}::{row.kwargs}" else: task_name = row.name user = None case_name = None if row.kwargs and row.kwargs != b'{}': kwargs = json.loads(row.kwargs.decode('utf-8')) if kwargs: user = kwargs.get('init_user') case_name = f"Case #{kwargs.get('caseid')}" task_name = f"{kwargs.get('module_name')}::{kwargs.get('hook_name')}" try: result = pickle.loads(row.result) except: result = None if isinstance(result, IIStatus): try: success = result.is_success() except: success = None else: success = None tkp['state'] = "success" if success else str(row.result) tkp['user'] = user if user else "Shadow Iris" tkp['module'] = task_name tkp['case'] = case_name if case_name else "" data.append(tkp) return response_success("", data=data) @dim_tasks_blueprint.route('/dim/tasks/status/', methods=['GET']) @ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access) def task_status(task_id, caseid, url_redir): if url_redir: return response_error("Invalid request") task = app.celery.AsyncResult(task_id) try: tinfo = task.info except AttributeError: # Legacy task task_info = {} task_info['Danger'] = 'This task was executed in a previous version of IRIS and the status cannot be read ' \ 'anymore.' task_info['Note'] = 'All the data readable by the current IRIS version is displayed in ' \ 'the table.' task_info['Additional information'] = 'The results of this tasks were stored in a pickled Class which does' \ ' not exists anymore in current IRIS version.' return render_template("modal_task_info.html", data=task_info, task_id=task.id) task_info = {} task_info['Task ID'] = task_id task_info['Task finished on'] = task.date_done task_info['Task state']: task.state.lower() task_info['Engine']: task.name if task.name else "No engine. Unrecoverable shadow failure" task_meta = task._get_task_meta() if task_meta.get('name') \ and ('task_hook_wrapper' in task_meta.get('name') or 'pipeline_dispatcher' in task_meta.get('name')): task_info['Module name'] = task_meta.get('kwargs').get('module_name') task_info['Hook name'] = task_meta.get('kwargs').get('hook_name') task_info['User'] = task_meta.get('kwargs').get('init_user') task_info['Case ID'] = task_meta.get('kwargs').get('caseid') if isinstance(task.info, IIStatus): success = task.info.is_success() task_info['Logs'] = task.info.get_logs() else: success = None task_info['User'] = "Shadow Iris" task_info['Logs'] = ['Task did not returned a valid IIStatus object'] if task_meta.get('traceback'): task_info['Traceback'] = task.traceback task_info['Success'] = "Success" if success else "Failure" return render_template("modal_task_info.html", data=task_info, task_id=task.id)