first sync
Some checks failed
Deployment Verification / deploy-and-test (push) Failing after 29s

This commit is contained in:
2025-03-04 07:59:21 +01:00
parent 9cdcf486b6
commit 506716e703
1450 changed files with 577316 additions and 62 deletions

View File

@@ -0,0 +1,82 @@
#!/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 os
from flask import Blueprint
from flask import redirect
from flask import render_template
from flask import url_for
from flask_wtf import FlaskForm
import app
from app.datamgmt.activities.activities_db import get_all_users_activities
from app.datamgmt.activities.activities_db import get_users_activities
from app.models.authorization import Permissions
from app.util import ac_api_requires
from app.util import ac_requires
from app.util import response_success
activities_blueprint = Blueprint(
'activities',
__name__,
template_folder='templates'
)
basedir = os.path.abspath(os.path.dirname(app.__file__))
# CONTENT ------------------------------------------------
@activities_blueprint.route('/activities', methods=['GET'])
@ac_requires(Permissions.activities_read, Permissions.all_activities_read)
def activities_index(caseid: int, url_redir):
if url_redir:
return redirect(url_for('activities.activities_index', cid=caseid, redirect=True))
form = FlaskForm()
return render_template('activities.html', form=form)
@activities_blueprint.route('/activities/list', methods=['GET'])
@ac_api_requires(Permissions.activities_read, Permissions.all_activities_read)
def list_activities(caseid):
# Get User activities from database
user_activities = get_users_activities()
data = [row._asdict() for row in user_activities]
data = sorted(data, key=lambda i: i['activity_date'], reverse=True)
return response_success("", data=data)
@activities_blueprint.route('/activities/list-all', methods=['GET'])
@ac_api_requires(Permissions.all_activities_read)
def list_all_activities(caseid):
# Get User activities from database
user_activities = get_all_users_activities()
data = [row._asdict() for row in user_activities]
data = sorted(data, key=lambda i: i['activity_date'], reverse=True)
return response_success("", data=data)

View File

@@ -0,0 +1,76 @@
{% extends "layouts/default.html" %}
{% block title %} Activities {% endblock title %}
{% block stylesheets %}
<link href="/static/assets/css/dataTables.buttons.min.css" rel="stylesheet">
{% endblock stylesheets %}
{% block content %}
{% if current_user.is_authenticated %}
<div class="page-inner">
<div class="row">
<div class="col-md-12">
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
<div class="card" id="card_main_load" style="display:none;">
<div class="card-header">
<div class="card-title">User activities ( max 10k entries + unbound )
<button type="button" class="btn btn-sm btn-outline-dark float-right ml-2" onclick="refresh_activities();">
Refresh
</button>
</div>
</div>
<div class="card-body">
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" value="" id="non_case_related_act">
<span class="form-check-sign">Show non case related activity</span>
</label>
</div>
<div class="table-responsive" id="activities_table_wrapper">
<div class="selectgroup">
<span id="table_buttons"></span>
</div>
<table class="table display table-striped table-hover" width="100%" cellspacing="0" id="activities_table" >
<thead>
<tr>
<th>Date</th>
<th>User</th>
<th>Case</th>
<th>Manual input</th>
<th>From API</th>
<th>Activity</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Date</th>
<th>User</th>
<th>Case</th>
<th>Manual input</th>
<th>From API</th>
<th>Activity</th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{{ form.hidden_tag() }}
{% endif %}
{% endblock content %}
{% block javascripts %}
<script src="/static/assets/js/plugin/datatables/dataTables.cellEdit.js"></script>
<script src="/static/assets/js/plugin/datatables/dataTables.buttons.min.js"></script>
<script src="/static/assets/js/plugin/datatables/buttons.html5.min.js"></script>
<script src="/static/assets/js/plugin/datatables/buttons.print.min.js"></script>
<script src="/static/assets/js/iris/datatablesUtils.js"></script>
<script src="/static/assets/js/iris/activities.js"></script>
{% endblock javascripts %}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,310 @@
{% extends "layouts/default.html" %}
{% block title %} Alerts {% endblock title %}
{% block stylesheets %}
<link rel="stylesheet" href="/static/assets/css/suggestags.css">
<link rel="stylesheet" href="/static/assets/css/alerts.css">
{% endblock stylesheets %}
{% block content %}
<div class="page-inner">
<div class="">
{{ form.csrf_token }}
<div class="container-fluid">
<div class="row justify-content-between align-items-center">
<div class="card-title mb-2">
<span id="alertsInfoFilter">Loading..</span>
</div>
</div>
<div class="row justify-content-between align-items-center">
<div class="col">
<div class="card-subtitle mt-2">
<div class="d-flex">
<button class="btn btn-sm" href="#filterCardBody" title="Filter" data-toggle="collapse" role="button" aria-expanded="false" aria-controls="filterCardBody">Filter</button>
<div class="preset-dropdown-container">
<div id="savedFiltersDropdown" class="dropdown"></div>
</div>
<button type="button" class="btn btn-sm btn-outline-dark ml-2" id="resetFilters" style="display: none;">Clear Filters</button>
</div>
</div>
</div>
<div class="col-auto">
<div class="card-subtitle mt-2">
<div class="d-flex">
<div id="alerts-batch-actions" style="display:none;" class="mr-4">
<button type="button" class="btn btn-sm ml-2 btn-alert-primary" onclick="mergeMultipleAlertsModal();">Merge</button>
<div class="dropdown ml-2 d-inline-block">
<button type="button" class="btn btn-alert-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Assign
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="#" onclick="updateBatchAlerts({alert_owner_id: userWhoami.user_id});">Assign to me</a>
<a class="dropdown-item" href="#" onclick="changeBatchAlertOwner();">Assign</a>
</div>
</div>
<div class="dropdown ml-2 d-inline-block">
<button type="button" class="btn btn-alert-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Set status
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="#" onclick="changeStatusBatchAlerts('New');">New</a>
<a class="dropdown-item" href="#" onclick="changeStatusBatchAlerts('In progress');">In progress</a>
<a class="dropdown-item" href="#" onclick="changeStatusBatchAlerts('Pending');">Pending</a>
<a class="dropdown-item" href="#" onclick="changeStatusBatchAlerts('Closed');">Closed</a>
<a class="dropdown-item" href="#" onclick="changeStatusBatchAlerts('Merged');">Merged</a>
</div>
</div>
<button type="button" class="btn btn-alert-danger btn-sm ml-2" onclick="deleteBatchAlerts();"><i class="fa fa-trash mr-2"></i>Delete</button>
</div>
<button class="btn btn-sm mr-2" id="select-deselect-all" style="display:none;">Select all</button>
<button class="btn btn-sm mr-2" id="toggle-selection-mode">Select</button>
<button class="btn btn-sm mr-2" id="orderAlertsBtn"><i class="fas fa-arrow-up-short-wide"></i></button>
<button id="toggleAllAlertsBtn" class="btn btn-sm mr-2" onclick="toggleCollapseAllAlerts()" data-is-expanded="false">Expand All</button>
<div class="d-inline-block position-relative">
<button class="btn btn-sm mr-2 ml-2" onclick="refreshAlerts();">Refresh</button>
<span class="badge badge-pill badge-danger position-absolute" id="newAlertsBadge" style="top: -10px; right: -10px; display: none;">0</span>
</div>
<div class="pagination-dropdown-container ml-3 mt-1">
<label for="alertsPerPage">Alerts per page:</label>
<select id="alertsPerPage">
<option value="5">5</option>
<option value="10" selected>10</option>
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="200">200</option>
<option value="500">500</option>
<option value="1000">1000</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="collapse" id="filterCardBody">
<form id="alertFilterForm" class="container mt-4">
<div class="form-row">
<div class="col-md-3 form-group">
<label for="alert_title">Title</label>
<input type="text" class="form-control" id="alert_title" name="alert_title">
</div>
<div class="col-md-3 form-group">
<label for="alert_description">Description</label>
<input type="text" class="form-control" id="alert_description" name="alert_description">
</div>
<div class="col-md-3 form-group">
<label for="alert_source">Source</label>
<input type="text" class="form-control" id="alert_source" name="alert_source">
</div>
<div class="col-md-3 form-group">
<label for="alert_tags">Tags</label>
<input type="text" class="form-control" id="alert_tags" name="alert_tags">
</div>
</div>
<div class="form-row">
<div class="col-md-3 form-group">
<label for="alert_status_id">Status</label>
<select class="form-control" id="alert_status_id" name="alert_status_id">
</select>
</div>
<div class="col-md-3 form-group">
<label for="alert_severity_id">Severity</label>
<select class="form-control" id="alert_severity_id" name="alert_severity_id">
</select>
</div>
<div class="col-md-3 form-group">
<label for="alert_classification_id">Classification</label>
<select class="form-control" id="alert_classification_id" name="alert_classification_id">
</select>
</div>
<div class="col-md-3 form-group">
<label for="alert_customer_id">Customer</label>
<select class="form-control" id="alert_customer_id" name="alert_customer_id">
</select>
</div>
</div>
<div class="form-row">
<div class="col-md-3 form-group">
<label for="source_start_date">Start Date</label>
<input type="date" class="form-control" id="source_start_date" name="source_start_date">
</div>
<div class="col-md-3 form-group">
<label for="source_end_date">End Date</label>
<input type="date" class="form-control" id="source_end_date" name="source_end_date">
</div>
<div class="col-md-3 form-group">
<label for="alert_assets">Asset(s) name</label>
<input class="form-control" id="alert_assets" name="alert_assets">
</div>
<div class="col-md-3 form-group">
<label for="alert_iocs">IOC(s)</label>
<input class="form-control" id="alert_iocs" name="alert_iocs">
</div>
</div>
<div class="form-row">
<div class="col-md-3 form-group">
<label for="alert_ids">Alert(s) ID</label>
<input class="form-control" id="alert_ids" name="alert_ids">
</div>
<div class="col-md-3 form-group">
<label for="case_id">Case ID</label>
<input type="number" class="form-control" id="case_id" name="case_id">
</div>
<div class="col-md-3 form-group">
<label for="alert_owner_id">Owner</label>
<select class="form-control" id="alert_owner_id" name="alert_owner_id">
</select>
</div>
<div class="col-md-3 form-group">
<label for="alert_resolution_id">Resolution Status</label>
<select class="form-control" id="alert_resolution_id" name="alert_resolution_id">
</select>
</div>
</div>
<div class="form-row mt-3">
<div class="col centered">
<button type="submit" class="btn btn-sm btn-primary">Apply Filters</button>
<button type="button" class="btn btn-sm btn-outline-success float-right" id="saveFilters">Save as filter</button>
</div>
</div>
</form>
</div>
</div>
<div class="row mt-4 mb-4 ml-1">
<div class="col">
<span id="alertsInfoFilterTags"></span>
</div>
</div>
<div class="row mt-2">
<div class="col">
<nav class="mt-3 float-right">
<ul class="pagination pagination-container">
</ul>
</nav>
</div>
</div>
<div class="list-group alerts-container">
</div>
<nav class="mt-3 float-right">
<ul class="pagination pagination-container">
</ul>
</nav>
</div>
<div class="dropdown-menu" id="context-menu-relationships" style="display: none;">
<a id="view-alert" class="dropdown-item" style="cursor: pointer;" onclick="viewAlertGraph();">
<i class="fas fa-eye mr-2"></i><span id="view-alert-text">View Alert</span></a>
</div>
<div class="modal" role="dialog" tabindex="-1" id="modal_comment" data-backdrop="false">
<div class="modal-lg modal-dialog modal-comment" role="document">
<div class="modal-content shadow-xl" id="modal_comment_content">
</div>
</div>
</div>
<div class="modal" role="dialog" tabindex="-1" id="modal_graph_options" data-backdrop="false">
<div class="modal-lg modal-dialog modal-comment" role="document">
<div class="modal-content shadow-xl" id="modal_graph_options_content">
</div>
</div>
</div>
<div class="modal" role="dialog" tabindex="-1" id="modal_alert_history">
<div class="modal-lg modal-dialog" role="document">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title">Alert history</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" id="modal_alert_history_content">
</div>
</div>
</div>
</div>
<div class="modal" id="editAlertModal" tabindex="-1" role="dialog" aria-labelledby="closeAlertModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="closeAlertModalLabel"></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<label class="form-label">Resolution status</label><br>
<div class="selectgroup ml-auto mr-auto">
<label class="selectgroup-item">
<input type="radio" name="resolutionStatus" value="not_applicable" class="selectgroup-input" checked="">
<span class="selectgroup-button">Not applicable</span>
</label>
<label class="selectgroup-item">
<input type="radio" name="resolutionStatus" value="false_positive" class="selectgroup-input">
<span class="selectgroup-button">False positive</span>
</label>
<label class="selectgroup-item selectgroup-warning">
<input type="radio" name="resolutionStatus" value="true_positive_without_impact" class="selectgroup-input">
<span class="selectgroup-button">True positive without impact</span>
</label>
<label class="selectgroup-item">
<input type="radio" name="resolutionStatus" value="true_positive_with_impact" class="selectgroup-input">
<span class="selectgroup-button">True positive with impact</span>
</label>
</div>
</div>
<div class="form-group">
<label for="editAlertNote">Note</label>
<textarea class="form-control" id="editAlertNote" rows="3"></textarea>
</div>
<div class="form-group">
<label for="editAlertTags">Tags</label>
<input type="text" class="form-control" id="editAlertTags">
</div>
</form>
<div class="form-group alert-edition-part">
<label for="editAlertClassification">Classification</label>
<select class="form-control" id="editAlertClassification">
</select>
</div>
<div class="form-group alert-edition-part">
<label for="editAlertSeverity">Severity</label>
<select class="form-control" id="editAlertSeverity">
</select>
</div>
<div class="mt-4">
<button type="button" class="btn btn-primary float-right mr-2" id="confirmAlertEdition">Close Alert</button>
<button type="button" class="btn btn-dark float-right mr-2" data-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
</div>
{% include 'modal_escalate.html' %}
{% include 'modal_enrichment.html' %}
{% include 'modal_new_alert_owner.html' %}
{% endblock content %}
{% block javascripts %}
<script src="/static/assets/js/plugin/vis/vis.min.js"></script>
<script src="/static/assets/js/plugin/vis/vis-network.min.js"></script>
<script src="/static/assets/js/iris/alerts.js"></script>
<script src="/static/assets/js/iris/comments.js"></script>
<script src="/static/assets/js/core/socket.io.js"></script>
{% endblock javascripts %}

View File

@@ -0,0 +1,18 @@
<div class="modal" id="enrichmentModal" tabindex="-1" role="dialog" aria-labelledby="enrichmentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="enrichmentModalLabel">Enrichment</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="enrichmentData"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-dark" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,86 @@
<div class="modal" id="escalateModal" tabindex="-1" role="dialog" aria-labelledby="escalateModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="escalateModalLabel"></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group row" id="modalAlertTitleContainer">
<div class="col">
<label for="modalAlertTitle">Alert title</label>
<input type="text" class="form-control" id="modalAlertTitle" readonly>
</div>
</div>
<div class="form-group">
<label class="form-label">Merge options</label><br/>
<div class="selectgroup ml-auto mr-auto">
<label class="selectgroup-item">
<input type="radio" name="mergeOption" value="new_case" class="selectgroup-input" checked>
<span class="selectgroup-button">Merge into a new case</span>
</label>
<label class="selectgroup-item">
<input type="radio" name="mergeOption" value="existing_case" class="selectgroup-input">
<span class="selectgroup-button">Merge into existing case</span>
</label>
</div>
</div>
<div class="form-group"><span id="escalateModalExplanation"></span></div>
<div class="form-group" id="modalEscalateCaseTitleContainer" style="display:none;">
<label>New case title *</label>
<input type="text" class="form-control" id="modalEscalateCaseTitle" name="new_case_title">
</div>
<div class="form-group" id="mergeAlertCaseSelectSection" style="display:none;">
<label for="mergeAlertCaseSelect">Select case to merge into *</label>
<select class="selectpicker form-control" data-dropup-auto="false" data-live-search="true" id="mergeAlertCaseSelect">
</select>
</div>
<div class="form-group" id="mergeAlertCaseTemplateSection">
<label for="mergeAlertCaseTemplate">Select case template</label>
<select class="selectpicker form-control" data-dropup-auto="false" data-live-search="true" id="mergeAlertCaseTemplateSelect">
</select>
</div>
<div class="form-group mt-4" id="ioc-container" style="display:none;">
<label>IOCs to import</label>
<button type="button" class="btn btn-sm btn-light ml-2 float-right" id="toggle-iocs">Deselect All</button>
<div class="form-control" id="ioCsList" style="height: auto; overflow-y: scroll; max-height: 150px;"></div>
</div>
<div class="form-group mt-4" id="asset-container" style="display:none;">
<label>Assets to import</label>
<button type="button" class="btn btn-sm btn-light ml-2 float-right" id="toggle-assets">Deselect All</button>
<div class="form-control" id="assetsList" style="height: auto; overflow-y: scroll; max-height: 150px;"></div>
</div>
<div class="form-group mt-4">
<label for="note">Escalation note</label>
<textarea class="form-control" id="note" rows="3"></textarea>
</div>
<div class="form-group">
<label for="case_tags">Case tags</label>
<input type="text" id="case_tags"
class="form-control col-md-12"/>
</div>
<div class="col-md-3 form-group">
<div class="form-check">
<label class="form-check-label mt-3">
<input checked="" class="form-check-input" id="importAsEvent" name="import_as_event" type="checkbox" value="y">
<span class="form-check-sign"> Add alert as event in the timeline
</span>
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-dark" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="escalateOrMergeButton">Merge</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,22 @@
<div class="modal" tabindex="-1" id="changeAlertOwnerModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Reassign Alert #<span id="alertIDAssignModal"></span></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="changeOwnerAlertSelect">New Owner</label>
<select class="form-control" id="changeOwnerAlertSelect"></select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-dark" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="assign-owner-button">Assign</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,50 @@
#!/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.
from flask import Blueprint
from app import app
from app.util import ac_api_requires
from app.util import response_success
api_blueprint = Blueprint(
'api',
__name__,
template_folder='templates'
)
# CONTENT ------------------------------------------------
@api_blueprint.route('/api/ping', methods=['GET'])
@ac_api_requires()
def api_ping(caseid):
return response_success("pong")
@api_blueprint.route('/api/versions', methods=['GET'])
@ac_api_requires()
def api_version(caseid):
versions = {
"iris_current": app.config.get('IRIS_VERSION'),
"api_min": app.config.get('API_MIN_VERSION'),
"api_current": app.config.get('API_MAX_VERSION')
}
return response_success(data=versions)

View File

@@ -0,0 +1,25 @@
#!/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 ------------------------------------------------
# VARS ---------------------------------------------------
# CONTENT ------------------------------------------------

View File

@@ -0,0 +1,503 @@
#!/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 csv
# IMPORTS ------------------------------------------------
from datetime import datetime
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 app import db
from app.blueprints.case.case_comments import case_comment_update
from app.datamgmt.case.case_assets_db import add_comment_to_asset
from app.datamgmt.case.case_assets_db import create_asset
from app.datamgmt.case.case_assets_db import delete_asset
from app.datamgmt.case.case_assets_db import delete_asset_comment
from app.datamgmt.case.case_assets_db import get_analysis_status_list
from app.datamgmt.case.case_assets_db import get_asset
from app.datamgmt.case.case_assets_db import get_asset_type_id
from app.datamgmt.case.case_assets_db import get_assets
from app.datamgmt.case.case_assets_db import get_assets_ioc_links
from app.datamgmt.case.case_assets_db import get_assets_types
from app.datamgmt.case.case_assets_db import get_case_asset_comment
from app.datamgmt.case.case_assets_db import get_case_asset_comments
from app.datamgmt.case.case_assets_db import get_case_assets_comments_count
from app.datamgmt.case.case_assets_db import get_compromise_status_list
from app.datamgmt.case.case_assets_db import get_linked_iocs_finfo_from_asset
from app.datamgmt.case.case_assets_db import get_linked_iocs_id_from_asset
from app.datamgmt.case.case_assets_db import get_similar_assets
from app.datamgmt.case.case_assets_db import set_ioc_links
from app.datamgmt.case.case_db import get_case
from app.datamgmt.case.case_db import get_case_client_id
from app.datamgmt.case.case_iocs_db import get_iocs
from app.datamgmt.manage.manage_attribute_db import get_default_custom_attributes
from app.datamgmt.manage.manage_users_db import get_user_cases_fast
from app.datamgmt.states import get_assets_state
from app.datamgmt.states import update_assets_state
from app.forms import AssetBasicForm
from app.forms import ModalAddCaseAssetForm
from app.iris_engine.module_handler.module_handler import call_modules_hook
from app.iris_engine.utils.tracker import track_activity
from app.models import AnalysisStatus
from app.models.authorization import CaseAccessLevel
from app.schema.marshables import CaseAssetsSchema
from app.schema.marshables import CommentSchema
from app.util import ac_api_case_requires
from app.util import ac_case_requires
from app.util import response_error
from app.util import response_success
case_assets_blueprint = Blueprint('case_assets',
__name__,
template_folder='templates')
@case_assets_blueprint.route('/case/assets', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_assets(caseid, url_redir):
"""
Returns the page of case assets, with the list of available assets types.
:return: The HTML page of case assets
"""
if url_redir:
return redirect(url_for('case_assets.case_assets', cid=caseid, redirect=True))
form = ModalAddCaseAssetForm()
# Get asset types from database
form.asset_id.choices = get_assets_types()
# Retrieve the assets linked to the investigation
case = get_case(caseid)
return render_template("case_assets.html", case=case, form=form)
@case_assets_blueprint.route('/case/assets/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_list_assets(caseid):
"""
Returns the list of assets from the case.
:return: A JSON object containing the assets of the case, enhanced with assets seen on other cases.
"""
# Get all assets objects from the case and the customer id
assets = get_assets(caseid)
customer_id = get_case_client_id(caseid)
ret = {}
ret['assets'] = []
ioc_links_req = get_assets_ioc_links(caseid)
cache_ioc_link = {}
for ioc in ioc_links_req:
if ioc.asset_id not in cache_ioc_link:
cache_ioc_link[ioc.asset_id] = [ioc._asdict()]
else:
cache_ioc_link[ioc.asset_id].append(ioc._asdict())
cases_access = get_user_cases_fast(current_user.id)
for asset in assets:
asset = asset._asdict()
if len(assets) < 300:
# Find similar assets from other cases with the same customer
asset['link'] = list(get_similar_assets(
asset['asset_name'], asset['asset_type_id'], caseid, customer_id, cases_access))
else:
asset['link'] = []
asset['ioc_links'] = cache_ioc_link.get(asset['asset_id'])
ret['assets'].append(asset)
ret['state'] = get_assets_state(caseid=caseid)
return response_success("", data=ret)
@case_assets_blueprint.route('/case/assets/state', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_assets_state(caseid):
os = get_assets_state(caseid=caseid)
if os:
return response_success(data=os)
else:
return response_error('No assets state for this case.')
@case_assets_blueprint.route('/case/assets/add/modal', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def add_asset_modal(caseid):
form = AssetBasicForm()
form.asset_type_id.choices = get_assets_types()
form.analysis_status_id.choices = get_analysis_status_list()
form.asset_compromise_status_id.choices = get_compromise_status_list()
# Get IoCs from the case
ioc = get_iocs(caseid)
attributes = get_default_custom_attributes('asset')
return render_template("modal_add_case_multi_asset.html", form=form, asset=None, ioc=ioc, attributes=attributes)
@case_assets_blueprint.route('/case/assets/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def add_asset(caseid):
try:
# validate before saving
add_asset_schema = CaseAssetsSchema()
request_data = call_modules_hook('on_preload_asset_create', data=request.get_json(), caseid=caseid)
asset = add_asset_schema.load(request_data)
asset = create_asset(asset=asset,
caseid=caseid,
user_id=current_user.id
)
if request_data.get('ioc_links'):
errors, logs = set_ioc_links(request_data.get('ioc_links'), asset.asset_id)
if errors:
return response_error(f'Encountered errors while linking IOC. Asset has still been updated.')
asset = call_modules_hook('on_postload_asset_create', data=asset, caseid=caseid)
if asset:
track_activity(f"added asset \"{asset.asset_name}\"", caseid=caseid)
return response_success("Asset added", data=add_asset_schema.dump(asset))
return response_error("Unable to create asset for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_assets_blueprint.route('/case/assets/upload', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_upload_ioc(caseid):
try:
# validate before saving
add_asset_schema = CaseAssetsSchema()
jsdata = request.get_json()
# get IOC list from request
csv_lines = jsdata["CSVData"].splitlines() # unavoidable since the file is passed as a string
headers = "asset_name,asset_type_name,asset_description,asset_ip,asset_domain,asset_tags"
if csv_lines[0].lower() != headers:
csv_lines.insert(0, headers)
# convert list of strings into CSV
csv_data = csv.DictReader(csv_lines, delimiter=',')
ret = []
errors = []
analysis_status = AnalysisStatus.query.filter(AnalysisStatus.name == 'Unspecified').first()
analysis_status_id = analysis_status.id
index = 0
for row in csv_data:
missing_field = False
for e in headers.split(','):
if row.get(e) is None:
errors.append(f"{e} is missing for row {index}")
missing_field = True
continue
if missing_field:
continue
# Asset name must not be empty
if not row.get("asset_name"):
errors.append(f"Empty asset name for row {index}")
track_activity(f"Attempted to upload an empty asset name")
index += 1
continue
if row.get("asset_tags"):
row["asset_tags"] = row.get("asset_tags").replace("|", ",") # Reformat Tags
if not row.get('asset_type_name'):
errors.append(f"Empty asset type for row {index}")
track_activity(f"Attempted to upload an empty asset type")
index += 1
continue
type_id = get_asset_type_id(row['asset_type_name'].lower())
if not type_id:
errors.append(f"{row.get('asset_name')} (invalid asset type: {row.get('asset_type_name')}) for row {index}")
track_activity(f"Attempted to upload unrecognized asset type \"{row.get('asset_type_name')}\"")
index += 1
continue
row['asset_type_id'] = type_id.asset_id
row.pop('asset_type_name', None)
row['analysis_status_id'] = analysis_status_id
request_data = call_modules_hook('on_preload_asset_create', data=row, caseid=caseid)
asset_sc = add_asset_schema.load(request_data)
asset_sc.custom_attributes = get_default_custom_attributes('asset')
asset = create_asset(asset=asset_sc,
caseid=caseid,
user_id=current_user.id
)
asset = call_modules_hook('on_postload_asset_create', data=asset, caseid=caseid)
if not asset:
errors.append(f"Unable to add asset for internal reason")
index += 1
continue
ret.append(request_data)
track_activity(f"added asset {asset.asset_name}", caseid=caseid)
index += 1
if len(errors) == 0:
msg = "Successfully imported data."
else:
msg = "Data is imported but we got errors with the following rows:\n- " + "\n- ".join(errors)
return response_success(msg=msg, data=ret)
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_assets_blueprint.route('/case/assets/<int:cur_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def asset_view(cur_id, caseid):
# Get IoCs already linked to the asset
asset_iocs = get_linked_iocs_finfo_from_asset(cur_id)
ioc_prefill = [row._asdict() for row in asset_iocs]
asset = get_asset(cur_id, caseid)
if not asset:
return response_error("Invalid asset ID for this case")
asset_schema = CaseAssetsSchema()
data = asset_schema.dump(asset)
data['linked_ioc'] = ioc_prefill
return response_success(data=data)
@case_assets_blueprint.route('/case/assets/<int:cur_id>/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def asset_view_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_assets.case_assets', cid=caseid, redirect=True))
# Get IoCs from the case
case_iocs = get_iocs(caseid)
# Get IoCs already linked to the asset
asset_iocs = get_linked_iocs_id_from_asset(cur_id)
ioc_prefill = [row for row in asset_iocs]
# Build the form
form = AssetBasicForm()
asset = get_asset(cur_id, caseid)
form.asset_name.render_kw = {'value': asset.asset_name}
form.asset_description.data = asset.asset_description
form.asset_info.data = asset.asset_info
form.asset_ip.render_kw = {'value': asset.asset_ip}
form.asset_domain.render_kw = {'value': asset.asset_domain}
form.asset_compromise_status_id.choices = get_compromise_status_list()
form.asset_type_id.choices = get_assets_types()
form.analysis_status_id.choices = get_analysis_status_list()
form.asset_tags.render_kw = {'value': asset.asset_tags}
comments_map = get_case_assets_comments_count([cur_id])
return render_template("modal_add_case_asset.html", form=form, asset=asset, map={}, ioc=case_iocs,
ioc_prefill=ioc_prefill, attributes=asset.custom_attributes, comments_map=comments_map)
@case_assets_blueprint.route('/case/assets/update/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def asset_update(cur_id, caseid):
try:
asset = get_asset(cur_id, caseid)
if not asset:
return response_error("Invalid asset ID for this case")
# validate before saving
add_asset_schema = CaseAssetsSchema()
request_data = call_modules_hook('on_preload_asset_update', data=request.get_json(), caseid=caseid)
request_data['asset_id'] = cur_id
asset_schema = add_asset_schema.load(request_data, instance=asset)
update_assets_state(caseid=caseid)
db.session.commit()
if hasattr(asset_schema, 'ioc_links'):
errors, logs = set_ioc_links(asset_schema.ioc_links, asset.asset_id)
if errors:
return response_error(f'Encountered errors while linking IOC. Asset has still been updated.')
asset_schema = call_modules_hook('on_postload_asset_update', data=asset_schema, caseid=caseid)
if asset_schema:
track_activity(f"updated asset \"{asset_schema.asset_name}\"", caseid=caseid)
return response_success("Updated asset {}".format(asset_schema.asset_name),
add_asset_schema.dump(asset_schema))
return response_error("Unable to update asset for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_assets_blueprint.route('/case/assets/delete/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def asset_delete(cur_id, caseid):
call_modules_hook('on_preload_asset_delete', data=cur_id, caseid=caseid)
asset = get_asset(cur_id, caseid)
if not asset:
return response_error("Invalid asset ID for this case")
# Deletes an asset and the potential links with the IoCs from the database
delete_asset(cur_id, caseid)
call_modules_hook('on_postload_asset_delete', data=cur_id, caseid=caseid)
track_activity(f"removed asset ID {asset.asset_name}", caseid=caseid)
return response_success("Deleted")
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_asset_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_task.case_task', cid=caseid, redirect=True))
asset = get_asset(cur_id, caseid=caseid)
if not asset:
return response_error('Invalid asset ID')
return render_template("modal_conversation.html", element_id=cur_id, element_type='assets',
title=asset.asset_name)
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_asset_list(cur_id, caseid):
asset_comments = get_case_asset_comments(cur_id)
if asset_comments is None:
return response_error('Invalid asset ID')
# CommentSchema(many=True).dump(task_comments)
# res = [com._asdict() for com in task_comments]
return response_success(data=CommentSchema(many=True).dump(asset_comments))
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_asset_add(cur_id, caseid):
try:
asset = get_asset(cur_id, caseid=caseid)
if not asset:
return response_error('Invalid asset ID')
comment_schema = CommentSchema()
comment = comment_schema.load(request.get_json())
comment.comment_case_id = caseid
comment.comment_user_id = current_user.id
comment.comment_date = datetime.now()
comment.comment_update_date = datetime.now()
db.session.add(comment)
db.session.commit()
add_comment_to_asset(asset.asset_id, comment.comment_id)
db.session.commit()
hook_data = {
"comment": comment_schema.dump(comment),
"asset": CaseAssetsSchema().dump(asset)
}
call_modules_hook('on_postload_asset_commented', data=hook_data, caseid=caseid)
track_activity(f"asset \"{asset.asset_name}\" commented", caseid=caseid)
return response_success("Asset commented", data=comment_schema.dump(comment))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.normalized_messages(), status=400)
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_asset_get(cur_id, com_id, caseid):
comment = get_case_asset_comment(cur_id, com_id)
if not comment:
return response_error("Invalid comment ID")
return response_success(data=comment._asdict())
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_asset_edit(cur_id, com_id, caseid):
return case_comment_update(com_id, 'assets', caseid)
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_asset_delete(cur_id, com_id, caseid):
success, msg = delete_asset_comment(cur_id, com_id, caseid)
if not success:
return response_error(msg)
call_modules_hook('on_postload_asset_comment_delete', data=com_id, caseid=caseid)
track_activity(f"comment {com_id} on asset {cur_id} deleted", caseid=caseid)
return response_success(msg)

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env python3
#
# IRIS Source Code
# DFIR-IRIS Team
# 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 marshmallow
from datetime import datetime
from flask import request
from app import db
from app.datamgmt.case.case_comments import get_case_comment
from app.iris_engine.module_handler.module_handler import call_modules_hook
from app.iris_engine.utils.tracker import track_activity
from app.schema.marshables import CommentSchema
from app.util import response_error
from app.util import response_success
def case_comment_update(comment_id, object_type, caseid):
comment = get_case_comment(comment_id, caseid=caseid)
if not comment:
return response_error("Invalid comment ID")
try:
rq_t = request.get_json()
comment_text = rq_t.get('comment_text')
comment.comment_text = comment_text
comment.comment_update_date = datetime.utcnow()
comment_schema = CommentSchema()
db.session.commit()
hook = object_type
if hook.endswith('s'):
hook = hook[:-1]
call_modules_hook(f'on_postload_{hook}_comment_update', data=comment_schema.dump(comment), caseid=caseid)
track_activity(f"comment {comment.comment_id} on {object_type} edited", caseid=caseid)
return response_success("Comment edited", data=comment_schema.dump(comment))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.normalized_messages(), status=400)

View File

@@ -0,0 +1,152 @@
#!/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 itertools
from datetime import datetime
from flask import Blueprint
from flask import redirect
from flask import render_template
from flask import url_for
from flask_login import current_user
from flask_wtf import FlaskForm
from app.datamgmt.case.case_db import get_case
from app.datamgmt.case.case_events_db import get_case_events_assets_graph
from app.datamgmt.case.case_events_db import get_case_events_ioc_graph
from app.models.authorization import CaseAccessLevel
from app.util import ac_api_case_requires
from app.util import ac_case_requires
from app.util import response_success
case_graph_blueprint = Blueprint('case_graph',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@case_graph_blueprint.route('/case/graph', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_graph(caseid, url_redir):
if url_redir:
return redirect(url_for('case_graph.case_graph', cid=caseid, redirect=True))
case = get_case(caseid)
form = FlaskForm()
return render_template("case_graph.html", case=case, form=form)
@case_graph_blueprint.route('/case/graph/getdata', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_graph_get_data(caseid):
events = get_case_events_assets_graph(caseid)
events.extend(get_case_events_ioc_graph(caseid))
nodes = []
edges = []
dates = {
"human": [],
"machine": []
}
tmp = {}
for event in events:
if hasattr(event, 'asset_compromise_status_id'):
if event.asset_compromise_status_id == 1:
img = event.asset_icon_compromised
else:
img = event.asset_icon_not_compromised
if event.asset_ip:
title = "{} -{}".format(event.asset_ip, event.asset_description)
else:
title = "{}".format(event.asset_description)
label = event.asset_name
idx = f'a{event.asset_id}'
node_type = 'asset'
else:
img = 'virus-covid-solid.png'
label = event.ioc_value
title = event.ioc_description
idx = f'b{event.ioc_id}'
node_type = 'ioc'
try:
date = "{}-{}-{}".format(event.event_date.day, event.event_date.month, event.event_date.year)
except:
date = '15-05-2021'
if date not in dates:
dates['human'].append(date)
dates['machine'].append(datetime.timestamp(event.event_date))
new_node = {
'id': idx,
'label': label,
'image': '/static/assets/img/graph/' + img,
'shape': 'image',
'title': title,
'value': 1
}
if current_user.in_dark_mode:
new_node['font'] = "12px verdana white"
if not any(node['id'] == idx for node in nodes):
nodes.append(new_node)
ak = {
'node_id': idx,
'node_title': "{} - {}".format(event.event_date, event.event_title),
'node_name': label,
'node_type': node_type
}
if tmp.get(event.event_id):
tmp[event.event_id]['list'].append(ak)
else:
tmp[event.event_id] = {
'master_node': [],
'list': [ak]
}
for event_id in tmp:
for subset in itertools.combinations(tmp[event_id]['list'], 2):
if subset[0]['node_type'] == 'ioc' and subset[1]['node_type'] == 'ioc' and len(tmp[event_id]['list']) != 2:
continue
edge = {
'from': subset[0]['node_id'],
'to': subset[1]['node_id'],
'title': subset[0]['node_title'],
'dashes': subset[0]['node_type'] == 'ioc' or subset[1]['node_type'] == 'ioc'
}
edges.append(edge)
resp = {
'nodes': nodes,
'edges': edges,
'dates': dates
}
return response_success("", data=resp)

View File

@@ -0,0 +1,453 @@
#!/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.
# IMPORTS ------------------------------------------------
from datetime import datetime
import csv
import logging as log
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 app import db
from app.blueprints.case.case_comments import case_comment_update
from app.datamgmt.case.case_assets_db import get_assets_types
from app.datamgmt.case.case_db import get_case
from app.datamgmt.case.case_iocs_db import add_comment_to_ioc
from app.datamgmt.case.case_iocs_db import add_ioc
from app.datamgmt.case.case_iocs_db import add_ioc_link
from app.datamgmt.case.case_iocs_db import check_ioc_type_id
from app.datamgmt.case.case_iocs_db import delete_ioc
from app.datamgmt.case.case_iocs_db import delete_ioc_comment
from app.datamgmt.case.case_iocs_db import get_case_ioc_comment
from app.datamgmt.case.case_iocs_db import get_case_ioc_comments
from app.datamgmt.case.case_iocs_db import get_case_iocs_comments_count
from app.datamgmt.case.case_iocs_db import get_detailed_iocs
from app.datamgmt.case.case_iocs_db import get_ioc
from app.datamgmt.case.case_iocs_db import get_ioc_links
from app.datamgmt.case.case_iocs_db import get_ioc_type_id
from app.datamgmt.case.case_iocs_db import get_ioc_types_list
from app.datamgmt.case.case_iocs_db import get_tlps
from app.datamgmt.case.case_iocs_db import get_tlps_dict
from app.datamgmt.manage.manage_attribute_db import get_default_custom_attributes
from app.datamgmt.states import get_ioc_state
from app.datamgmt.states import update_ioc_state
from app.forms import ModalAddCaseAssetForm
from app.forms import ModalAddCaseIOCForm
from app.iris_engine.module_handler.module_handler import call_modules_hook
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import CaseAccessLevel
from app.models.models import Ioc
from app.schema.marshables import CommentSchema
from app.schema.marshables import IocSchema
from app.util import ac_api_case_requires
from app.util import ac_case_requires
from app.util import response_error
from app.util import response_success
case_ioc_blueprint = Blueprint(
'case_ioc',
__name__,
template_folder='templates'
)
# CONTENT ------------------------------------------------
@case_ioc_blueprint.route('/case/ioc', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_ioc(caseid, url_redir):
if url_redir:
return redirect(url_for('case_ioc.case_ioc', cid=caseid, redirect=True))
form = ModalAddCaseAssetForm()
form.asset_id.choices = get_assets_types()
# Retrieve the assets linked to the investigation
case = get_case(caseid)
return render_template("case_ioc.html", case=case, form=form)
@case_ioc_blueprint.route('/case/ioc/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_list_ioc(caseid):
iocs = get_detailed_iocs(caseid)
ret = {}
ret['ioc'] = []
for ioc in iocs:
out = ioc._asdict()
# Get links of the IoCs seen in other cases
ial = get_ioc_links(ioc.ioc_id, caseid)
out['link'] = [row._asdict() for row in ial]
# Legacy, must be changed next version
out['misp_link'] = None
ret['ioc'].append(out)
ret['state'] = get_ioc_state(caseid=caseid)
return response_success("", data=ret)
@case_ioc_blueprint.route('/case/ioc/state', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_ioc_state(caseid):
os = get_ioc_state(caseid=caseid)
if os:
return response_success(data=os)
else:
return response_error('No IOC state for this case.')
@case_ioc_blueprint.route('/case/ioc/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_add_ioc(caseid):
try:
# validate before saving
add_ioc_schema = IocSchema()
request_data = call_modules_hook('on_preload_ioc_create', data=request.get_json(), caseid=caseid)
ioc = add_ioc_schema.load(request_data)
if not check_ioc_type_id(type_id=ioc.ioc_type_id):
return response_error("Not a valid IOC type")
ioc, existed = add_ioc(ioc=ioc,
user_id=current_user.id,
caseid=caseid
)
link_existed = add_ioc_link(ioc.ioc_id, caseid)
if link_existed:
return response_success("IOC already exists and linked to this case", data=add_ioc_schema.dump(ioc))
if not link_existed:
ioc = call_modules_hook('on_postload_ioc_create', data=ioc, caseid=caseid)
if ioc:
track_activity("added ioc \"{}\"".format(ioc.ioc_value), caseid=caseid)
msg = "IOC already existed in DB. Updated with info on DB." if existed else "IOC added"
return response_success(msg=msg, data=add_ioc_schema.dump(ioc))
return response_error("Unable to create IOC for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_ioc_blueprint.route('/case/ioc/upload', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_upload_ioc(caseid):
try:
# validate before saving
add_ioc_schema = IocSchema()
jsdata = request.get_json()
# get IOC list from request
headers = "ioc_value,ioc_type,ioc_description,ioc_tags,ioc_tlp"
csv_lines = jsdata["CSVData"].splitlines() # unavoidable since the file is passed as a string
if csv_lines[0].lower() != headers:
csv_lines.insert(0, headers)
# convert list of strings into CSV
csv_data = csv.DictReader(csv_lines, quotechar='"', delimiter=',')
# build a Dict of possible TLP
tlp_dict = get_tlps_dict()
ret = []
errors = []
index = 0
for row in csv_data:
for e in headers.split(','):
if row.get(e) is None:
errors.append(f"{e} is missing for row {index}")
index += 1
continue
# IOC value must not be empty
if not row.get("ioc_value"):
errors.append(f"Empty IOC value for row {index}")
track_activity(f"Attempted to upload an empty IOC value")
index += 1
continue
row["ioc_tags"] = row["ioc_tags"].replace("|", ",") # Reformat Tags
# Convert TLP into TLP id
if row["ioc_tlp"] in tlp_dict:
row["ioc_tlp_id"] = tlp_dict[row["ioc_tlp"]]
else:
row["ioc_tlp_id"] = ""
row.pop("ioc_tlp", None)
type_id = get_ioc_type_id(row['ioc_type'].lower())
if not type_id:
errors.append(f"{row['ioc_value']} (invalid ioc type: {row['ioc_type']}) for row {index}")
log.error(f'Unrecognised IOC type {row["ioc_type"]}')
index += 1
continue
row['ioc_type_id'] = type_id.type_id
row.pop('ioc_type', None)
request_data = call_modules_hook('on_preload_ioc_create', data=row, caseid=caseid)
ioc = add_ioc_schema.load(request_data)
ioc.custom_attributes = get_default_custom_attributes('ioc')
ioc, existed = add_ioc(ioc=ioc,
user_id=current_user.id,
caseid=caseid
)
link_existed = add_ioc_link(ioc.ioc_id, caseid)
if link_existed:
errors.append(f"{ioc.ioc_value} (already exists and linked to this case)")
log.error(f"IOC {ioc.ioc_value} already exists and linked to this case")
index += 1
continue
if ioc:
ioc = call_modules_hook('on_postload_ioc_create', data=ioc, caseid=caseid)
ret.append(request_data)
track_activity(f"added ioc \"{ioc.ioc_value}\"", caseid=caseid)
else:
errors.append(f"{ioc.ioc_value} (internal reasons)")
log.error(f"Unable to create IOC {ioc.ioc_value} for internal reasons")
index += 1
if len(errors) == 0:
msg = "Successfully imported data."
else:
msg = "Data is imported but we got errors with the following rows:\n- " + "\n- ".join(errors)
return response_success(msg=msg, data=ret)
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_ioc_blueprint.route('/case/ioc/add/modal', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_add_ioc_modal(caseid):
form = ModalAddCaseIOCForm()
form.ioc_type_id.choices = [(row['type_id'], row['type_name']) for row in get_ioc_types_list()]
form.ioc_tlp_id.choices = get_tlps()
attributes = get_default_custom_attributes('ioc')
return render_template("modal_add_case_ioc.html", form=form, ioc=Ioc(), attributes=attributes)
@case_ioc_blueprint.route('/case/ioc/delete/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_delete_ioc(cur_id, caseid):
call_modules_hook('on_preload_ioc_delete', data=cur_id, caseid=caseid)
ioc = get_ioc(cur_id, caseid)
if not ioc:
return response_error('Not a valid IOC for this case')
if not delete_ioc(ioc, caseid):
track_activity(f"unlinked IOC ID {ioc.ioc_value}", caseid=caseid)
return response_success(f"IOC {cur_id} unlinked")
call_modules_hook('on_postload_ioc_delete', data=cur_id, caseid=caseid)
track_activity(f"deleted IOC \"{ioc.ioc_value}\"", caseid=caseid)
return response_success(f"IOC {cur_id} deleted")
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_view_ioc_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_assets.case_assets', cid=caseid, redirect=True))
form = ModalAddCaseIOCForm()
ioc = get_ioc(cur_id, caseid)
if not ioc:
return response_error("Invalid IOC ID for this case")
form.ioc_type_id.choices = [(row['type_id'], row['type_name']) for row in get_ioc_types_list()]
form.ioc_tlp_id.choices = get_tlps()
# Render the IOC
form.ioc_tags.render_kw = {'value': ioc.ioc_tags}
form.ioc_description.data = ioc.ioc_description
form.ioc_value.data = ioc.ioc_value
comments_map = get_case_iocs_comments_count([cur_id])
return render_template("modal_add_case_ioc.html", form=form, ioc=ioc, attributes=ioc.custom_attributes,
comments_map=comments_map)
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_view_ioc(cur_id, caseid):
ioc_schema = IocSchema()
ioc = get_ioc(cur_id, caseid)
if not ioc:
return response_error("Invalid IOC ID for this case")
return response_success(data=ioc_schema.dump(ioc))
@case_ioc_blueprint.route('/case/ioc/update/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_update_ioc(cur_id, caseid):
try:
ioc = get_ioc(cur_id, caseid)
if not ioc:
return response_error("Invalid IOC ID for this case")
request_data = call_modules_hook('on_preload_ioc_update', data=request.get_json(), caseid=caseid)
# validate before saving
ioc_schema = IocSchema()
request_data['ioc_id'] = cur_id
ioc_sc = ioc_schema.load(request_data, instance=ioc)
ioc_sc.user_id = current_user.id
if not check_ioc_type_id(type_id=ioc_sc.ioc_type_id):
return response_error("Not a valid IOC type")
update_ioc_state(caseid=caseid)
db.session.commit()
ioc_sc = call_modules_hook('on_postload_ioc_update', data=ioc_sc, caseid=caseid)
if ioc_sc:
track_activity(f"updated ioc \"{ioc_sc.ioc_value}\"", caseid=caseid)
return response_success(f"Updated ioc \"{ioc_sc.ioc_value}\"", data=ioc_schema.dump(ioc))
return response_error("Unable to update ioc for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_ioc_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_ioc.case_ioc', cid=caseid, redirect=True))
ioc = get_ioc(cur_id, caseid=caseid)
if not ioc:
return response_error('Invalid ioc ID')
return render_template("modal_conversation.html", element_id=cur_id, element_type='ioc',
title=ioc.ioc_value)
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_ioc_list(cur_id, caseid):
ioc_comments = get_case_ioc_comments(cur_id)
if ioc_comments is None:
return response_error('Invalid ioc ID')
return response_success(data=CommentSchema(many=True).dump(ioc_comments))
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_ioc_add(cur_id, caseid):
try:
ioc = get_ioc(cur_id, caseid=caseid)
if not ioc:
return response_error('Invalid ioc ID')
comment_schema = CommentSchema()
comment = comment_schema.load(request.get_json())
comment.comment_case_id = caseid
comment.comment_user_id = current_user.id
comment.comment_date = datetime.now()
comment.comment_update_date = datetime.now()
db.session.add(comment)
db.session.commit()
add_comment_to_ioc(ioc.ioc_id, comment.comment_id)
db.session.commit()
hook_data = {
"comment": comment_schema.dump(comment),
"ioc": IocSchema().dump(ioc)
}
call_modules_hook('on_postload_ioc_commented', data=hook_data, caseid=caseid)
track_activity(f"ioc \"{ioc.ioc_value}\" commented", caseid=caseid)
return response_success("Event commented", data=comment_schema.dump(comment))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.normalized_messages(), status=400)
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_ioc_get(cur_id, com_id, caseid):
comment = get_case_ioc_comment(cur_id, com_id)
if not comment:
return response_error("Invalid comment ID")
return response_success(data=comment._asdict())
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_ioc_edit(cur_id, com_id, caseid):
return case_comment_update(com_id, 'ioc', caseid)
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_ioc_delete(cur_id, com_id, caseid):
success, msg = delete_ioc_comment(cur_id, com_id)
if not success:
return response_error(msg)
call_modules_hook('on_postload_ioc_comment_delete', data=com_id, caseid=caseid)
track_activity(f"comment {com_id} on ioc {cur_id} deleted", caseid=caseid)
return response_success(msg)

View File

@@ -0,0 +1,511 @@
#!/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 marshmallow
# IMPORTS ------------------------------------------------
from datetime import datetime
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, join_room, leave_room
from flask_wtf import FlaskForm
from app import db, socket_io
from app.blueprints.case.case_comments import case_comment_update
from app.datamgmt.case.case_db import case_get_desc_crc
from app.datamgmt.case.case_db import get_case
from app.datamgmt.case.case_notes_db import add_comment_to_note
from app.datamgmt.case.case_notes_db import add_note
from app.datamgmt.case.case_notes_db import add_note_group
from app.datamgmt.case.case_notes_db import delete_note
from app.datamgmt.case.case_notes_db import delete_note_comment
from app.datamgmt.case.case_notes_db import delete_note_group
from app.datamgmt.case.case_notes_db import find_pattern_in_notes
from app.datamgmt.case.case_notes_db import get_case_note_comment
from app.datamgmt.case.case_notes_db import get_case_note_comments
from app.datamgmt.case.case_notes_db import get_case_notes_comments_count
from app.datamgmt.case.case_notes_db import get_group_details
from app.datamgmt.case.case_notes_db import get_groups_short
from app.datamgmt.case.case_notes_db import get_note
from app.datamgmt.case.case_notes_db import get_notes_from_group
from app.datamgmt.case.case_notes_db import update_note
from app.datamgmt.case.case_notes_db import update_note_group
from app.datamgmt.states import get_notes_state
from app.forms import CaseNoteForm
from app.iris_engine.module_handler.module_handler import call_modules_hook
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import CaseAccessLevel
from app.schema.marshables import CaseAddNoteSchema
from app.schema.marshables import CaseGroupNoteSchema
from app.schema.marshables import CaseNoteSchema
from app.schema.marshables import CommentSchema
from app.util import ac_api_case_requires, ac_socket_requires
from app.util import ac_case_requires
from app.util import response_error
from app.util import response_success
case_notes_blueprint = Blueprint('case_notes',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@case_notes_blueprint.route('/case/notes', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_notes(caseid, url_redir):
if url_redir:
return redirect(url_for('case_notes.case_notes', cid=caseid, redirect=True))
form = FlaskForm()
case = get_case(caseid)
if case:
crc32, desc = case_get_desc_crc(caseid)
else:
crc32 = None
desc = None
return render_template('case_notes.html', case=case, form=form, th_desc=desc, crc=crc32)
@case_notes_blueprint.route('/case/notes/<int:cur_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_note_detail(cur_id, caseid):
try:
note = get_note(cur_id, caseid=caseid)
if not note:
return response_error(msg="Invalid note ID")
note_schema = CaseNoteSchema()
return response_success(data=note_schema.dump(note))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_notes_blueprint.route('/case/notes/<int:cur_id>/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_note_detail_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_notes.case_notes', cid=caseid, redirect=True))
form = CaseNoteForm()
note = get_note(cur_id, caseid)
ca = None
if note:
form.content = note.note_content
form.title = note.note_title
form.note_title.render_kw = {"value": note.note_title}
setattr(form, 'note_id', note.note_id)
setattr(form, 'note_uuid', note.note_uuid)
ca = note.custom_attributes
comments_map = get_case_notes_comments_count([cur_id])
return render_template("modal_note_edit.html", note=form, id=cur_id, attributes=ca,
ncid=note.note_case_id, comments_map=comments_map)
return response_error(f'Unable to find note ID {cur_id} for case {caseid}')
@case_notes_blueprint.route('/case/notes/delete/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_note_delete(cur_id, caseid):
call_modules_hook('on_preload_note_delete', data=cur_id, caseid=caseid)
note = get_note(cur_id, caseid)
if not note:
return response_error("Invalid note ID for this case")
try:
delete_note(cur_id, caseid)
except Exception as e:
return response_error("Unable to remove note", data=e.__traceback__)
call_modules_hook('on_postload_note_delete', data=cur_id, caseid=caseid)
track_activity(f"deleted note \"{note.note_title}\"", caseid=caseid)
return response_success(f"Note deleted {cur_id}")
@case_notes_blueprint.route('/case/notes/update/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_note_save(cur_id, caseid):
try:
# validate before saving
addnote_schema = CaseAddNoteSchema()
request_data = call_modules_hook('on_preload_note_update', data=request.get_json(), caseid=caseid)
request_data['note_id'] = cur_id
addnote_schema.load(request_data, partial=['group_id'])
note = update_note(note_content=request_data.get('note_content'),
note_title=request_data.get('note_title'),
update_date=datetime.utcnow(),
user_id=current_user.id,
note_id=cur_id,
caseid=caseid
)
if not note:
return response_error("Invalid note ID for this case")
note = call_modules_hook('on_postload_note_update', data=note, caseid=caseid)
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
track_activity(f"updated note \"{note.note_title}\"", caseid=caseid)
return response_success(f"Note ID {cur_id} saved", data=addnote_schema.dump(note))
@case_notes_blueprint.route('/case/notes/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_note_add(caseid):
try:
# validate before saving
addnote_schema = CaseAddNoteSchema()
request_data = call_modules_hook('on_preload_note_create', data=request.get_json(), caseid=caseid)
addnote_schema.verify_group_id(request_data, caseid=caseid)
addnote_schema.load(request_data)
note = add_note(request_data.get('note_title'),
datetime.utcnow(),
current_user.id,
caseid,
request_data.get('group_id'),
note_content=request_data.get('note_content'))
note = call_modules_hook('on_postload_note_create', data=note, caseid=caseid)
if note:
casenote_schema = CaseNoteSchema()
track_activity(f"added note \"{note.note_title}\"", caseid=caseid)
return response_success('Note added', data=casenote_schema.dump(note))
return response_error("Unable to create note for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_notes_blueprint.route('/case/notes/groups/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_load_notes_groups(caseid):
if not get_case(caseid=caseid):
return response_error("Invalid case ID")
groups_short = get_groups_short(caseid)
sta = []
for group in groups_short:
notes = get_notes_from_group(caseid=caseid, group_id=group.group_id)
group_d = group._asdict()
group_d['notes'] = [note._asdict() for note in notes]
sta.append(group_d)
sta = sorted(sta, key=lambda i: i['group_id'])
ret = {
'groups': sta,
'state': get_notes_state(caseid=caseid)
}
return response_success("", data=ret)
@case_notes_blueprint.route('/case/notes/state', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_notes_state(caseid):
os = get_notes_state(caseid=caseid)
if os:
return response_success(data=os)
else:
return response_error('No notes state for this case.')
@case_notes_blueprint.route('/case/notes/search', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_search_notes(caseid):
if request.is_json:
search = request.json.get('search_term')
ns = []
if search:
search = "%{}%".format(search)
ns = find_pattern_in_notes(search, caseid)
ns = [row._asdict() for row in ns]
return response_success("", data=ns)
return response_error("Invalid request")
@case_notes_blueprint.route('/case/notes/groups/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_add_notes_groups(caseid):
title = ''
if request.is_json:
title = request.json.get('group_title') or ''
ng = add_note_group(group_title=title,
caseid=caseid,
userid=current_user.id,
creationdate=datetime.utcnow())
if ng.group_id:
group_schema = CaseGroupNoteSchema()
track_activity(f"added group note \"{ng.group_title}\"", caseid=caseid)
return response_success("Notes group added", data=group_schema.dump(ng))
else:
return response_error("Unable to add a new group")
return response_error("Invalid request")
@case_notes_blueprint.route('/case/notes/groups/delete/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_delete_notes_groups(cur_id, caseid):
if not delete_note_group(cur_id, caseid):
return response_error("Invalid group ID")
track_activity("deleted group note ID {}".format(cur_id), caseid=caseid)
return response_success("Group ID {} deleted".format(cur_id))
@case_notes_blueprint.route('/case/notes/groups/<int:cur_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_get_notes_group(cur_id, caseid):
group = get_group_details(cur_id, caseid)
if not group:
return response_error(f"Group ID {cur_id} not found")
return response_success("", data=group)
@case_notes_blueprint.route('/case/notes/groups/update/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_edit_notes_groups(cur_id, caseid):
js_data = request.get_json()
if not js_data:
return response_error("Invalid data")
group_title = js_data.get('group_title')
if not group_title:
return response_error("Missing field group_title")
ng = update_note_group(group_title, cur_id, caseid)
if ng:
# Note group has been properly found and updated in db
track_activity("updated group note \"{}\"".format(group_title), caseid=caseid)
group_schema = CaseGroupNoteSchema()
return response_success("Updated title of group ID {}".format(cur_id), data=group_schema.dump(ng))
return response_error("Group ID {} not found".format(cur_id))
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_note_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_note.case_note', cid=caseid, redirect=True))
note = get_note(cur_id, caseid=caseid)
if not note:
return response_error('Invalid note ID')
return render_template("modal_conversation.html", element_id=cur_id, element_type='notes',
title=note.note_title)
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_note_list(cur_id, caseid):
note_comments = get_case_note_comments(cur_id)
if note_comments is None:
return response_error('Invalid note ID')
return response_success(data=CommentSchema(many=True).dump(note_comments))
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_note_add(cur_id, caseid):
try:
note = get_note(cur_id, caseid=caseid)
if not note:
return response_error('Invalid note ID')
comment_schema = CommentSchema()
comment = comment_schema.load(request.get_json())
comment.comment_case_id = caseid
comment.comment_user_id = current_user.id
comment.comment_date = datetime.now()
comment.comment_update_date = datetime.now()
db.session.add(comment)
db.session.commit()
add_comment_to_note(note.note_id, comment.comment_id)
db.session.commit()
hook_data = {
"comment": comment_schema.dump(comment),
"note": CaseNoteSchema().dump(note)
}
call_modules_hook('on_postload_note_commented', data=hook_data, caseid=caseid)
track_activity("note \"{}\" commented".format(note.note_title), caseid=caseid)
return response_success("Event commented", data=comment_schema.dump(comment))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.normalized_messages(), status=400)
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_note_get(cur_id, com_id, caseid):
comment = get_case_note_comment(cur_id, com_id)
if not comment:
return response_error("Invalid comment ID")
return response_success(data=comment._asdict())
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_note_edit(cur_id, com_id, caseid):
return case_comment_update(com_id, 'notes', caseid)
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_note_delete(cur_id, com_id, caseid):
success, msg = delete_note_comment(cur_id, com_id)
if not success:
return response_error(msg)
call_modules_hook('on_postload_note_comment_delete', data=com_id, caseid=caseid)
track_activity(f"comment {com_id} on note {cur_id} deleted", caseid=caseid)
return response_success(msg)
@socket_io.on('change-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_change_note(data):
data['last_change'] = current_user.user
emit('change-note', data, to=data['channel'], skip_sid=request.sid, room=data['channel'])
@socket_io.on('save-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_save_note(data):
data['last_saved'] = current_user.user
emit('save-note', data, to=data['channel'], skip_sid=request.sid, room=data['channel'])
@socket_io.on('clear_buffer-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_clear_buffer_note(message):
emit('clear_buffer-note', message, room=message['channel'])
@socket_io.on('join-notes')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_join_note(data):
room = data['channel']
join_room(room=room)
emit('join-notes', {
'message': f"{current_user.user} just joined",
"user": current_user.user
}, room=room)
@socket_io.on('ping-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_ping_note(data):
emit('ping-note', {"user": current_user.name, "note_id": data['note_id']}, room=data['channel'])
@socket_io.on('pong-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_ping_note(data):
emit('pong-note', {"user": current_user.name, "note_id": data['note_id']}, room=data['channel'])
@socket_io.on('overview-map-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_overview_map_note(data):
emit('overview-map-note', {"user": current_user.user, "note_id": data['note_id']}, room=data['channel'])
@socket_io.on('join-notes-overview')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_join_overview(data):
room = data['channel']
join_room(room=room)
emit('join-notes-overview', {
'message': f"{current_user.user} just joined",
"user": current_user.user
}, room=room)
@socket_io.on('disconnect')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_disconnect(data):
emit('disconnect', current_user.user, broadcast=True)

View File

@@ -0,0 +1,306 @@
#!/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.
# IMPORTS ------------------------------------------------
from datetime import datetime
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 app import db
from app.blueprints.case.case_comments import case_comment_update
from app.datamgmt.case.case_db import get_case
from app.datamgmt.case.case_rfiles_db import add_comment_to_evidence
from app.datamgmt.case.case_rfiles_db import add_rfile
from app.datamgmt.case.case_rfiles_db import delete_evidence_comment
from app.datamgmt.case.case_rfiles_db import delete_rfile
from app.datamgmt.case.case_rfiles_db import get_case_evidence_comment
from app.datamgmt.case.case_rfiles_db import get_case_evidence_comments
from app.datamgmt.case.case_rfiles_db import get_case_evidence_comments_count
from app.datamgmt.case.case_rfiles_db import get_rfile
from app.datamgmt.case.case_rfiles_db import get_rfiles
from app.datamgmt.case.case_rfiles_db import update_rfile
from app.datamgmt.manage.manage_attribute_db import get_default_custom_attributes
from app.datamgmt.states import get_evidences_state
from app.iris_engine.module_handler.module_handler import call_modules_hook
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import CaseAccessLevel
from app.schema.marshables import CaseEvidenceSchema
from app.schema.marshables import CommentSchema
from app.util import ac_api_case_requires
from app.util import ac_case_requires
from app.util import response_error
from app.util import response_success
case_rfiles_blueprint = Blueprint(
'case_rfiles',
__name__,
template_folder='templates'
)
# CONTENT ------------------------------------------------
@case_rfiles_blueprint.route('/case/evidences', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_rfile(caseid, url_redir):
if url_redir:
return redirect(url_for('case_rfiles.case_rfile', cid=caseid, redirect=True))
form = FlaskForm()
case = get_case(caseid)
return render_template("case_rfile.html", case=case, form=form)
@case_rfiles_blueprint.route('/case/evidences/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_list_rfiles(caseid):
crf = get_rfiles(caseid)
ret = {
"evidences": [row._asdict() for row in crf],
"state": get_evidences_state(caseid=caseid)
}
return response_success("", data=ret)
@case_rfiles_blueprint.route('/case/evidences/state', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_rfiles_state(caseid):
os = get_evidences_state(caseid=caseid)
if os:
return response_success(data=os)
else:
return response_error('No evidences state for this case.')
@case_rfiles_blueprint.route('/case/evidences/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_add_rfile(caseid):
try:
# validate before saving
evidence_schema = CaseEvidenceSchema()
request_data = call_modules_hook('on_preload_evidence_create', data=request.get_json(), caseid=caseid)
evidence = evidence_schema.load(request_data)
crf = add_rfile(evidence=evidence,
user_id=current_user.id,
caseid=caseid
)
crf = call_modules_hook('on_postload_evidence_create', data=crf, caseid=caseid)
if crf:
track_activity(f"added evidence \"{crf.filename}\"", caseid=caseid)
return response_success("Evidence added", data=evidence_schema.dump(crf))
return response_error("Unable to create task for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_get_evidence(cur_id, caseid):
crf = get_rfile(cur_id, caseid)
if not crf:
return response_error("Invalid evidence ID for this case")
evidence_schema = CaseEvidenceSchema()
return response_success(data=evidence_schema.dump(crf))
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_edit_rfile_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_rfiles.case_rfile', cid=caseid, redirect=True))
crf = get_rfile(cur_id, caseid)
if not crf:
return response_error("Invalid evidence ID for this case")
comments_map = get_case_evidence_comments_count([cur_id])
return render_template("modal_add_case_rfile.html", rfile=crf, attributes=crf.custom_attributes,
comments_map=comments_map)
@case_rfiles_blueprint.route('/case/evidences/add/modal', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_add_rfile_modal(caseid):
return render_template("modal_add_case_rfile.html", rfile=None, attributes=get_default_custom_attributes('evidence'))
@case_rfiles_blueprint.route('/case/evidences/update/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_edit_rfile(cur_id, caseid):
try:
# validate before saving
evidence_schema = CaseEvidenceSchema()
request_data = call_modules_hook('on_preload_evidence_update', data=request.get_json(), caseid=caseid)
crf = get_rfile(cur_id, caseid)
if not crf:
return response_error("Invalid evidence ID for this case")
request_data['id'] = cur_id
evidence = evidence_schema.load(request_data, instance=crf)
evd = update_rfile(evidence=evidence,
user_id=current_user.id,
caseid=caseid
)
evd = call_modules_hook('on_postload_evidence_update', data=evd, caseid=caseid)
if evd:
track_activity(f"updated evidence \"{evd.filename}\"", caseid=caseid)
return response_success("Evidence {} updated".format(evd.filename), data=evidence_schema.dump(evd))
return response_error("Unable to update task for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_rfiles_blueprint.route('/case/evidences/delete/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_delete_rfile(cur_id, caseid):
call_modules_hook('on_preload_evidence_delete', data=cur_id, caseid=caseid)
crf = get_rfile(cur_id, caseid)
if not crf:
return response_error("Invalid evidence ID for this case")
delete_rfile(cur_id, caseid=caseid)
call_modules_hook('on_postload_evidence_delete', data=cur_id, caseid=caseid)
track_activity(f"deleted evidence \"{crf.filename}\" from registry", caseid)
return response_success("Evidence deleted")
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_evidence_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_task.case_task', cid=caseid, redirect=True))
evidence = get_rfile(cur_id, caseid=caseid)
if not evidence:
return response_error('Invalid evidence ID')
return render_template("modal_conversation.html", element_id=cur_id, element_type='evidences',
title=evidence.filename)
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_evidence_list(cur_id, caseid):
evidence_comments = get_case_evidence_comments(cur_id)
if evidence_comments is None:
return response_error('Invalid evidence ID')
return response_success(data=CommentSchema(many=True).dump(evidence_comments))
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_evidence_add(cur_id, caseid):
try:
evidence = get_rfile(cur_id, caseid=caseid)
if not evidence:
return response_error('Invalid evidence ID')
comment_schema = CommentSchema()
comment = comment_schema.load(request.get_json())
comment.comment_case_id = caseid
comment.comment_user_id = current_user.id
comment.comment_date = datetime.now()
comment.comment_update_date = datetime.now()
db.session.add(comment)
db.session.commit()
add_comment_to_evidence(evidence.id, comment.comment_id)
db.session.commit()
hook_data = {
"comment": comment_schema.dump(comment),
"evidence": CaseEvidenceSchema().dump(evidence)
}
call_modules_hook('on_postload_evidence_commented', data=hook_data, caseid=caseid)
track_activity(f"evidence \"{evidence.filename}\" commented", caseid=caseid)
return response_success("Event commented", data=comment_schema.dump(comment))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.normalized_messages(), status=400)
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_evidence_get(cur_id, com_id, caseid):
comment = get_case_evidence_comment(cur_id, com_id)
if not comment:
return response_error("Invalid comment ID")
return response_success(data=comment._asdict())
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_evidence_edit(cur_id, com_id, caseid):
return case_comment_update(com_id, 'tasks', caseid)
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_evidence_delete(cur_id, com_id, caseid):
success, msg = delete_evidence_comment(cur_id, com_id)
if not success:
return response_error(msg)
call_modules_hook('on_postload_evidence_comment_delete', data=com_id, caseid=caseid)
track_activity(f"comment {com_id} on evidence {cur_id} deleted", caseid=caseid)
return response_success(msg)

View File

@@ -0,0 +1,449 @@
#!/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))

View File

@@ -0,0 +1,368 @@
#!/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.
# IMPORTS ------------------------------------------------
from datetime import datetime
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 app import db
from app.blueprints.case.case_comments import case_comment_update
from app.datamgmt.case.case_db import get_case
from app.datamgmt.case.case_tasks_db import add_comment_to_task
from app.datamgmt.case.case_tasks_db import add_task
from app.datamgmt.case.case_tasks_db import delete_task
from app.datamgmt.case.case_tasks_db import delete_task_comment
from app.datamgmt.case.case_tasks_db import get_case_task_comment
from app.datamgmt.case.case_tasks_db import get_case_task_comments
from app.datamgmt.case.case_tasks_db import get_case_tasks_comments_count
from app.datamgmt.case.case_tasks_db import get_task
from app.datamgmt.case.case_tasks_db import get_task_with_assignees
from app.datamgmt.case.case_tasks_db import get_tasks_status
from app.datamgmt.case.case_tasks_db import get_tasks_with_assignees
from app.datamgmt.case.case_tasks_db import update_task_assignees
from app.datamgmt.case.case_tasks_db import update_task_status
from app.datamgmt.manage.manage_attribute_db import get_default_custom_attributes
from app.datamgmt.states import get_tasks_state
from app.datamgmt.states import update_tasks_state
from app.forms import CaseTaskForm
from app.iris_engine.module_handler.module_handler import call_modules_hook
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import CaseAccessLevel
from app.models.authorization import User
from app.models.models import CaseTasks
from app.schema.marshables import CaseTaskSchema
from app.schema.marshables import CommentSchema
from app.util import ac_api_case_requires
from app.util import ac_case_requires
from app.util import response_error
from app.util import response_success
case_tasks_blueprint = Blueprint('case_tasks',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@case_tasks_blueprint.route('/case/tasks', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_tasks(caseid, url_redir):
if url_redir:
return redirect(url_for('case_tasks.case_tasks', cid=caseid, redirect=True))
form = FlaskForm()
case = get_case(caseid)
return render_template("case_tasks.html", case=case, form=form)
@case_tasks_blueprint.route('/case/tasks/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_get_tasks(caseid):
ct = get_tasks_with_assignees(caseid)
if not ct:
output = []
else:
output = ct
ret = {
"tasks_status": get_tasks_status(),
"tasks": output,
"state": get_tasks_state(caseid=caseid)
}
return response_success("", data=ret)
@case_tasks_blueprint.route('/case/tasks/state', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_get_tasks_state(caseid):
os = get_tasks_state(caseid=caseid)
if os:
return response_success(data=os)
else:
return response_error('No tasks state for this case.')
@case_tasks_blueprint.route('/case/tasks/status/update/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_task_statusupdate(cur_id, caseid):
task = get_task(task_id=cur_id, caseid=caseid)
if not task:
return response_error("Invalid task ID for this case")
if request.is_json:
if update_task_status(request.json.get('task_status_id'), cur_id, caseid):
task_schema = CaseTaskSchema()
return response_success("Task status updated", data=task_schema.dump(task))
else:
return response_error("Invalid status")
else:
return response_error("Invalid request")
@case_tasks_blueprint.route('/case/tasks/add/modal', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_add_task_modal(caseid):
task = CaseTasks()
task.custom_attributes = get_default_custom_attributes('task')
form = CaseTaskForm()
form.task_status_id.choices = [(a.id, a.status_name) for a in get_tasks_status()]
form.task_assignees_id.choices = []
return render_template("modal_add_case_task.html", form=form, task=task, uid=current_user.id, user_name=None,
attributes=task.custom_attributes)
@case_tasks_blueprint.route('/case/tasks/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_add_task(caseid):
try:
# validate before saving
task_schema = CaseTaskSchema()
request_data = call_modules_hook('on_preload_task_create', data=request.get_json(), caseid=caseid)
if 'task_assignee_id' in request_data or 'task_assignees_id' not in request_data:
return response_error('task_assignee_id is not valid anymore since v1.5.0')
task_assignee_list = request_data['task_assignees_id']
del request_data['task_assignees_id']
task = task_schema.load(request_data)
ctask = add_task(task=task,
assignee_id_list=task_assignee_list,
user_id=current_user.id,
caseid=caseid
)
ctask = call_modules_hook('on_postload_task_create', data=ctask, caseid=caseid)
if ctask:
track_activity(f"added task \"{ctask.task_title}\"", caseid=caseid)
return response_success("Task '{}' added".format(ctask.task_title), data=task_schema.dump(ctask))
return response_error("Unable to create task for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_task_view(cur_id, caseid):
task = get_task_with_assignees(task_id=cur_id, case_id=caseid)
if not task:
return response_error("Invalid task ID for this case")
task_schema = CaseTaskSchema()
return response_success(data=task_schema.dump(task))
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_task_view_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_tasks.case_tasks', cid=caseid, redirect=True))
form = CaseTaskForm()
task = get_task_with_assignees(task_id=cur_id, case_id=caseid)
form.task_status_id.choices = [(a.id, a.status_name) for a in get_tasks_status()]
form.task_assignees_id.choices = []
if not task:
return response_error("Invalid task ID for this case")
form.task_title.render_kw = {'value': task.task_title}
form.task_description.data = task.task_description
user_name, = User.query.with_entities(User.name).filter(User.id == task.task_userid_update).first()
comments_map = get_case_tasks_comments_count([task.id])
return render_template("modal_add_case_task.html", form=form, task=task, user_name=user_name,
comments_map=comments_map, attributes=task.custom_attributes)
@case_tasks_blueprint.route('/case/tasks/update/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_edit_task(cur_id, caseid):
try:
task = get_task_with_assignees(task_id=cur_id, case_id=caseid)
if not task:
return response_error("Invalid task ID for this case")
request_data = call_modules_hook('on_preload_task_update', data=request.get_json(), caseid=caseid)
if 'task_assignee_id' in request_data or 'task_assignees_id' not in request_data:
return response_error('task_assignee_id is not valid anymore since v1.5.0')
# validate before saving
task_assignee_list = request_data['task_assignees_id']
del request_data['task_assignees_id']
task_schema = CaseTaskSchema()
request_data['id'] = cur_id
task = task_schema.load(request_data, instance=task)
task.task_userid_update = current_user.id
task.task_last_update = datetime.utcnow()
update_task_assignees(task, task_assignee_list, caseid)
update_tasks_state(caseid=caseid)
db.session.commit()
task = call_modules_hook('on_postload_task_update', data=task, caseid=caseid)
if task:
track_activity(f"updated task \"{task.task_title}\" (status {task.task_status_id})",
caseid=caseid)
return response_success("Task '{}' updated".format(task.task_title), data=task_schema.dump(task))
return response_error("Unable to update task for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_tasks_blueprint.route('/case/tasks/delete/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_delete_task(cur_id, caseid):
call_modules_hook('on_preload_task_delete', data=cur_id, caseid=caseid)
task = get_task_with_assignees(task_id=cur_id, case_id=caseid)
if not task:
return response_error("Invalid task ID for this case")
delete_task(task.id)
update_tasks_state(caseid=caseid)
call_modules_hook('on_postload_task_delete', data=cur_id, caseid=caseid)
track_activity(f"deleted task \"{task.task_title}\"")
return response_success("Task deleted")
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_task_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_task.case_task', cid=caseid, redirect=True))
task = get_task(cur_id, caseid=caseid)
if not task:
return response_error('Invalid task ID')
return render_template("modal_conversation.html", element_id=cur_id, element_type='tasks',
title=task.task_title)
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_task_list(cur_id, caseid):
task_comments = get_case_task_comments(cur_id)
if task_comments is None:
return response_error('Invalid task ID')
return response_success(data=CommentSchema(many=True).dump(task_comments))
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_task_add(cur_id, caseid):
try:
task = get_task(cur_id, caseid=caseid)
if not task:
return response_error('Invalid task ID')
comment_schema = CommentSchema()
comment = comment_schema.load(request.get_json())
comment.comment_case_id = caseid
comment.comment_user_id = current_user.id
comment.comment_date = datetime.now()
comment.comment_update_date = datetime.now()
db.session.add(comment)
db.session.commit()
add_comment_to_task(task.id, comment.comment_id)
db.session.commit()
hook_data = {
"comment": comment_schema.dump(comment),
"task": CaseTaskSchema().dump(task)
}
call_modules_hook('on_postload_task_commented', data=hook_data, caseid=caseid)
track_activity(f"task \"{task.task_title}\" commented", caseid=caseid)
return response_success("Task commented", data=comment_schema.dump(comment))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.normalized_messages(), status=400)
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_task_get(cur_id, com_id, caseid):
comment = get_case_task_comment(cur_id, com_id)
if not comment:
return response_error("Invalid comment ID")
return response_success(data=comment._asdict())
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_task_edit(cur_id, com_id, caseid):
return case_comment_update(com_id, 'tasks', caseid)
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_task_delete(cur_id, com_id, caseid):
success, msg = delete_task_comment(cur_id, com_id)
if not success:
return response_error(msg)
call_modules_hook('on_postload_task_comment_delete', data=com_id, caseid=caseid)
track_activity(f"comment {com_id} on task {cur_id} deleted", caseid=caseid)
return response_success(msg)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
<nav class="nav-bottom rounded" id="h_nav_tab">
<div class="container-fluid" >
<ul class="navbar-nav rounded" style="background-color:#4c72a130;">
<li class="nav-item {{ active if page == 'case' }}">
<a class="nav-link" href="/case?cid={{session['current_case'].case_id}}">
<span class="menu-title">Summary</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/case/notes?cid={{session['current_case'].case_id}}">
<span class="menu-title">Notes</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/case/assets?cid={{session['current_case'].case_id}}">
<span class="menu-title">Assets</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/case/ioc?cid={{session['current_case'].case_id}}">
<span class="menu-title">IOC</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/case/timeline?cid={{session['current_case'].case_id}}">
<span class="menu-title">Timeline</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/case/graph?cid={{session['current_case'].case_id}}">
<span class="menu-title">Graph</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/case/tasks?cid={{session['current_case'].case_id}}">
<span class="menu-title">Tasks</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/case/evidences?cid={{session['current_case'].case_id}}">
<span class="menu-title">Evidences</span>
</a>
</li>
</ul>
</div>
</nav>

View File

@@ -0,0 +1,51 @@
<div class="secondary-header">
<div class="nav-bottom ">
<div class="container-fluid float-left">
<div class="row">
<div class="col col-md-ml-12 col-sm-12">
<ul class="nav page-navigation page-navigation-style-2 page-navigation-primary">
<li class="nav-item {{ active if page == 'case' }}">
<button class="btn btn-sm btn-light" href="#" onclick="case_detail('{{ case.case_id }}');">
<i class="fa-solid fa-gear mr-1"></i>
<span class="">Manage</span>
</button>
</li>
<li class="nav-item">
<div class="dropdown">
<button class="btn btn-sm btn-light float-left ml-2" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span aria-hidden="true"><i class="fa-solid fa-bolt mr-1"></i> Processors</span>
</button>
<div class="dropdown-menu pull-right" id="case_modal_quick_actions" aria-labelledby="dropdownMenuButton">
</div>
</div>
</li>
<li class="nav-item">
<button class="btn btn-sm btn-light float-left ml-2" id="case_pipeline" onclick="case_pipeline_popup();">
<span aria-hidden="true"><i class="fa-solid fa-upload mr-1"></i>Pipelines</span>
</button>
</li>
<li class="nav-item ml-auto">
{% if case.review_status.status_name == "Not reviewed" or not case.review_status %}
<button class="btn btn-sm btn-light float-left ml-2" id="request_review">
<span aria-hidden="true"><i class="fa-solid fa-clipboard-check mr-1"></i>Request review</span>
</button>
{% endif %}
<btn href="#" onclick="report_template_selector();" class="btn btn-dark btn-sm float-right ml-2">
<span class="btn-label">
<i class="fa fa-file-arrow-down"></i>
</span>
Generate report
</btn>
<btn href="#" onclick="act_report_template_selector();" class="btn btn-sm btn-dark float-right ml-2">
<span class="btn-label">
<i class="fa fa-chart-line"></i>
</span>
Activity report
</btn>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,337 @@
{% extends "layouts/default_ext.html" %} {% block title %} Case summary {% endblock title %}
{% block stylesheets %}
{% include 'includes/header_case.html' %}
<link rel="stylesheet" href="/static/assets/css/select2.css">
<link rel="stylesheet" href="/static/assets/css/bootstrap-multiselect.min.css">
<link rel="stylesheet" href="/static/assets/css/bootstrap-select.min.css">
{% endblock stylesheets %}
{% block content %}
{% if current_user.is_authenticated %}
{% include 'includes/navigation_ext.html' %}
{% include 'includes/sidenav.html' %}
<div class="main-panel">
<div class="content">
{% if case.close_date %}
<div class="panel-header bg-close-gradient">
{% else %}
<div class="panel-header bg-info-gradient">
{% endif %}
<div class="page-inner py-5">
<div class="d-flex align-items-left align-items-md-center flex-column flex-md-row mt--3">
<div class="col">
<div class="row">
<h2 class="text-white pb-2 fw-bold case-name"> <i class="icon-big flaticon-network mr-2"></i> {{ case.name|unquote }}
</h2>
</div>
<h5 class="text-white op-7 mb-1"><b>Open on</b> {{ case.open_date }} by {{ case.user.name }}</h5>
<h5 class="text-white op-7 mb-3"><b>Owned by</b> {{ case.owner.name }}</h5>
{% if case.close_date %}
<h5 class="text-warning mb-1">Closed on {{ case.close_date }}</h5>
{% endif %}
</div>
<div class="col mt-4">
<div class="row">
<span title="Case outcome" class="float-right btn btn-rounded badge-pill hidden-caret ml-auto btn-xs mr-2 mb-2 {% if case.status_id == 1%}badge-success{% elif case.status_id == 2 %}badge-danger{% else %}btn-light{% endif %}"
onclick="case_detail('{{ case.case_id }}', true);"
><i class="fa-solid fa-group-arrows-rotate mr-2"></i>{{ case.status_name }}</span>
</div>
<div class="row">
<div class="ml-auto">
<div class="row">
<h5 class="text-white op-7 mb-2 float-right mr-4"><b>Customer</b> : {{ case.client.name }}</h5>
</div>
<div class="row">
{% if case.soc_id %} <h5 class="text-white op-7 mb-2 mr-4"><b>SOC ID :</b> {{ case.soc_id }}</h5> {% endif %}
</div>
</div>
</div>
</div>
</div>
<div class="row mt-2 mb--2">
<div class="ml-2 col">
<div class="row ml-1">
{% if case.state %}<h5 title="Case state" onclick="case_detail('{{ case.case_id }}', true);" style="cursor:pointer;"><span class="btn-rounded badge-pill hidden-caret btn-sm btn-light"><i class="fa-solid fa-business-time mr-1"></i> {{ case.state.state_name }}</span></h5>{% endif %}
{% if case.classification %}<h5 title="Classification" onclick="case_detail('{{ case.case_id }}', true);" style="cursor:pointer;"><span class="btn-rounded badge-pill hidden-caret btn-sm btn-light ml-2"><i class="fa-solid fa-shield-virus mr-1"></i>{{ case.classification.name_expanded }}</span></h5>{% endif %}
{% if case.alerts| length > 0 %}<h5 title="Alerts"><a class="btn-rounded badge-pill hidden-caret btn-sm btn-dark ml-2 badge-warning" href="/alerts?cid={{ case.case_id }}&sort=desc&case_id={{ case.case_id }}" target="_blank" rel="noopener"><i class="fa-solid fa-bell mr-1"></i> {{ case.alerts| length }} related alerts</a></h5>{% endif %}
{% if case.review_status.status_name == "Reviewed" %}
<h5 title="Reviewed"> <a class="text-white btn-rounded badge-pill hidden-caret btn-sm ml-2 badge-success"><i class="fa-regular fa-circle-check mr-2"></i>Case reviewed by {% if case.reviewer.id == current_user.id %} you {% else %} {{ case.reviewer.name }} {% endif %}</a></h5>
{% endif %}
</div>
</div>
<div class="col mr-2">
{% if case.case_tags %}
{% for tag in case.case_tags %}
<span class="badge badge-pill badge-light ml-1 pull-right"><i class="fa fa-tag mr-1"></i> {{ tag }}</span>
{% endfor %}
{% endif %}
</div>
</div>
</div>
</div>
<div class="page-inner mt--5">
<div class="row row-card-no-pd" style="padding-top: 0px;padding-bottom: 3px;">
{% include 'case-nav_landing.html' %}
</div>
<div id="caseReviewState" data-review-state="{{ case.review_status.status_name }}" data-reviewer-id="{{ case.reviewer_id }}" data-reviewer-name="{{ case.reviewer.name }}" style="display: none;"></div>
{% if case.reviewer_id == current_user.id and case.review_status.status_name != "Reviewed" and case.review_status.status_name != "Not reviewed" %}
<div class="row row-card-no-pd review-card mt--3 mb--3 bg-warning-gradient" style="display: none;">
<div class="col-md-12">
<h4 class="font-weight-bold"><i class="fa-solid fa-triangle-exclamation text-danger ml-2 mr-2"></i>Review requested
<button class="btn btn-sm float-right btn-dark mr-3 mt-2 btn-start-review">Start review</button>
<button class="btn btn-sm float-right btn-success mr-3 mt-2 btn-confirm-review" style="display:none">Confirm review</button>
<button class="btn btn-sm float-right btn-light mr-3 mt-2 btn-cancel-review" style="display:none">Cancel review</button></h4>
<span class="ml-2" id="reviewSubtitle">You have been requested to review this case.</span>
</div>
</div>
{% elif case.review_status.status_name == "Review in progress" %}
<div class="row row-card-no-pd mt--3 mb--3 bg-warning-gradient">
<div class="col-md-12">
<h4 class="font-weight-bold mt-1"><i class="fa-solid fa-list-check ml-2 mr-2"></i>Review by {{ case.reviewer.name }} in progress</h4>
</div>
</div>
{% elif case.review_status.status_name == "Pending review" %}
<div class="row row-card-no-pd mt--3 mb--3 bg-warning-gradient">
<div class="col-md-12">
<h4 class="font-weight-bold mt-1"><i class="fa-solid fa-triangle-exclamation text-danger ml-2 mr-2"></i>Review by {{ case.reviewer.name }} pending</h4>
</div>
</div>
{% endif %}
<div class="row row-card-no-pd">
<div class="col-md-12">
<div class="card mb-4" id="rescard1">
<div class="card-header">
<div class="row">
{{ form.hidden_tag() }}
<a href="#case_summary_card" class="d-block nav-link mr-auto" data-toggle="collapse" aria-expanded="true" aria-controls="case_summary_card">
<h4 class="m-0 font-weight-bold">Case summary {{ "(Syncing with DB )" if case.id }}</h4>
</a>
<div class="mr-0 float-right">
<small id="content_typing" class="mr-3 mt-1"></small>
<small id="content_last_saved_by" class="mr-3 mt-1"></small>
<span id="last_saved" class="badge mr-3 ml-2"></span>
<small id="content_last_sync"></small>
<button class="btn btn-sm mr-2 ml-3" onclick="edit_case_summary();" id="sum_edit_btn" >Edit</button>
<button type="button" id="sum_refresh_btn" class="btn btn-sm btn-outline-default mr-3" onclick="sync_editor();">
Refresh
</button>
</div>
</div>
</div>
<div class="collapsed" id="case_summary_card">
<div class="card-body">
<div class="row mb-1">
<div class="col" id="summary_edition_btn" style="display:none;">
<div class="btn btn-sm btn-light mr-1 " title="CTRL-B" onclick="editor.insertSnippet('**${1:$SELECTION}**');editor.focus();"><i class="fa-solid fa-bold"></i></div>
<div class="btn btn-sm btn-light mr-1" title="CTRL-I" onclick="editor.insertSnippet('*${1:$SELECTION}*');editor.focus();"><i class="fa-solid fa-italic"></i></div>
<div class="btn btn-sm btn-light mr-1" title="CTRL-SHIFT-1" onclick="editor.insertSnippet('# ${1:$SELECTION}');editor.focus();">H1</div>
<div class="btn btn-sm btn-light mr-1" title="CTRL-SHIFT-2" onclick="editor.insertSnippet('## ${1:$SELECTION}');editor.focus();">H2</div>
<div class="btn btn-sm btn-light mr-1" title="CTRL-SHIFT-3" onclick="editor.insertSnippet('### ${1:$SELECTION}');editor.focus();">H3</div>
<div class="btn btn-sm btn-light mr-1" title="CTRL-SHIFT-4" onclick="editor.insertSnippet('#### ${1:$SELECTION}');editor.focus();">H4</div>
<div class="btn btn-sm btn-light mr-1" title="Insert code" onclick="editor.insertSnippet('```${1:$SELECTION}```');editor.focus();"><i class="fa-solid fa-code"></i></div>
<div class="btn btn-sm btn-light mr-1" title="Insert link" onclick="editor.insertSnippet('[${1:$SELECTION}](url)');editor.focus();"><i class="fa-solid fa-link"></i></div>
<div class="btn btn-sm btn-light mr-1" title="Insert table" onclick="editor.insertSnippet('|\t|\t|\t|\n|--|--|--|\n|\t|\t|\t|\n|\t|\t|\t|');editor.focus();"><i class="fa-solid fa-table"></i></div>
<div class="btn btn-sm btn-light mr-1" title="Insert bullet list" onclick="editor.insertSnippet('\n- \n- \n- ');editor.focus();"><i class="fa-solid fa-list"></i></div>
<div class="btn btn-sm btn-light mr-1" title="Insert numbered list" onclick="editor.insertSnippet('\n1. a \n2. b \n3. c ');editor.focus();"><i class="fa-solid fa-list-ol"></i></div>
</div>
</div>
<div class="row">
<div class="col-md-6" id="container_editor_summary">
<div style="display: none" id="fetched_crc"></div>
<div id="editor_summary" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}"></div>
<textarea id="case_summary" rows="10" cols="82" style="display: none"></textarea>
</div>
<div class="col-md-6" id="ctrd_casesum">
<div id="targetDiv"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal " tabindex="-1" role="dialog" id="modal_select_report" data-backdrop="true">
<div class="modal-lg modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5>Select report template</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
{% if reports| length == 0 %}
<div class="alert alert-warning" role="alert">
<h4 class="alert-heading">No report template found</h4>
<p>Report templates are configured in <a href="/manage/templates?cid={{case.case_id}}">the management section</a>.</p>
</div>
{% else %}
<div class="col">
<p>Since IRIS v2.0.0, the report generation supports images. Integration of images might fail depending on the situation.<br/><code>Safe Mode</code> can be used to generate the report without them.</p>
</div>
<select class="selectpicker form-control bg-outline-success dropdown-submenu" data-show-subtext="true" data-live-search="true" id="select_report">
{% for report in reports %}
<option data-toggle="tooltip" value="{{ report[0] }}" data-subtext="{{ report[3] }}">{{ report[1] }} ({{ report[2].capitalize() }})</option>
{% endfor %}
</select>
</div>
<div class="modal-footer">
<a href="#" class="btn btn-light float-left mt-2 mr-auto" onclick="gen_report(true);">
<span class="btn-label">
<i class="fa fa-file-download"></i>
</span>
Generate in Safe Mode
</a>
<a href="#" class="btn btn-light float-right mt-2 ml-2" onclick="gen_report(false);">
<span class="btn-label">
<i class="fa fa-file-download"></i>
</span>
Generate
</a>
{% endif %}
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
<div class="modal " tabindex="-1" role="dialog" id="modal_select_report_act" data-backdrop="true">
<div class="modal-lg modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5>Select activity report template</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
{% if reports| length == 0 %}
<div class="alert alert-warning" role="alert">
<h4 class="alert-heading">No report template found</h4>
<p>Report templates are configured in <a href="/manage/templates?cid={{case.case_id}}">the management section</a>.</p>
</div>
{% else %}
<div class="col">
<p>Since IRIS v2.0.0, the report generation supports images. Integration of images might fail depending on the situation.<br/><code>Safe Mode</code> can be used to generate the report without them.</p>
</div>
<select class="selectpicker form-control bg-outline-success dropdown-submenu mb-2" data-show-subtext="true" data-live-search="true" id="select_report_act">
{% for report in reports_act %}
<option data-toggle="tooltip" value="{{ report[0] }}" data-subtext="{{ report[3] }}">{{ report[1] }} ({{ report[2].capitalize() }})</option>
{% endfor %}
</select>
</div>
<div class="modal-footer">
<a href="#" class="btn btn-light float-left mt-2 mr-auto" onclick="gen_act_report(true);">
<span class="btn-label">
<i class="fa fa-file-download"></i>
</span>
Generate in Safe Mode
</a>
<a href="#" class="btn btn-light float-right mt-2 ml-2" onclick="gen_act_report(false);">
<span class="btn-label">
<i class="fa fa-file-download"></i>
</span>
Generate
</a>
{% endif %}
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
<div class="modal " tabindex="-1" role="dialog" id="modal_choose_reviewer" data-backdrop="true">
<div class="modal-lg modal-dialog" role="document">
<div class="modal-content">
<form method="post" action="" id="form_choose_reviewer">
<div class="modal-header">
<h5>Choose reviewer</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<div class="row mb-2">
<div class="col-12">
<div class="form-group">
<select class="selectpicker form-control" data-dropup-auto="false" data-live-search="true" id="reviewer_id">
</select>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12 d-flex">
<button type="button" class="btn btn-default mr-auto" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-success ml-auto" id="submit_set_reviewer">Request</button>
</div>
</div>
</div>
</form>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
<div class="modal" tabindex="-1" role="dialog" id="modal_case_detail" data-backdrop="true">
<div class="modal-xl modal-dialog" role="document">
<div class="modal-content" id="info_case_modal_content">
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
<div class="modal bg-shadow-gradient" tabindex="-1" role="dialog" id="modal_ac_additional" data-backdrop="true">
</div>
<div class="modal bg-shadow-gradient" tabindex="-1" role="dialog" id="modal_case_review" data-backdrop="static" data-keyboard="false">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Case Review</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-12">
<p>Do you confirm that the case has been reviewed?</p>
</div>
</div>
<div class="row">
<div class="col-12">
<button type="button" class="btn btn-danger float-left" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success float-right" id="confirmReview">Confirm Review</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% include 'includes/footer.html' %}
{% endif %} {% endblock content %} {% block javascripts %}
<script src="/static/assets/js/plugin/ace/src-noconflict/ace.js" type="text/javascript" charset="utf-8"></script>
<script src="/static/assets/js/plugin/ace/src-noconflict/ext-language_tools.js" type="text/javascript" charset="utf-8"></script>
<script src="/static/assets/js/core/socket.io.js"></script>
<script src="/static/assets/js/plugin/select/select2.js"></script>
<script src="/static/assets/js/plugin/showdown/showdown.min.js"></script>
<script src="/static/assets/js/iris/crc32.js"></script>
<script src="/static/assets/js/iris/datatablesUtils.js"></script>
<script src="/static/assets/js/iris/case.js"></script>
<script src="/static/assets/js/iris/manage.cases.common.js"></script>
<script src="/static/assets/js/iris/case.summary.js"></script>
<script src="/static/assets/js/plugin/select/bootstrap-select.min.js"></script>
<script src="/static/assets/js/plugin/select/bootstrap-multiselect.min.js"></script>
<script>
$('#modal_select_report').selectpicker();
load_menu_mod_options_modal([{{case.case_id}}], 'case', $("#case_modal_quick_actions"));
</script>
{% endblock javascripts %}

View File

@@ -0,0 +1,148 @@
{% extends "layouts/default_ext.html" %} {% block title %} Case Assets {% endblock title %} {% block stylesheets %}
{% include 'includes/header_case.html' %}
<link rel="stylesheet" href="/static/assets/css/bootstrap-select.min.css">
{% endblock stylesheets %}
{% block content %}
{% include 'includes/navigation_ext.html' %}
{% include 'includes/sidenav.html' %}
<div class="main-panel">
<div class="content">
{% if current_user.is_authenticated %}
{{ form.hidden_tag() }}
<nav class="navbar navbar-header navbar-expand-lg pt-2 pb-2 bg-primary-gradient">
<div class="container-fluid">
<div class="collapse" id="search-nav">
<div id="tables_button"></div>
</div>
<ul class="navbar-nav topbar-nav ml-md-auto align-items-center page-navigation page-navigation-style-2 page-navigation-secondary">
<li class="nav-item ml-2">
<span class="text-white text-sm mr-2" id="last_resfresh">Loading</span>
<span class="text-warning text-sm mr-2" id="page_warning"></span>
</li>
<li class="nav-item">
<button class="btn btn-primary btn-sm" onclick="reload_assets();">
<span class="menu-title">Refresh</span>
</button>
</li>
<li class="nav-item">
<button class="btn btn-dark btn-sm" onclick="add_assets();">
<span class="menu-title">Add assets</span>
</button>
</li>
<li class="nav-item">
<div class="dropdown">
<button class="btn btn-sm btn-border btn-black" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="menu-title"><i class="fas fa-ellipsis-v"></i></span>
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="#" onclick="fire_upload_assets();">Upload CSV of assets</a>
</div>
</div>
</li>
</ul>
</div>
</nav>
<div class="page-inner ">
<div class="row ">
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
<div class="col-md-12">
<div class="card" id="card_main_load" style="display:none;">
<div class="card-body">
<div class="col" id="assets_table_wrapper">
<table class="table display table-striped table-hover responsive" width="100%" cellspacing="0" id="assets_table" >
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>IP</th>
<th>Compromised</th>
<th>IOC</th>
<th>Tags</th>
<th>Analysis</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>IP</th>
<th>Compromised</th>
<th>IOC</th>
<th>Tags</th>
<th>Analysis</th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal modal-case-focus" tabindex="-1" role="dialog" id="modal_add_asset" data-backdrop="true">
<div class="modal-xl modal-dialog" role="document">
<div class="modal-content" id="modal_add_asset_content">
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
{% endif %}
</div>
<div class="modal " tabindex="-1" role="dialog" id="modal_upload_assets" data-backdrop="true">
<div class="modal-lg modal-dialog" role="document">
<form method="post" action="" id="form_upload_assets">
<div class="modal-content">
<div class="modal-header">
<h5>Upload assets list (CSV format)</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="ioc_format" class="placeholder">Expected CSV File format</label>
<textarea class="form-control col-md-12 col-sm-12 sizable-textarea" rows="2" disabled>asset_name,asset_type_name,asset_description,asset_ip,asset_domain,asset_tags (separated with &quot;|&quot;)</textarea>
</div>
<div class="form-group">
<label class="placeholder">CSV File format example</label>
<textarea class="form-control col-md-12 col-sm-12 sizable-textarea" rows="3" disabled>asset_name,asset_type_name,asset_description,asset_ip,asset_domain,asset_tags
"My computer","Mac - Computer","Computer of Mme Michu","192.168.15.5","iris.local","Compta|Mac"
"XCAS","Windows - Server","Xcas server","192.168.15.48","iris.local",""</textarea>
</div>
<div class="form-group">
<label class="placeholder">Choose CSV file to import : </label>
<input id="input_upload_assets" type="file" accept="text/csv">
</div>
</div>
<div class='invalid-feedback' id='ioc-invalid-msg'></div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark mr-auto" onclick="generate_sample_csv();">Download sample CSV</button>
<button type="button" class="btn btn-outline-success" onclick="upload_assets();">Upload</button>
</div>
</div><!-- /.modal-content -->
</form>
</div><!-- /.modal-dialog -->
</div>
{% include 'includes/footer.html' %}
</div>
{% endblock content %}
{% block javascripts %}
{% include 'includes/footer_case.html' %}
<script src="/static/assets/js/plugin/sortable/sortable.js "></script>
<script src="/static/assets/js/iris/case.asset.js"></script>
{% endblock javascripts %}

View File

@@ -0,0 +1,71 @@
{% extends "layouts/default_ext.html" %}
{% block title %} Case Graph {% endblock title %}
{% block stylesheets %}
<link rel="stylesheet" href="/static/assets/css/bootstrap-slider.min.css">
<link rel="stylesheet" href="/static/assets/css/select2.css">
<link rel="stylesheet" href="/static/assets/css/bootstrap-select.min.css">
<link href="/static/assets/css/vis.min.css" rel="stylesheet" type="text/css" />
{% endblock stylesheets %}
{% block content %}
{% include 'includes/navigation_ext.html' %}
{% include 'includes/sidenav.html' %}
<div class="main-panel">
<div class="content">
<nav class="navbar navbar-header navbar-expand-lg bg-primary-gradient">
<ul class="container-fluid mt-3 mb--2">
<ul class="navbar-nav topbar-nav ml-md-auto align-items-center page-navigation page-navigation-style-2 page-navigation-secondary">
<li class="nav-item ml-2">
<span class="text-white text-sm mr-2" id="last_resfresh">Loading</span>
</li>
<li class="nav-item hidden-caret">
<button class="btn btn-primary btn-sm" onclick="get_case_graph();">
<span class="menu-title">Refresh</span>
</button>
</li>
</ul>
</ul>
</nav>
{% if current_user.is_authenticated %}
{{ form.hidden_tag() }}
<div class="page-inner">
<div class="row">
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
<div class="card mb-4 col-md-12" id="card_main_load" style="display:none;">
<div class="card-body">
<div class="row">
<div id='graph-container'></div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% include 'includes/footer.html' %}
</div>
{% endblock content %}
{% block javascripts %}
<script src="/static/assets/js/plugin/vis/vis.min.js"></script>
<script src="/static/assets/js/plugin/vis/vis-network.min.js"></script>
<script src="/static/assets/js/plugin/bootstrap-slider/bootstrap-slider.min.js"></script>
<script src="/static/assets/js/plugin/select/select2.js"></script>
<script src="/static/assets/js/plugin/select/bootstrap-select.min.js"></script>
<script src="/static/assets/js/iris/case.js"></script>
<script src="/static/assets/js/iris/case.graph.js"></script>
{% endblock javascripts %}

View File

@@ -0,0 +1,86 @@
{% extends "layouts/default_ext.html" %}
{% block title %} Case Graph Timeline {% endblock title %}
{% block stylesheets %}
<link rel="stylesheet" href="/static/assets/css/bootstrap-datetime.css">
<link rel="stylesheet" href="/static/assets/css/suggestags.css">
<link rel="stylesheet" href="/static/assets/css/bootstrap-select.min.css">
<link rel="stylesheet" href="/static/assets/css/select2.css">
<link rel="stylesheet" href="/static/assets/css/vis.graph.css">
{% endblock stylesheets %}
{% block content %}
{% include 'includes/navigation_ext.html' %}
{% include 'includes/sidenav.html' %}
<div class="main-panel">
<div class="content">
<nav class="navbar navbar-header navbar-expand-lg bg-primary-gradient">
<ul class="container-fluid mt-3 mb--2">
<ul class="navbar-nav">
<li class="nav-item hidden-caret">
<a class="menu-title btn btn-dark btn-sm" href="visualize?cid={{session['current_case'].case_id}}">No group</a>
</li>
<li class="nav-item hidden-caret">
<a class="menu-title btn btn-dark btn-sm" href="visualize?cid={{session['current_case'].case_id}}&group-by=asset"><span class="text-decoration-none">Group by asset</span></a>
</li>
<li class="nav-item hidden-caret">
<a class="menu-title btn btn-dark btn-sm" href="visualize?cid={{session['current_case'].case_id}}&group-by=category">Group by category</a>
</li>
</ul>
<ul class="navbar-nav topbar-nav ml-md-auto align-items-center page-navigation page-navigation-style-2 page-navigation-secondary">
<li class="nav-item ml-2">
<span class="text-white text-sm mr-2" id="last_resfresh">Loading</span>
</li>
<li class="nav-item hidden-caret">
<button class="btn btn-primary btn-sm" onclick="refresh_timeline_graph();">
<span class="menu-title">Refresh</span>
</button>
</li>
</ul>
</ul>
</nav>
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
<div class="page-inner">
<div class="row">
<div class="col-12">
<div class="card" id="card_main_load" style="display:none;">
<div class="card-body">
<div id='visualization'></div>
</div>
</div>
</div>
</div>
</div>
</div>
{% include 'includes/footer.html' %}
</div>
{% endblock content %}
{% block javascripts %}
<script src="/static/assets/js/core/moments.min.js"></script>
<script src="/static/assets/js/core/bootstrap-datetimepicker.min.js"></script>
<script src="/static/assets/js/plugin/tagsinput/suggesttag.js"></script>
<script src="/static/assets/js/plugin/select/select2.js"></script>
<script src="/static/assets/js/plugin/select/bootstrap-select.min.js"></script>
<script src="/static/assets/js/iris/case.js"></script>
<script src="/static/assets/js/iris/case.timeline.visu.js"></script>
<script src="/static/assets/js/plugin/vis/vis.graph.js"></script>
<script>
$(document).ready(function(){
show_loader();
refresh_timeline_graph();
});
</script>
{% endblock javascripts %}

View File

@@ -0,0 +1,149 @@
{% extends "layouts/default_ext.html" %}
{% block title %} Case IOC {% endblock title %}
{% block stylesheets %}
{% include 'includes/header_case.html' %}
{% endblock stylesheets %}
{% block content %}
{% include 'includes/navigation_ext.html' %}
{% include 'includes/sidenav.html' %}
<div class="main-panel">
<div class="content">
{% if current_user.is_authenticated %}
{{ form.hidden_tag() }}
<nav class="navbar navbar-header navbar-expand-lg pt-2 pb-2 bg-primary-gradient">
<div class="container-fluid">
<div class="collapse" id="search-nav">
<div id="tables_button"></div>
</div>
<ul class="navbar-nav topbar-nav ml-md-auto align-items-center page-navigation page-navigation-style-2 page-navigation-secondary">
<li class="nav-item ml-2">
<span class="text-white text-sm mr-2" id="last_resfresh">Loading</span>
</li>
<li class="nav-item">
<button class="btn btn-primary btn-sm" onclick="reload_iocs();">
<span class="menu-title">Refresh</span>
</button>
</li>
<li class="nav-item">
<button class="btn btn-dark btn-sm" onclick="add_ioc();">
<span class="menu-title">Add IOC</span>
</button>
</li>
<li class="nav-item">
<div class="dropdown">
<button class="btn btn-sm btn-border btn-black" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="menu-title"><i class="fas fa-ellipsis-v"></i></span>
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="#" onclick="fire_upload_iocs();">Upload CSV of IOCs</a>
</div>
</div>
</li>
</ul>
</div>
</nav>
<div class="page-inner">
<div class="row">
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
<div class="col-md-12">
<div class="card" id="card_main_load" style="display:none;">
<div class="card-body">
<table class="table display wrap col-border table-striped table-hover" width="100%" cellspacing="0" id="ioc_table" >
<thead>
<tr>
<th>Value</th>
<th>Type</th>
<th>Description</th>
<th>Tags</th>
<th>Linked cases</th>
<th>TLP</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Value</th>
<th>Type</th>
<th>Description</th>
<th>Tags</th>
<th>Linked cases</th>
<th>TLP</th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% include 'includes/footer.html' %}
</div>
<div class="modal" tabindex="-1" role="dialog" id="modal_add_ioc" data-backdrop="true">
<div class="modal-xl modal-dialog" role="document">
<div class="modal-content" id="modal_add_ioc_content">
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<div class="modal " tabindex="-1" role="dialog" id="modal_upload_ioc" data-backdrop="true">
<div class="modal-lg modal-dialog" role="document">
<form method="post" action="" id="form_upload_ioc">
<div class="modal-content">
<div class="modal-header">
<h5>Upload IOC list (CSV format)</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="ioc_format" class="placeholder">Expected CSV File format</label>
<textarea class="form-control col-md-12 col-sm-12 sizable-textarea" rows="2" disabled>ioc_value,ioc_type,ioc_description,ioc_tags,ioc_tlp
&lt;Value&gt;,&lt;Type&gt;,&lt;Description&gt;,&lt;TLP&gt;,Tags separated with &quot;|&quot;</textarea>
</div>
<div class="form-group">
<label class="placeholder">CSV File format example</label>
<textarea class="form-control col-md-12 col-sm-12 sizable-textarea" rows="3" disabled>ioc_value,ioc_type,ioc_description,ioc_tags,ioc_tlp
1.1.1.1,IP,Cloudfare DNS IP address,Cloudfare|DNS,green
wannacry.exe,File,Wannacry sample found,Wannacry|Malware|PE,amber</textarea>
</div>
<div class="form-group">
<label class="placeholder">Choose CSV file to import : </label>
<input id="input_upload_ioc" type="file" accept="text/csv">
</div>
</div>
<div class='invalid-feedback' id='ioc-invalid-msg'></div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark mr-auto" onclick="generate_sample_csv();">Download sample CSV</button>
<button type="button" class="btn btn-outline-success" onclick="upload_ioc();">Upload</button>
</div>
</div><!-- /.modal-content -->
</form>
</div><!-- /.modal-dialog -->
</div>
{% endblock content %}
{% block javascripts %}
{% include 'includes/footer_case.html' %}
<script src="/static/assets/js/iris/case.ioc.js"></script>
{% endblock javascripts %}

View File

@@ -0,0 +1,59 @@
<div class="modal shortcut_modal bg-shadow-gradient" id="shortcutModal" tabindex="-1" aria-labelledby="shortcutModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="shortcutModalLabel">Shortcuts</h5>
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
<div class="modal-body">
<table class="table">
<thead>
<tr>
<th scope="col">Shortcut</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>CTRL-S</td>
<td>Save note</td>
</tr>
<tr>
<td>CTRL-B</td>
<td>Bold</td>
</tr>
<tr>
<td>CTRL-I</td>
<td>Italic</td>
</tr>
<tr>
<td>CTRL-SHIFT-1</td>
<td>Heading 1</td>
</tr>
<tr>
<td>CTRL-SHIFT-2</td>
<td>Heading 2</td>
</tr>
<tr>
<td>CTRL-SHIFT-3</td>
<td>Heading 3</td>
</tr>
<tr>
<td>CTRL-SHIFT-4</td>
<td>Heading 4</td>
</tr>
<tr>
<td>CTRL-`</td>
<td>Insert code</td>
</tr>
<tr>
<td>CTRL-K</td>
<td>Insert link</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,134 @@
{% extends "layouts/default_ext.html" %} {% block title %} Case notes {% endblock title %} {% block stylesheets %}
{% include 'includes/header_case.html' %}
<link rel="stylesheet" href="/static/assets/css/bootstrap-select.min.css">
{% endblock stylesheets %}
{% block content %}
{% include 'includes/navigation_ext.html' %}
{% include 'includes/sidenav.html' %}
<div class="main-panel">
<div class="content">
{% if current_user.is_authenticated %}
{{ form.hidden_tag() }}
<nav class="navbar navbar-header navbar-expand-lg pt-2 pb-2 bg-primary-gradient">
<div class="container-fluid">
<div class="collapse search-flex" id="search-nav">
<ul class="list-group list-group-bordered hidden-caret" id="notes_search_list"></ul>
<input type="text" class="form-control mr-3" style="max-width:400px;" id="search_note_input" onkeyup="search_notes()" placeholder="Search in notes..">
</div>
<ul class="navbar-nav topbar-nav ml-md-auto align-items-center page-navigation page-navigation-style-2 page-navigation-secondary">
<li class="nav-item ml-2">
<span class="text-white text-sm mr-2" id="last_resfresh">Loading</span>
</li>
<li class="nav-item">
<button class="btn btn-primary btn-sm" onclick="draw_kanban();">
<span class="menu-title">Refresh</span>
</button>
</li>
<li class="nav-item">
<button class="btn btn-dark btn-sm" onclick="add_remote_groupnote();">
<span class="menu-title">Add notes group</span>
</button>
</li>
</ul>
</div>
</nav>
<div class="page-inner">
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
<div id="empty-set-notes" style="display:none;">
<h4 class="text-dark text-sm text-center ml-mr-auto">It looks pretty empty <i class="fa-solid fa-mug-hot ml-2"></i></h4>
<h4 class="text-dark text-sm text-center ml-mr-auto"><a href="#" onclick="add_remote_groupnote();">Click here to add the first note group</a></h4>
</div>
<div class="row" id="card_main_load" style="display:none;">
<div class="container-fluid">
<div class="float-right mt-2 col">
</div>
</div>
</div>
<div class="row">
<div id="myKanban" class="board">
</div>
</div>
<div id="side_timeline">
<button class="btn btn-round btn-primary-success btn_over_page_i" onclick="add_remote_groupnote();"><i class="fas fa-plus-circle"></i></button>
</div>
</div>
<div class="kanban-item row" id="_subnote_" style="display: none;" onclick="edit_note(this);" title="">
<a href="#" class="kanban-title text-truncate w-100" draggable="false">New note</a><br />
<em><small href="#" class="text-sm text-muted kanban-info" draggable="false"><i
class="flaticon-tool mr-1"></i>Hello</small></em>
<iris_note style="display: none;" id="xqx00qxq">New note</iris_note>
<div class="kanban-badge avatar-group-note col-12" id="kanban_badge_">
</div>
</div>
<div data-id="_todo" class="kanban-board" id="group_" title="" draggable="false" style="display: none;">
<header class="kanban-board-header">
<div class="row">
<div class="col-8">
<div contenteditable="true" maxlength="25" class="kanban-title-board" onclick="">Note group</div>
</div>
<div class="col">
<div class="kanban-title-button">
<div class="row mr-1">
<button class="mr-2" onclick="" style="display: none;"><i
class="fas fa-check-circle text-success"></i></button>
<button class="mr-2" onclick=""><i class="fas fa-plus-circle "></i></button>
<div class="dropdown dropdown-kanban ">
<button class="dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<i class="icon-options-vertical"></i>
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton ">
<a class="dropdown-item" href="#" draggable="false">Delete</a>
</div>
</div>
</div>
</div>
</div>
</div>
</header>
<main class="kanban-drag" id="_main">
</main>
</div>
<div class="modal modal-case-focus" tabindex="-1" role="dialog" id="modal_note_detail" data-backdrop="true">
<div class="modal-dialog modal-xxl modal-xl" role="document">
<div class="modal-content" id="info_note_modal_content">
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
{% endif %}
</div>
{% include 'includes/footer.html' %}
</div>
{% endblock content %}
{% block javascripts %}
{% include 'includes/footer_case.html' %}
<script src="/static/assets/js/plugin/sortable/sortable.js "></script>
<script src="/static/assets/js/core/socket.io.js"></script>
<script src="/static/assets/js/iris/case.notes.js "></script>
<script>
/* Wait for document to be ready before loading the kanban board */
$(document).ready(function () {
/* load board */
boardNotes.init();
setInterval(function() { check_update('notes/state'); }, 3000);
draw_kanban();
});
</script>
{% endblock javascripts %}

View File

@@ -0,0 +1,105 @@
{% extends "layouts/default_ext.html" %} {% block title %} Case Evidences {% endblock title %} {% block stylesheets %}
{% include 'includes/header_case.html' %}
{% endblock stylesheets %}
{% block content %}
{% include 'includes/navigation_ext.html' %}
{% include 'includes/sidenav.html' %}
<div class="main-panel">
<div class="content">
{% if current_user.is_authenticated %}
{{ form.hidden_tag() }}
<nav class="navbar navbar-header navbar-expand-lg pt-2 pb-2 bg-primary-gradient">
<div class="container-fluid">
<div class="collapse" id="search-nav">
<div id="tables_button"></div>
</div>
<ul class="navbar-nav topbar-nav ml-md-auto align-items-center page-navigation page-navigation-style-2 page-navigation-secondary">
<li class="nav-item ml-2">
<span class="text-white text-sm mr-2" id="last_resfresh">Loading</span>
</li>
<li class="nav-item">
<button class="btn btn-primary btn-sm" onclick="reload_rfiles(true);">
<span class="menu-title">Refresh</span>
</button>
</li>
<li class="nav-item">
<button onclick="add_modal_rfile()" class="btn btn-dark btn-sm">
<span class="menu-title">Register Evidence</span>
</button>
</li>
</ul>
</div>
</nav>
<div class="page-inner">
<div class="row">
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
<div class="col-md-12">
<div class="card" id="card_main_load" style="display:none;">
<div class="card-body">
<div class="table-responsive" id="rfiles_table_wrapper">
<div class="selectgroup">
<span id="table_buttons"></span>
</div>
<table class="table display wrap col-border table-striped table-hover dataTable" width="100%"
cellspacing="0" id="rfiles_table">
<thead>
<tr>
<th>Name</th>
<th>Date</th>
<th>Hash</th>
<th>Size (bytes)</th>
<th>Description</th>
<th>Added by</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Name</th>
<th>Date</th>
<th>Hash</th>
<th>Size(bytes)</th>
<th>Description</th>
<th>Added by</th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal" tabindex="-1" role="dialog" id="modal_add_rfiles" data-backdrop="true">
<div class="modal-lg modal-dialog" role="document">
<div class="modal-content" id="modal_add_rfiles_content">
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
{% endif %}
</div>
{% include 'includes/footer.html' %}
</div>
{% endblock content %}
{% block javascripts %}
{% include 'includes/footer_case.html' %}
<script src="/static/assets/js/iris/case.rfiles.js"></script>
<script>
</script>
{% endblock javascripts %}

View File

@@ -0,0 +1,109 @@
{% extends "layouts/default_ext.html" %}
{% block title %} Case Tasks {% endblock title %}
{% block stylesheets %}
{% include 'includes/header_case.html' %}
<link rel="stylesheet" href="/static/assets/css/bootstrap-multiselect.min.css">
{% endblock stylesheets %}
{% block content %}
{% include 'includes/navigation_ext.html' %}
{% include 'includes/sidenav.html' %}
<div class="main-panel">
<div class="content">
{% if current_user.is_authenticated %}
{{ form.hidden_tag() }}
<nav class="navbar navbar-header navbar-expand-lg pt-2 pb-2 bg-primary-gradient">
<div class="container-fluid">
<div class="collapse" id="search-nav">
<div id="tables_button"></div>
</div>
<ul class="navbar-nav topbar-nav ml-md-auto align-items-center page-navigation page-navigation-style-2 page-navigation-secondary">
<li class="nav-item ml-2">
<span class="text-white text-sm mr-2" id="last_resfresh">Loading</span>
</li>
<li class="nav-item">
<button class="btn btn-primary btn-sm" onclick="get_tasks();">
<span class="menu-title">Refresh</span>
</button>
</li>
<li class="nav-item">
<button class="btn btn-dark btn-sm" onclick="add_task();">
<span class="menu-title">Add task</span>
</button>
</li>
</ul>
</div>
</nav>
<div class="page-inner ">
<div class="row ">
<div class="col-md-12">
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
<section class="card" id="card_main_load" style="display:none;">
<div class="card-body">
<div class="table-responsive" id="tasks_table_wrapper">
<table class="table display wrap col-border table-striped table-hover dataTable" width="100%"
cellspacing="0" id="tasks_table">
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Status</th>
<th>Assigned to</th>
<th>Open date</th>
<th>Tags</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Title</th>
<th>Description</th>
<th>Status</th>
<th>Assigned to</th>
<th>Open date</th>
<th>Tags</th>
</tr>
</tfoot>
</table>
</div>
</div>
</section>
</div>
</div>
</div>
<div class="modal " tabindex="-1" role="dialog" id="modal_add_task" data-backdrop="true">
<div class="modal-xxl modal-dialog" role="document">
<div class="modal-content" id="modal_add_task_content">
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
{% endif %}
</div>
{% include 'includes/footer.html' %}
</div>
{% endblock content %}
{% block javascripts %}
{% include 'includes/footer_case.html' %}
<script src="/static/assets/js/plugin/datatables/dataTables.cellEdit.js"></script>
<script src="/static/assets/js/plugin/select/bootstrap-multiselect.min.js"></script>
<script src="/static/assets/js/iris/case.tasks.js"></script>
{% endblock javascripts %}

View File

@@ -0,0 +1,178 @@
{% extends "layouts/default_ext.html" %}
{% block title %} Case Timeline {% endblock title %}
{% block stylesheets %}
<link rel="stylesheet" href="/static/assets/css/bootstrap-datetime.css">
<link rel="stylesheet" href="/static/assets/css/suggestags.css">
<link rel="stylesheet" href="/static/assets/css/bootstrap-select.min.css">
<link rel="stylesheet" href="/static/assets/css/select2.css">
{% endblock stylesheets %}
{% block content %}
{% include 'includes/navigation_ext.html' %}
{% include 'includes/sidenav.html' %}
<div class="main-panel">
<div class="content">
<!-- Navbar Header -->
<nav class="navbar navbar-header navbar-expand-lg bg-primary-gradient">
{{ form.hidden_tag() }}
<ul class="container-fluid mt-3 mb--2">
<ul class="navbar-nav col-8">
<li class="nav-item hidden-caret col-12">
<div class="row">
<div id='timeline_filtering' class="col-9 pt-2 pl-2" style="border-radius:3px;" ></div>
<button class="btn btn-sm btn-light ml-2 pt-2" onclick="filter_timeline();">
Apply filter
</button>
<button class="btn btn-sm btn-light ml-1 pt-2" onclick="reset_filters();">
Reset
</button>
<i class="ml-1 mt-1 fa-regular text-white fa-circle-question" title="Filter help" style="cursor:pointer;" onclick="show_timeline_filter_help();"></i>
</div>
</li>
</ul>
<ul class="navbar-nav topbar-nav ml-md-auto align-items-center page-navigation page-navigation-style-2 page-navigation-secondary">
<li class="nav-item ml-2">
<span class="text-white text-sm mr-2" id="last_resfresh">Loading</span>
</li>
<li class="nav-item hidden-caret">
<button class="btn btn-primary btn-sm" onclick="get_or_filter_tm();">
<span class="menu-title">Refresh</span>
</button>
</li>
<li class="nav-item hidden-caret">
<button class="btn btn-dark btn-sm" onclick="add_event();">
<span class="menu-title">Add event</span>
</button>
</li>
<li class="nav-item">
<div class="dropdown">
<button class="btn btn-sm btn-border btn-black" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="menu-title"><i class="fas fa-ellipsis-v"></i></span>
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="timeline/visualize?cid={{session['current_case'].case_id}}"> Visualize</a>
<a class="dropdown-item" href="timeline/visualize?cid={{session['current_case'].case_id}}&group-by=asset"> Visualize by asset</a>
<a class="dropdown-item" href="timeline/visualize?cid={{session['current_case'].case_id}}&group-by=category">Visualize by category</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" onclick="timelineToCsv();"><small class="fa fa-download mr-2"></small> Download as CSV</a>
<a class="dropdown-item" href="#" onclick="timelineToCsvWithUI();"><small class="fa fa-download mr-2"></small> Download as CSV with user info</a>
<!-- BEGIN_RS_CODE -->
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" onclick="fire_upload_csv_events();"><small class="fa fa-upload mr-2"></small> Upload CSV of events</a>
<!-- END_RS_CODE -->
</div>
</div>
</li>
</ul>
</ul>
</nav>
{% if current_user.is_authenticated %}
<div class="page-inner">
<div class="row">
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
<div class="col-md-12" id="card_main_load" style="display:none;">
<div id="paginator"></div>
<ul class="timeline" id="timeline_list">
</ul>
</div>
</div>
<div id="side_timeline">
<div class="btn_over_page_a">
<button class="btn btn-round btn-light btn-conditional-2 colorinput-color bg-white" title="color1" onclick="events_set_attribute('event_color', '#fff')"></button>
<button class="btn btn-round btn-light btn-conditional-2 colorinput-color bg-primary" title="color2" onclick="events_set_attribute('event_color', '#1572E899')"></button>
<button class="btn btn-round btn-light btn-conditional-2 colorinput-color bg-secondary" title="color3" onclick="events_set_attribute('event_color', '#6861CE99')"></button>
<button class="btn btn-round btn-light btn-conditional-2 colorinput-color bg-info" title="color4" onclick="events_set_attribute('event_color', '#48ABF799')"></button>
<button class="btn btn-round btn-light btn-conditional-2 colorinput-color bg-success" title="color5" onclick="events_set_attribute('event_color', '#31CE3699')"></button>
<button class="btn btn-round btn-light btn-conditional-2 colorinput-color bg-danger" title="color5" onclick="events_set_attribute('event_color', '#F2596199')"></button>
<button class="btn btn-round btn-light btn-conditional-2 colorinput-color bg-warning" title="color5" onclick="events_set_attribute('event_color', '#FFAD4699')"></button>
<button class="btn btn-round btn-light btn-conditional" title="Change color" onclick="toggle_colors()"><i class="fas fa-tint"></i></button>
</div>
<button class="btn btn-round btn-light btn_over_page_delete btn-conditional" title="Delete selected events" onclick="events_bulk_delete();"><i class="fas fa-trash text-danger"></i></button>
<button class="btn btn-round btn-light btn_over_page_b btn-conditional" title="Toggle Summary" onclick="events_set_attribute('event_in_summary')"><i class="fas fa-newspaper"></i></button>
<button class="btn btn-round btn-light btn_over_page_c btn-conditional" title="Toggle Graph" onclick="events_set_attribute('event_in_graph')"><i class="fas fa-share-alt"></i></button>
<button class="btn btn-round btn-light btn_over_page_d" title="Select rows" onclick="toggle_selector();" id="selector-btn"><i class="fas fa-check"></i></button>
<button class="btn btn-round btn-light btn_over_page_e" title="Add new event" onclick="add_event();"><i class="fas fa-plus-circle"></i></button>
<button class="btn btn-round btn-light btn_over_page_f" title="Refresh" onclick="get_or_filter_tm();"><i class="fas fa-redo-alt"></i></button>
<button class="btn btn-round btn-light btn_over_page_g" title="Go at the top" onclick="to_page_up();"><i class="fas fa-arrow-up"></i></button>
<button class="btn btn-round btn-light btn_over_page_h" title="Go at the bottom" onclick="to_page_down();"><i class="fas fa-arrow-down"></i></button>
<button class="btn btn-round btn-light btn_over_page_i" title="Toggle compact view" onclick="toggle_compact_view();"><i class="fas fa-list"></i></button>
</div>
</div>
<div class="modal shadow-lg" tabindex="-1" id="modal_add_event" data-backdrop="true">
<div class="modal-xl modal-dialog" role="document">
<div class="modal-content" id="modal_add_event_content">
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
{% endif %}
</div>
<!-- BEGIN RS_CODE -->
<div class="modal " tabindex="-1" role="dialog" id="modal_upload_csv_events" data-backdrop="true">
<div class="modal-lg modal-dialog" role="document">
<form method="post" action="" id="form_upload_csv_events">
<div class="modal-content">
<div class="modal-header">
<h5>Upload events list (CSV format)</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="csv_format" class="placeholder">Expected Events CSV File format</label>
<textarea id="csv_format" class="form-control col-md-12 col-sm-12 sizable-textarea" rows="2" disabled>event_timestamp,event_title,event_description,linked_assets,linked_IoCs,event_tags (separated with &quot;|&quot;),event_color,event_raw_data</textarea>
</div>
<div class="form-group">
<label class="placeholder">Events CSV File format example</label>
<textarea class="form-control col-md-12 col-sm-12 sizable-textarea" rows="3" disabled>
event_date,event_tz,event_title,event_category,event_content,event_raw,event_source,event_assets,event_iocs,event_tags
"2023-03-26T03:00:30.000","+00:00","An event","Unspecified","Event description","raw","source","","","defender|malicious"
"2023-03-23T12:00:35.000","+00:00","An event","Legitimate","Event description","raw","source","","","admin_action"
</textarea>
</div>
<div class="form-group">
<label class="placeholder">Choose CSV file to import : </label>
<input id="input_upload_csv_events" type="file" accept="text/csv">
</div>
</div>
<div class='invalid-feedback' id='ioc-invalid-msg'></div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark mr-auto" onclick="generate_events_sample_csv();">Download sample CSV</button>
<button type="button" class="btn btn-outline-success" onclick="upload_csv_events();">Upload</button>
</div>
</div><!-- /.modal-content -->
</form>
</div><!-- /.modal-dialog -->
</div>
<!-- END_RS_CODE -->
{% include 'includes/footer.html' %}
</div>
{% endblock content %}
{% block javascripts %}
{% include 'includes/footer_case.html' %}
<script src="/static/assets/js/iris/case.timeline.js"></script>
<script src="/static/assets/js/timeline.js"></script>
{% endblock javascripts %}

View File

@@ -0,0 +1,201 @@
<div class="modal-header">
<div class="col md-12">
<div class="row">
<div class="col align-self-center">
<h4 class="modal-title mr-4">{{ "Asset #{}".format(asset.asset_id) if asset.asset_name else "Add asset" }}</h4>
<small><a class="text-muted">{{ "#{}".format(asset.asset_uuid) if asset.asset_uuid else "" }}</a></small>
</div>
{% include 'modals/modal_attributes_nav.html' %}
<div class="col">
<div class="row float-right">
{% if asset.asset_id %}
<div class="dropdown">
<button class="btn bg-transparent pull-right" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span aria-hidden="true"><i class="fas fa-ellipsis-v"></i></span>
</button>
<div class="dropdown-menu pull-right" id="asset_modal_quick_actions" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="#" onclick='copy_object_link({{asset.asset_id}});return false;'><i class="fa fa-share mr-2"></i>Share</a>
<a class="dropdown-item" href="#" onclick='copy_object_link_md("asset", {{asset.asset_id}});return false;'><i class="fa-brands fa-markdown mr-2"></i>Markdown Link</a>
</div>
</div>
<button type="button" class="btn bg-transparent btn-xs" onclick="comment_element({{ asset.asset_id }}, 'assets')" title="Comments">
<span class="btn-label">
<i class="fa-solid fa-comments"></i><span class="notification" id="object_comments_number">{{ comments_map|length }}</span>
</span>
</button>
{% endif %}
<button class="float-right btn bg-transparent" title="Minimize" onclick="modal_minimized('modal_add_asset', '{{ "Asset {}".format(asset.asset_name) if asset.asset_name else "Add asset" }}');"> <i class='fa fa-minus'></i> </button>
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
</div>
</div>
</div>
</div>
<div class="modal-body">
<div role="tabpanel">
<div class="tab-content">
<div class="tab-pane active" id="details">
<div class="container col-md-12">
<form method="post" action="" id="form_new_asset">
<div class="col-md-12 col-lg-12 col-sm-12">
{{ form.hidden_tag() }}
<div class="form-row ml-2">
<div class="form-group col-6">
<label for="name" class="placeholder">Asset Name *</label>
{{ form.asset_name(class='form-control', autocomplete="off") }}
</div>
<div class="form-group col-6">
<label for="asset_type" class="placeholder">Asset Type *</label>
{{ form.asset_type_id(class="selectpicker form-control") }}
</div>
</div>
<div class="form-group mt-3">
<label for="asset_description" class="placeholder">Description</label>
<div class="md_description_field">
<div class="form-group mt--2">
<button type="button" class="float-right icon-note btn btn-circle btn-sm mt-2" onclick="edit_in_asset_desc();">
</button>
<button type="button" style="display: none;" class="btn btn-dark btn-sm float-right mr-2 mt-2" onclick="preview_asset_description();" id="asset_preview_button"><i class="fa-solid fa-eye"></i></button>
</div>
<div class="row">
<div class="col mb--2 ml--2" id="asset_edition_btn" style="display:none;">
</div>
</div>
<div class="row" style="margin-left:0px;">
<div class="col-12" id="container_asset_desc_content">
<div id="asset_description" class="mr-2" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}">{% if asset %}{{ asset.asset_description }}{% endif %}</div>
<textarea id="asset_desc_content" rows="10" cols="82" style="display: none"></textarea>
</div>
<div class="col-12" id="container_asset_description" style="display:none">
<div id="target_asset_desc"></div>
</div>
</div>
</div>
</div>
<div class="form-row ml-2">
<div class="form-group col-6">
<label for="asset_domain" class="placeholder">Domain</label>
{{ form.asset_domain(class='form-control', autocomplete="off") }}
</div>
<div class="form-group col-6">
<label for="asset_ip" class="placeholder">IP</label>
{{ form.asset_ip(class='form-control', autocomplete="off") }}
</div>
</div>
<div class="form-group">
<a class="btn btn-light btn-sm" data-toggle="collapse" href="#collapseAddInfo" role="button" aria-expanded="false" aria-controls="collapseAddInfo">> Additional information</a>
<div class="collapse" id="collapseAddInfo">
<div class="card card-body">
<label for="asset_info" class="placeholder">Additional information</label>
{{ form.asset_info(class='form-control col-md-12 col-sm-12 sizable-textarea', autocomplete="off") }}
</div>
</div>
</div>
<div class="form-row ml-2">
<div class="form-group col-6">
<label for="asset_compromise_status_id" class="placeholder mt-2">Compromise Status </label>
{{ form.asset_compromise_status_id(class="selectpicker col-9") }}
</div>
<div class="form-group col-6">
<label for="analysis_status_id" class="placeholder mt-2">Analysis Status </label>
{{ form.analysis_status_id(class="selectpicker col-9 float-right") }}
</div>
</div>
<div class="form-group">
<label for="asset_tags">Asset tags
</label>
<input type="text" id="asset_tags"
class="form-control col-md-12" {% if asset.asset_tags %} value="{{ asset.asset_tags }}" {% endif %}/>
</div>
<div class="form-group" data-select2-id="7">
<label>Related IOC</label>
<div class="select2-input ml-12" data-select2-id="6">
<select id="ioc_links" name="ioc_links" class="form-control select2-hidden-accessible ml-12" multiple="" data-select2-id="ioc_links" tabindex="-1" aria-hidden="true" style="width: 100%">
</select>
</div>
</div>
</div>
</form>
</div>
</div>
{% include 'modals/modal_attributes_tabs.html' %}
</div>
{% if asset.asset_id %}
<button type="button" class="btn btn-outline-danger ml-4 mt-5"
onclick="delete_asset({{ asset.asset_id }});">Delete</button>
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right" id="submit_new_asset">Update</button>
{% else %}
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right" id="submit_new_asset">Save</button>
{% endif %}
</div>
</div>
<script>
$('form#form_new_case').validate();
$('#asset_tags').amsifySuggestags({
printValues: false,
suggestions: [ {% for tag in tags %} '{{ tag }}', {% endfor %} ]
});
$('#asset_type_id').selectpicker({
liveSearch: true,
title: "None",
style: "btn-outline-white",
});
$('#analysis_status_id').selectpicker({
liveSearch: true,
title: "None",
style: "btn-outline-white"
});
$('#analysis_status_id').selectpicker('val', '1');
$('#asset_compromise_status_id').selectpicker({
liveSearch: true,
title: "To be determined",
style: "btn-outline-white"
});
$('#asset_compromise_status_id').selectpicker('val', '0');
</script>
{% if asset.asset_id %}
<script>
$('#asset_type_id').selectpicker('val', '{{ asset.asset_type_id }}');
</script>
{% endif %}
{% if asset.analysis_status_id %}
<script>
$('#analysis_status_id').selectpicker('val', '{{ asset.analysis_status_id }}');
</script>
{% endif %}
{% if asset.asset_compromise_status_id %}
<script>
$('#asset_compromise_status_id').selectpicker('val', '{{ asset.asset_compromise_status_id }}');
</script>
{% endif %}
{% if ioc %}
<script>
var data = [
{% for e in ioc %}
{
id: {{ e.ioc_id }},
text: {{ e.ioc_value| tojson }}
},
{% endfor %}
];
$('#ioc_links').select2({ data: data });
</script>
{% endif %}
{% if ioc_prefill %}
<script>
$('#ioc_links').val([
{% for ioc in ioc_prefill %} {{ ioc[0] }}, {% endfor %}
]);
$('#ioc_links').trigger('change');
</script>
{% endif %}

View File

@@ -0,0 +1,327 @@
<div class="modal-header">
<div class="col md-12">
<div class="row">
<div class="col align-self-center">
<h4 class="modal-title mr-4">{% if event.event_id %} Event ID #{{ event.event_id }} {% else %} Add event {% endif %}
{% if event.modification_history %}
<i class="fa-solid fa-clock-rotate-left ml-3 mt-2" data-toggle="popover" data-html="true" id="pop_history" style="cursor: pointer;"
title="Modifications history"
data-content="<small>{% for mod in event.modification_history %}<code>{{ mod|format_datetime('%Y-%m-%d %H:%M') }}</code> - {{ event.modification_history[mod].action }} by {{ event.modification_history[mod].user }}<br/>{% endfor %}</small>">
</i>
{% endif %}
</h4>
<small><i class="text-muted">{% if event.event_uuid %}#{{ event.event_uuid }}{% endif %}</i></small>
</div>
{% include 'modals/modal_attributes_nav.html' %}
<div class="col ">
<div class="row float-right">
{% if event.event_id %}
<div class="dropdown">
<button class="btn bg-transparent pull-right" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span aria-hidden="true"><i class="fas fa-ellipsis-v"></i></span>
</button>
<div class="dropdown-menu pull-right" id="event_modal_quick_actions" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="#" onclick='copy_object_link({{event.event_id}});return false;'><i class="fa fa-share mr-2"></i>Share</a>
<a class="dropdown-item" href="#" onclick='copy_object_link_md("event", {{event.event_id}});return false;'><i class="fa-brands fa-markdown mr-2"></i>Markdown Link</a>
<a class="dropdown-item" href="#" onclick='duplicate_event({{event.event_id}});return false;'><i class="fa fa-clone mr-2"></i>Duplicate</a>
</div>
</div>
<button type="button" class="btn bg-transparent btn-xs" onclick="comment_element({{ event.event_id }}, 'timeline/events')" title="Comments">
<span class="btn-label">
<i class="fa-solid fa-comments"></i><span class="notification" id="object_comments_number">{{ comments_map|length }}</span>
</span>
</button>
{% endif %}
<button class="float-right btn bg-transparent" title="Minimize" onclick="modal_minimized('modal_add_event', '{% if event.event_id %} Event ID #{{ event.event_id }} {% else %} Add event {% endif %}');"> <i class='fa fa-minus'></i> </button>
<button type="button" class="float-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
</div>
</div>
</div>
</div>
<div class="modal-body">
<div role="tabpanel">
<div class="tab-content">
<div class="tab-pane active" id="details">
<div class="container col-md-12">
<form method="post" action="" id="form_new_event">
<div class="col-md-12 col-lg-12 col-sm-12">
{{ form.hidden_tag() }}
<div class="row">
<div class="form-group col-xl-5 col-md-12">
<label for="event_title" class="placeholder">{{ form.event_title.label.text }} *</label>
{{ form.event_title(class='form-control col-md-12 col-sm-12', autocomplete="off") }}
</div>
<div class="form-group col-xl-7 col-md-12">
<label for="event_timetitle" class="placeholder ml-2">Event Time *</label>
<div class="row ml-2" id="event_date_inputs">
<input class="form-control col-5 mr-2" type="date" id="event_date" {% if event.event_date_wtz %} value="{{ event.event_date_wtz.strftime('%Y-%m-%d') }}"{% endif %}>
<span></span>
<input class="form-control col-4" type="time" step="0.001" id="event_time" {% if event.event_date_wtz %} value="{{ event.event_date_wtz.strftime('%H:%M:%S.%f')[:-3] }}" {% else %} value="00:00:00.000" {% endif %}>
<span></span>
<input class="form-control col-2" type="text" id="event_tz" {% if event.event_tz %} value="{{ event.event_tz }}" {% else %} value="+00:00" {% endif %}>
<button class="btn btn-sm btn-outline-white" type="button" onclick="show_time_converter();return false;"><i class="fas fa-magic"></i></button>
</div>
<div class="row ml-2" id="event_date_convert" style="display:none;">
<div class="input-group ">
<input class="form-control col-9" type="text" id="event_date_convert_input" placeholder="Enter date in any format and submit to try auto-parsing">
<div class="input-group-append">
<button class="btn btn-sm btn-outline-secondary mr-2" type="button" onclick="time_converter();return false;">Submit</button>
<button class="btn btn-sm btn-outline" type="button" onclick="hide_time_converter();return false;"><i class="fas fa-magic"></i></button>
</div>
</div>
<span id="convert_bad_feedback" class="text-danger"></span>
</div>
</div>
</div>
<div class="row">
<div class="form-group mt-3 col-12">
<label for="event_content" class="placeholder">Event description</label>
<div class="md_description_field">
<div class="form-group mt--2">
<button type="button" class="float-right icon-note btn btn-circle btn-sm mt-2" onclick="edit_in_event_desc();" >
</button>
<button type="button" style="display: none;" class="btn btn-dark btn-sm float-right mr-2 mt-2"
onclick="preview_event_description();" id="event_preview_button"><i class="fa-solid fa-eye"></i></button>
</div>
<div class="row">
<div class="col mb--2 ml--2" id="event_edition_btn" style="display:none;">
</div>
</div>
<div class="row" style="margin-left:0px;">
<div class="col-12" id="container_event_desc_content">
<div id="event_description" class="mr-2" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}">{% if event.event_content %}{{ event.event_content }}{% endif %}</div>
<textarea id="event_desc_content" rows="10" cols="82" style="display: none"></textarea>
</div>
<div class="col-12" id="container_event_description" style="display:none">
<div id="target_event_desc"></div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="form-group col-12 mt-1">
<a class="btn btn-light btn-sm" data-toggle="collapse" href="#collapseRawEvent" role="button" aria-expanded="false" aria-controls="collapseRawEvent">> Edit raw event data</a>
<div class="collapse" id="collapseRawEvent">
<div class="card card-body">
<label for="event_raw" class="placeholder">{{ form.event_raw.label.text }}</label>
{{ form.event_raw(class='form-control sizable-textarea', autocomplete="off") }}
</div>
</div>
</div>
</div>
<div class="row">
<div class="form-group col-xl-6 col-md-12">
<label for="event_title" class="placeholder">{{ form.event_source.label.text }}</label>
{{ form.event_source(class='form-control col-md-12 col-sm-12', autocomplete="off") }}
</div>
<div class="form-group col-xl-6 col-md-12">
<label for="event_tags">Event tags
</label>
<input type="text" id="event_tags"
class="form-control col-md-12" {% if event.event_tags %} value="{{ event.event_tags }}" {% endif %}/>
</div>
</div>
<div class="row">
<div class="form-group col-12">
<label for="event_assets">Link to assets
</label>
<div class="select2-input ml-12" data-select2-id="6">
<select id="event_assets" name="event_assets" class="form-control select2-hidden-accessible ml-12" multiple="" data-select2-id="event_assets" tabindex="-1" aria-hidden="true" style="width: 100%">
</select>
</div>
</div>
</div>
<div class="row">
<div class="form-group col-10">
<label for="event_assets">Link to IOCs
</label>
<div class="select2-input ml-12" data-select2-id="6">
<select id="event_iocs" name="event_iocs" class="form-control select2-hidden-accessible ml-12" multiple="" data-select2-id="event_iocs" tabindex="-1" aria-hidden="true" style="width: 100%">
</select>
</div>
</div>
<div class="form-group col-2">
<div class="form-check">
<label class="form-check-label mt-3">
<input class="form-check-input" type="checkbox" id="event_sync_iocs_assets" checked>
<span class="form-check-sign"> Push IOCs to assets
<i class="ml-1 mt-1 fa-regular fa-circle-question" title="If checked, the IOCs related to this event will be associated with the specified assets" style="cursor:pointer;"></i>
</span>
</label>
</div>
</div>
</div>
<div class="row">
<div class="form-group col-5">
<label for="event_category_id" class="form-label">{{ form.event_category_id.label.text }}</label>
<div class="row col-12">
{{ form.event_category_id(class="selectpicker") }}
</div>
</div>
<div class="form-group col-xl-2 col-md-12">
<div class="form-check">
<label class="form-check-label mt-3">
{{ form.event_in_summary(class="form-check-input", type="checkbox") }}
<span class="form-check-sign"> Add to summary
<i class="ml-1 mt-1 fa-regular fa-circle-question" title="If checked, the event will be integrated in the Timeline Visualization" style="cursor:pointer;"></i>
</span>
</label>
</div>
</div>
<div class="form-group col-xl-2 col-md-12">
<div class="form-check">
<label class="form-check-label mt-3">
{{ form.event_in_graph(class="form-check-input", type="checkbox") }}
<span class="form-check-sign"> Display in graph
<i class="ml-1 mt-1 fa-regular fa-circle-question" title="If checked, the event will be integrated in the Graph section of the case" style="cursor:pointer;"></i>
</span>
</label>
</div>
</div>
<div class="form-group col-xl-3 col-md-12">
<label class="form-label">Event color</label>
<div class="row gutters-xs">
<div class="col-auto">
<label class="selectgroup-item">
<input name="event_color" type="radio" value="#fff" {% if event.event_color == "#fff" %} checked="checked" {% endif %} class="colorinput-input">
<span class="colorinput-color bg-white"></span>
</label>
</div>
<div class="col-auto">
<label class="selectgroup-item">
<input name="event_color" type="radio" value="#1572E899" {% if event.event_color == "#1572E899" %} checked="checked" {% endif %} class="colorinput-input">
<span class="colorinput-color bg-primary"></span>
</label>
</div>
<div class="col-auto">
<label class="selectgroup-item">
<input name="event_color" type="radio" value="#6861CE99" {% if event.event_color == "#6861CE99" %} checked="checked" {% endif %} class="colorinput-input">
<span class="colorinput-color bg-secondary"></span>
</label>
</div>
<div class="col-auto">
<label class="selectgroup-item">
<input name="event_color" type="radio" value="#48ABF799" {% if event.event_color == "#48ABF799" %} checked="checked" {% endif %}class="colorinput-input">
<span class="colorinput-color bg-info"></span>
</label>
</div>
<div class="col-auto">
<label class="selectgroup-item">
<input name="event_color" type="radio" value="#31CE3699" {% if event.event_color == "#31CE3699" %} checked="checked" {% endif %} class="colorinput-input">
<span class="colorinput-color bg-success"></span>
</label>
</div>
<div class="col-auto">
<label class="selectgroup-item">
<input name="event_color" type="radio" value="#F2596199" {% if event.event_color == "#F2596199" %} checked="checked" {% endif %} class="colorinput-input">
<span class="colorinput-color bg-danger"></span>
</label>
</div>
<div class="col-auto">
<label class="selectgroup-item">
<input name="event_color" type="radio" value="#FFAD4699" {% if event.event_color == "#FFAD4699" %} checked="checked" {% endif %} class="colorinput-input">
<span class="colorinput-color bg-warning"></span>
</label>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
{% include 'modals/modal_attributes_tabs.html' %}
</div>
{% if event.event_id %}
<button type="button" class="btn btn-outline-danger mt-5"
onclick="delete_event({{ event.event_id }} );">Delete</button>
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right" id="submit_new_event"
onclick="update_event({{ event.event_id }} );">Update</button>
{% else %}
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
id="submit_new_event">Save</button>
{% endif %}
</div>
</div>
<script>
$('#event_tags').amsifySuggestags({
printValues: true,
suggestions: [ {% for tag in tags %} '{{ tag }}', {% endfor %} ],
whiteList: false,
selectOnHover: false,
});
$('[data-toggle="popover"]').popover();
$('#event_category_id').selectpicker({
width:"100%",
liveSearch: true,
title: "None",
style: "btn-outline-white",
});
</script>
{% if assets %}
<script>
var data = [
{% for e in assets %}
{
id: {{ e.asset_id }},
text: {{ e.asset_name| tojson }}
},
{% endfor %}
];
$('#event_assets').select2({ data: data });
</script>
{% endif %}
{% if iocs %}
<script>
var data = [
{% for e in iocs %}
{
id: {{ e.ioc_id }},
text: {{ e.ioc_value| tojson }}
},
{% endfor %}
];
$('#event_iocs').select2({ data: data });
</script>
{% endif %}
{% if category %}
<script>
$('#event_category_id').val([
{{ category[0].id }},
]);
$('#event_category_id').trigger('change');
</script>
{% else %}
<script>
$('#event_category_id').val(1);
$('#event_category_id').trigger('change');
</script>
{% endif %}
{% if assets_prefill %}
<script>
$('#event_assets').val([
{% for asset in assets_prefill %} {{ asset }}, {% endfor %}
]);
$('#event_assets').trigger('change');
</script>
{% endif %}
{% if iocs_prefill %}
<script>
$('#event_iocs').val([
{% for ioc in iocs_prefill %} {{ ioc }}, {% endfor %}
]);
$('#event_iocs').trigger('change');
</script>
{% endif %}

View File

@@ -0,0 +1,150 @@
<div class="modal-header">
<div class="col md-12">
<div class="row">
<div class="col align-self-center">
<h4 class="modal-title mr-4">{% if ioc.ioc_id %}Edit IOC #{{ ioc.ioc_id }}{% else %} Add IOC {% endif %}</h4>
<small><i class="text-muted">{% if ioc.ioc_uuid %}#{{ ioc.ioc_uuid }}{% endif %}</i></small>
</div>
{% include 'modals/modal_attributes_nav.html' %}
<div class="col">
<div class="row float-right">
{% if ioc.ioc_id %}
<div class="dropdown">
<button class="btn bg-transparent pull-right" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span aria-hidden="true"><i class="fas fa-ellipsis-v"></i></span>
</button>
<div class="dropdown-menu pull-right" id="ioc_modal_quick_actions" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="#" onclick='copy_object_link({{ioc.ioc_id}});return false;'><i class="fa fa-share mr-2"></i>Share</a>
<a class="dropdown-item" href="#" onclick='copy_object_link_md("ioc", {{ioc.ioc_id}});return false;'><i class="fa-brands fa-markdown mr-2"></i>Markdown Link</a>
</div>
<div class="dropdown-menu pull-right" aria-labelledby="dropdownMenuButton">
</div>
</div>
<button type="button" class="btn bg-transparent btn-xs" onclick="comment_element({{ ioc.ioc_id }}, 'ioc')" title="Comments">
<span class="btn-label">
<i class="fa-solid fa-comments"></i><span class="notification" id="object_comments_number">{{ comments_map|length }}</span>
</span>
</button>
{% endif %}
<button class="float-right btn bg-transparent" title="Minimize" onclick="modal_minimized('modal_add_ioc', '{% if ioc.ioc_id %}Edit IOC #{{ ioc.ioc_id }} {% else %} Add IOC {% endif %}');"> <i class='fa fa-minus'></i> </button>
<button type="button" class="float-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
</div>
</div>
</div>
</div>
<div class="modal-body">
<div role="tabpanel">
<div class="tab-content">
<div class="tab-pane active" id="details">
<div class="container col-md-12">
<form method="post" action="" id="form_new_ioc">
<div class="col-md-12 col-lg-12 col-sm-12">
{{ form.hidden_tag() }}
<div class="form-group row">
<div class="col-6">
<label for="ioc_type" class="mr-4">Type *
</label>
{{ form.ioc_type_id(class="selectpicker pl--6 col-10") }}
</div>
<div class="col-6">
<label for="ioc_type" class="mr-4">TLP *
</label>
{{ form.ioc_tlp_id(class="selectpicker pl--6 col-10") }}
</div>
</div>
<div class="form-group">
<label for="ioc_value" class="placeholder">{{ form.ioc_value.label.text }} *</label>
{{ form.ioc_value(class='form-control col-md-12 col-sm-12 sizable-textarea', autocomplete="off") }}
</div>
{% if not ioc.ioc_id %}
<div class="form-group col-2">
<div class="form-check">
<label class="form-check-label mt-3">
<input class="form-check-input" type="checkbox" id="ioc_one_per_line" checked>
<span class="form-check-sign"> One IOC per line
<i class="ml-1 mt-1 fa-regular fa-circle-question" title="If checked, each new line will create a new IOC" style="cursor:pointer;"></i>
</span>
</label>
</div>
</div>
{% endif %}
<div class="form-group mt-3">
<label for="ioc_description" class="placeholder">Description</label>
<div class="md_description_field">
<div class="form-group mt--2">
<button type="button" class="float-right icon-note btn btn-circle btn-sm mt-2" onclick="edit_in_ioc_desc();">
</button>
<button type="button" style="display: none;" class="btn btn-dark btn-sm float-right mr-2 mt-2"
onclick="preview_ioc_description();" id="ioc_preview_button"><i class="fa-solid fa-eye"></i></button>
</div>
<div class="row">
<div class="col mb--2 ml--2" id="ioc_edition_btn" style="display:none;">
</div>
</div>
<div class="row" style="margin-left:0px;">
<div class="col-12" id="container_ioc_desc_content">
<div id="ioc_description" class="mr-2" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}">{% if ioc and ioc.ioc_description %}{{ ioc.ioc_description }}{% endif %}</div>
<textarea id="ioc_desc_content" rows="10" cols="82" style="display: none"></textarea>
</div>
<div class="col-12" id="container_ioc_description" style="display:none">
<div id="target_ioc_desc"></div>
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="ioc_tags">IOC tags
</label>
<input type="text" id="ioc_tags"
class="form-control col-md-12" {% if ioc.ioc_tags %} value="{{ ioc.ioc_tags }}" {% endif %}/>
</div>
<div class='invalid-feedback' id='ioc-invalid-msg'></div>
</div>
</form>
</div>
</div>
{% include 'modals/modal_attributes_tabs.html' %}
</div>
{% if ioc.ioc_id %}
<button type="button" class="btn btn-outline-danger mt-5"
onclick="delete_ioc('{{ ioc.ioc_id }}');">Delete</button>
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right" id="submit_new_ioc"
onclick="update_ioc('{{ ioc.ioc_id }}');">Update</button>
{% else %}
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
id="submit_new_ioc">Save</button>
{% endif %}
</div>
<script>
$('form#form_new_ioc').validate();
$('#ioc_tags').amsifySuggestags({
printValues: false,
suggestions: [ {% for tag in tags %} '{{ tag }}', {% endfor %} ]
});
$('#ioc_type_id').selectpicker({
liveSearch: true,
title: "None",
style: "btn-outline-white",
size: 10
});
$('#ioc_tlp_id').selectpicker({
liveSearch: true,
title: "None",
style: "btn-outline-white",
});
{% if ioc.ioc_id %}
$('#ioc_type_id').selectpicker('val', '{{ioc.ioc_type_id}}');
$('#ioc_tlp_id').selectpicker('val', '{{ioc.ioc_tlp_id}}');
{% else %}
$('#ioc_tlp_id').selectpicker('val', '2');
{% endif %}
</script>

View File

@@ -0,0 +1,162 @@
<div class="modal-header">
<div class="col md-12">
<div class="row">
<div class="col align-self-center">
<h4 class="modal-title mr-4">Add multiple assets</h4>
<small><a class="text-muted"></a></small>
</div>
{% include 'modals/modal_attributes_nav.html' %}
<div class="col">
<div class="row float-right">
<button class="float-right btn bg-transparent" title="Minimize" onclick="modal_minimized('modal_add_asset', 'Add asset');"> <i class='fa fa-minus'></i> </button>
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
</div>
</div>
</div>
</div>
<div class="modal-body">
<div role="tabpanel">
<div class="tab-content">
<div class="tab-pane active" id="details">
<div class="container col-md-12">
<form method="post" action="" id="form_new_assets">
<div class="col-md-12 col-lg-12 col-sm-12">
{{ form.hidden_tag() }}
<div class="form-row ml-2">
<div class="form-group col-12">
<label for="asset_type" class="placeholder">Assets Type *</label>
{{ form.asset_type_id(class="selectpicker form-control") }}
</div>
<div class="form-group col-12">
<label for="name" class="placeholder">Assets Name *</label>
<textarea class="form-control sizable-textarea" autocomplete="off" rows="1" name="assets_name" id="assets_name" placeholder="One asset per line"></textarea>
</div>
</div>
<div class="form-group mt-3">
<label for="asset_description" class="placeholder">Description</label>
<div class="md_description_field">
<div class="form-group mt--2">
<button type="button" class="float-right icon-note btn btn-circle btn-sm mt-2" onclick="edit_in_asset_desc();">
</button>
<button type="button" style="display: none;" class="btn btn-dark btn-sm float-right mr-2 mt-2" onclick="preview_asset_description();" id="asset_preview_button"><i class="fa-solid fa-eye"></i></button>
</div>
<div class="row">
<div class="col mb--2 ml--2" id="asset_edition_btn" style="display:none;">
</div>
</div>
<div class="row" style="margin-left:0px;">
<div class="col-12" id="container_asset_desc_content">
<div id="asset_description" class="mr-2" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}">{% if asset %}{{ asset.asset_description }}{% endif %}</div>
<textarea id="asset_desc_content" rows="10" cols="82" style="display: none"></textarea>
</div>
<div class="col-12" id="container_asset_description" style="display:none">
<div id="target_asset_desc"></div>
</div>
</div>
</div>
</div>
<div class="form-row ml-2">
<div class="form-group col-6">
<label for="asset_domain" class="placeholder">Domain</label>
{{ form.asset_domain(class='form-control', autocomplete="off") }}
</div>
<div class="form-group col-6">
<label for="asset_ip" class="placeholder">IP</label>
{{ form.asset_ip(class='form-control', autocomplete="off") }}
</div>
</div>
<div class="form-group">
<a class="btn btn-light btn-sm" data-toggle="collapse" href="#collapseAddInfo" role="button" aria-expanded="false" aria-controls="collapseAddInfo">> Additional information</a>
<div class="collapse" id="collapseAddInfo">
<div class="card card-body">
<label for="asset_info" class="placeholder">Additional information</label>
{{ form.asset_info(class='form-control col-md-12 col-sm-12 sizable-textarea', autocomplete="off") }}
</div>
</div>
</div>
<div class="form-row ml-2">
<div class="form-group col-6">
<label for="asset_compromise_status_id" class="placeholder mt-2">Compromise Status </label>
{{ form.asset_compromise_status_id(class="selectpicker col-9") }}
</div>
<div class="form-group col-6">
<label for="analysis_status_id" class="placeholder mt-2">Analysis Status </label>
{{ form.analysis_status_id(class="selectpicker col-9 float-right") }}
</div>
</div>
<div class="form-group">
<label for="asset_tags">Asset tags
</label>
<input type="text" id="asset_tags"
class="form-control col-md-12"/>
</div>
<div class="form-group" data-select2-id="7">
<label>Related IOC</label>
<div class="select2-input ml-12" data-select2-id="6">
<select id="ioc_links" name="ioc_links" class="form-control select2-hidden-accessible ml-12" multiple="" data-select2-id="ioc_links" tabindex="-1" aria-hidden="true" style="width: 100%">
</select>
</div>
</div>
</div>
</form>
</div>
</div>
{% include 'modals/modal_attributes_tabs.html' %}
</div>
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right" id="submit_new_assets">Save</button>
</div>
</div>
<script>
$('form#form_new_case').validate();
$('#asset_tags').amsifySuggestags({
printValues: false,
suggestions: [ {% for tag in tags %} '{{ tag }}', {% endfor %} ]
});
$('#asset_type_id').selectpicker({
liveSearch: true,
title: "None",
style: "btn-outline-white",
});
$('#analysis_status_id').selectpicker({
liveSearch: true,
title: "None",
style: "btn-outline-white"
});
$('#analysis_status_id').selectpicker('val', '1');
$('#asset_compromise_status_id').selectpicker({
liveSearch: true,
title: "To be determined",
style: "btn-outline-white"
});
$('#asset_compromise_status_id').selectpicker('val', '0');
</script>
{% if ioc %}
<script>
var data = [
{% for e in ioc %}
{
id: {{ e.ioc_id }},
text: {{ e.ioc_value| tojson }}
},
{% endfor %}
];
$('#ioc_links').select2({ data: data });
</script>
{% endif %}
{% if ioc_prefill %}
<script>
$('#ioc_links').val([
{% for ioc in ioc_prefill %} {{ ioc[0] }}, {% endfor %}
]);
$('#ioc_links').trigger('change');
</script>
{% endif %}

View File

@@ -0,0 +1,98 @@
<div class="modal-header">
<div class="col md-12">
<div class="row">
<div class="col align-self-center">
<h4 class="modal-title mr-4">{% if rfile.id %}Edit evidence #{{rfile.id}}{% else %}Register evidence{% endif %}</h4>
{% if rfile.id %}<small><i class="text-muted">#{{ rfile.file_uuid }}</i></small>{% endif %}
</div>
{% include 'modals/modal_attributes_nav.html' %}
<div class="col">
<div class="row float-right">
{% if rfile.id %}
<div class="dropdown">
<button class="btn bg-transparent pull-right" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span aria-hidden="true"><i class="fas fa-ellipsis-v"></i></span>
</button>
<div class="dropdown-menu pull-right" id="evidence_modal_quick_actions" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="#" onclick='copy_object_link({{rfile.id}});return false;'><i class="fa fa-share mr-2"></i>Share</a>
<a class="dropdown-item" href="#" onclick='copy_object_link_md("evidence", {{rfile.id}});return false;'><i class="fa-brands fa-markdown mr-2"></i>Markdown Link</a>
</div>
</div>
<button type="button" class="btn bg-transparent btn-xs" onclick="comment_element({{ rfile.id }}, 'evidences')" title="Comments">
<span class="btn-label">
<i class="fa-solid fa-comments"></i><span class="notification" id="object_comments_number">{{ comments_map|length }}</span>
</span>
</button>
{% endif %}
<button class="float-right btn bg-transparent" title="Minimize" onclick="modal_minimized('modal_add_rfiles', 'Edit evidence #{{rfile.id}}');"> <i class='fa fa-minus'></i> </button>
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
</div>
</div>
</div>
</div>
<div class="modal-body">
<div role="tabpanel">
<div class="tab-content">
<div class="tab-pane active" id="details">
<div class="container col-md-12">
<form method="post" action="" id="form_edit_rfile">
<div class="col-md-12 col-lg-12 col-sm-12">
<div class="form-group">
<label for="rfile_filename" class="placeholder">Filename *</label>
<input class="form-control" placeholder="Filename" id="filename" required name="filename" value="{{ rfile.filename }}"/>
</div>
<div class="form-group">
<label for="rfile_size" class="placeholder">File size (bytes) *</label>
<input class="form-control" placeholder="Size in bytes" id="file_size" name="file_size" value="{{ rfile.file_size }}"/>
</div>
<div class="form-group">
<label for="rfile_hash" class="placeholder">File Hash</label>
<input class="form-control" placeholder="Hash" id="file_hash" name="file_hash" value="{{ rfile.file_hash }}"/>
</div>
<div class="form-group">
<label for="rfile_desc" class="placeholder">File description</label>
<div class="md_description_field">
<div class="form-group mt--2">
<button type="button" class="float-right icon-note btn btn-circle btn-sm mt-2" onclick="edit_in_evidence_desc();" >
</button>
<button type="button" style="display: none;" class="btn btn-dark btn-sm float-right mr-2 mt-2"
onclick="preview_evidence_description();" id="evidence_preview_button"><i class="fa-solid fa-eye"></i></button>
</div>
<div class="row">
<div class="col mb--2 ml--2" id="evidence_edition_btn" style="display:none;">
</div>
</div>
<div class="row" style="margin-left:0px;">
<div class="col-12" id="container_evidence_desc_content">
<div id="evidence_description" class="mr-2" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}">{% if rfile %}{{ rfile.file_description }}{% endif %}</div>
<textarea id="evidence_desc_content" rows="10" cols="82" style="display: none"></textarea>
</div>
<div class="col-12" id="container_evidence_description" style="display:none">
<div id="target_evidence_desc"></div>
</div>
</div>
</div>
</div>
<div class="form-group">
<p>Automatically compute file information by selecting it below. The file will not be uploaded nor saved.</p>
<input id="input_autofill" type="file">
<button class="btn btn-sm" type="button" onclick="get_hash()" id="btn_rfile_proc">Process</button>
</div>
</div>
</form>
</div>
</div>
{% include 'modals/modal_attributes_tabs.html' %}
</div>
{% if rfile.id %}
<button type="button" class="btn btn-outline-danger mt-5"
onclick="delete_rfile('{{ rfile.id }}');">Delete</button>
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
onclick="update_rfile('{{ rfile.id }}');" id="submit_new_rfiles">Update</button>
{% else %}
<button type="button" class="btn btn-outline-success float-right" onclick="add_rfile();">Register</button>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,139 @@
<div class="modal-header">
<div class="col md-12">
<div class="row">
<div class="col align-self-center">
<h4 class="modal-title mr-4">{% if task.id %} Task ID #{{ task.id }}{% else %} Add task {% endif %}</h4>
<small><i class="text-muted">#{{ task.task_uuid }}</i></small>
</div>
{% include 'modals/modal_attributes_nav.html' %}
<div class="col">
<div class="row float-right">
{% if task.id %}
<div class="dropdown">
<button class="btn bg-transparent pull-right" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span aria-hidden="true"><i class="fas fa-ellipsis-v"></i></span>
</button>
<div class="dropdown-menu pull-right" id="task_modal_quick_actions" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="#" onclick='copy_object_link({{task.id}});return false;'><i class="fa fa-share mr-2"></i>Share</a>
<a class="dropdown-item" href="#" onclick='copy_object_link_md("task", {{task.id}});return false;'><i class="fa-brands fa-markdown mr-2"></i>Markdown Link</a>
</div>
</div>
<button type="button" class="btn bg-transparent btn-xs" onclick="comment_element({{ task.id }}, 'tasks')" title="Comments">
<span class="btn-label">
<i class="fa-solid fa-comments"></i><span class="notification" id="object_comments_number">{{ comments_map|length }}</span>
</span>
</button>
{% endif %}
<button class="float-right btn bg-transparent" title="Minimize" onclick="modal_minimized('modal_add_task', '{% if task.id %} Task ID #{{ task.id }}{% else %} Add task {% endif %}');"> <i class='fa fa-minus'></i> </button>
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
</div>
</div>
</div>
</div>
<div class="modal-body">
<div role="tabpanel">
<div class="tab-content">
<div class="tab-pane active" id="details">
<div class="container col-md-12">
<form method="post" action="" id="form_new_task">
<div class="col-md-12 col-lg-12 col-sm-12">
{{ form.hidden_tag() }}
<div class="form-group mt-3 row">
<div class="col-6 col-xs-12">
<label for="task_assignee_id" class="placeholder">Assigned to *</label>
{{ form.task_assignees_id(class="selectpicker col-12", data_actions_box="true", data_dropup_auto="false") }}
</div>
<div class="col-6 col-xs-12">
<label for="task_status_id" class="placeholder">Status *</label>
{{ form.task_status_id(class="selectpicker col-12") }}
</div>
</div>
<div class="form-group">
<label for="task_title" class="placeholder">{{ form.task_title.label.text }} *</label>
{{ form.task_title(class='form-control col-md-12 col-sm-12', autocomplete="off") }}
</div>
<div class="form-group mt-3">
<label for="task_description" class="placeholder">Description</label>
<div class="md_description_field">
<div class="form-group mt--2">
<button type="button" class="float-right icon-note btn btn-circle btn-sm mt-2" onclick="edit_in_task_desc();">
</button>
<button type="button" style="display: none;" class="btn btn-dark btn-sm float-right mr-2 mt-2"
onclick="preview_task_description();" id="task_preview_button"><i class="fa-solid fa-eye"></i></button>
</div>
<div class="row">
<div class="col mb--2 ml--2" id="task_edition_btn" style="display:none;">
</div>
</div>
<div class="row" style="margin-left:0px;">
<div class="col-12" id="container_task_desc_content">
<div id="task_description" class="mr-2" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}">{% if task.task_description %}{{ task.task_description }}{% endif %}</div>
<textarea id="task_desc_content" rows="10" cols="82" style="display: none"></textarea>
</div>
<div class="col-12" id="container_task_description" style="display:none">
<div id="target_task_desc"></div>
</div>
</div>
</div>
</div>
<div class="form-group mt-3">
<label for="task_tags">Task tags
</label>
<input type="text" id="task_tags"
class="form-control col-md-12" {% if task.task_tags %} value="{{ task.task_tags }}" {% endif %}/>
</div>
</div>
</form>
</div>
</div>
{% include 'modals/modal_attributes_tabs.html' %}
</div>
</div>
{% if task.id %}
<button type="button" class="btn btn-outline-danger mt-5"
onclick="delete_task({{ task.id }});">Delete</button>
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right" id="submit_new_task"
onclick="update_task({{ task.id }});">Update</button>
{% else %}
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
id="submit_new_task">Save</button>
{% endif %}
</div>
<script>
var data = [];
if (current_users_list.length === 0) {
refresh_users(do_list_users, [{% for assignee in task.task_assignees %} {{ assignee.id }}, {% endfor %}]);
} else {
do_list_users(current_users_list, [{% for assignee in task.task_assignees %} {{ assignee.id }}, {% endfor %}]);
}
$('form#form_new_task').validate();
$('#task_tags').amsifySuggestags({
printValues: false,
suggestions: [ {% for tag in tags %} '{{ tag }}', {% endfor %} ]
});
$('#task_status_id').selectpicker({
liveSearch: true,
title: "Select task status"
});
{% if task.task_status_id %}
$('#task_status_id').selectpicker('val', '{{task.task_status_id}}');
{% else %}
$('#task_status_id').selectpicker('val', 'To do');
{% endif %}
$('[data-toggle="popover"]').popover();
</script>

View File

@@ -0,0 +1,33 @@
<div class="modal-content">
<form method="post" action="" id="form_add_asset">
<div class="modal-header">
<h5>Add asset to graph</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="asset" class="mr-4">Add an existing asset to the case graph.<br>If you want to create a new asset, please go to the Asset tab.
</label>
<select class="selectpicker form-control bg-outline-success dropdown-submenu" data-show-subtext="true" data-live-search="true" id="asset">
{% for asset in assets_list %}
<option value="{{ asset.IP }}">{{ asset.Name }} </option>
{% endfor %}
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success float-right" id="submit_add_asset">Add</button>
</div>
</form>
</div><!-- /.modal-content -->
<script>
$('#asset').selectpicker({
liveSearch: true,
title: "None",
style: "Bootstrap 4: 'btn-outline-primary'",
});
</script>

View File

@@ -0,0 +1,54 @@
<link rel="stylesheet" href="/static/assets/css/dropzone.css">
<div class="modal-header">
<h4 class="modal-title mt-2 mr-4">Processing pipelines</h4>
<button type="button" class="pull-right btn btn-white" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
<div class="modal-body">
<div class="container col-md-12">
<div class="row">
<div class="col-12">
<p>Select processing pipeline and drop analysis files below. Press Process to start the processing. Do not close the page until the upload is finished.<br/>
Supports up to 40 files and 10GB at once.</p>
<div class="form-group">
<div class="form-group">
<label for="import_pipeline" class="placeholder">Processing pipeline</label>
<i class="fas fa-question-circle mr-2" data-toggle="popover"
title="Pipelines"
data-content="Pipelines are the way files dropped below are processed. Each pipelines handles a different type of file."></i>
{{ form.pipeline(class="selectpicker pl--6 btn-outline-white", id="update_pipeline_selector") }}
</div>
<div class="form-group col-md-6 mb-2">
{% for itm in pipeline_args %}
{% for tm in itm[2] %}
<div class="input-group mb-4 control-update-pipeline-args control-update-pipeline-{{itm[0]}}">
<div class="input-group-prepend">
<span class="input-group-text">{{tm[0]}} ({{tm[1]}})</span>
</div>
<input class="form-control update-{{itm[0]}}" id="{{tm[0]}}" name="{{tm[0]}}" type="text" value="" {{tm[1]}}>
</div>
{% endfor %}
{% endfor %}
</div>
<div class="tab-content col-md-12">
<div class="dropzone col-md-12" id="files_drop_1">
</div>
</div>
</div>
<span id="msg_submit" class="ml-4"></span>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default float-left" data-dismiss="modal">Dismiss</button>
<button type="button" class="btn btn-outline-success float-right"
id="submit_update_case" onclick="submit_update_casefn();">Process</button>
</div>
<script src="/static/assets/js/plugin/dropzone/dropzone.js"></script>
<script src="/static/assets/js/iris/case.pipelines.js"></script>

View File

@@ -0,0 +1,52 @@
<div class="modal-header">
<div style="display:none;" id="current_username">{{current_user.user}}</div>
<div class="col md-12">
<div class="row">
<div class="col-8">
<h4 class="modal-title mt-2 mr-4 text-truncate">Comments on <i>{{ title }}</i></h4>
</div>
<div class="col">
<div class="row float-right">
<button class="btn bg-transparent" title="Minimize" onclick="modal_minimized('modal_comment', 'Comment {{ element_type }}');"> <i class='fa fa-minus'></i> </button>
<button type="button" class="float-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
</div>
</div>
</div>
</div>
<div class="modal-body comments-listing">
<div id="comments_list">
</div>
</div>
<div class="modal-footer">
<div class="col">
<div class="row">
<div class="col-12">
<div class="row">
<div class="col mb--2 ml--2" id="comment_edition_btn">
</div>
</div>
<div class="row mb-3">
<div class="col-12 comment-content" id="container_comment_content">
<div id="comment_message" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}"></div>
<textarea id="comment_content" rows="2" style="display: none"></textarea>
</div>
<div class="col-12 comment-content" id="container_comment_preview" style="display:none;">
<div id="target_comment_content"></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12" id="container_comment_submit">
<button type="button" class="btn btn-primary btn-sm float-right ml-2" id="comment_submit" onclick="save_comment({{ element_id }}, '{{ element_type }}');"><i class="fa-regular fa-paper-plane"></i> Comment</button>
<button type="button" class="btn btn-primary btn-sm float-right ml-2" id="comment_edition" style="display: none;" onclick="save_edit_comment({{ element_id }}, '{{ element_type }}');"><i class="fa-regular fa-paper-plane"></i> Save</button>
<button type="button" class="btn btn-danger btn-sm float-right ml-2" id="cancel_edition" style="display: none;" onclick="cancel_edition();"><i class="fa-solid fa-xmark"></i> Cancel</button>
<button type="button" class="btn btn-dark btn-sm float-right ml-2" onclick="preview_comment();" id="comment_preview_button"><i class="fa-solid fa-eye"></i> Preview</button>
<button type="button" class="btn btn-light btn-sm float-left" onclick="load_comments({{ element_id }}, '{{ element_type }}', null, true);"><i class="fa-solid fa-refresh"></i> Refresh</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,33 @@
<div class="modal-xl modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title mt-1 mr-4">Timeline filtering help</h4>
<button type="button" class="float-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
<div class="modal-body mb-2">
<div class="row">
<div class="col-12">
<p>The timeline can be filtered thanks to simple queries. The query schema is : <code>target_element:search_value AND target_element2:search_value2</code>.<br/>
There is no <code>OR</code> condition and searching without target does not work.
<p>The following target elements can be used to filter :</p>
<ul>
<li><code>asset</code>: Asset linked to the event</li>
<li><code>ioc</code>: IOC linked to the event</li>
<li><code>tag</code>: Tag within the event</li>
<li><code>title</code>: Title of the event</li>
<li><code>description</code>: Description of the event</li>
<li><code>raw</code> : Raw event content</li>
<li><code>category</code>: Category of the event</li>
<li><code>source</code>: Source of the event</li>
<li><code>startDate</code>: Start date to filter with</li>
<li><code>endDate</code>: End date to filter with</li>
</ul>
The dates filters uses the same guessing as the date parser in events, so a lots of format are handled.<br/>
Example of filter :
<code>asset: DESKTOP-X5487 AND description: rdp connection to AND source: Windows Security</code>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,98 @@
<div class="modal-header">
<div class="col md-12">
<div class="row">
<div class="col align-self-center">
<h4 class="modal-title mr-4">Note #{{ note.note_id }}</h4>
<small><i class="text-muted">#{{ note.note_uuid }}</i></small>
</div>
{% include 'modals/modal_attributes_nav.html' %}
<div class="col">
<div class="row float-right">
<div class="avatar-group-note mt-2 float-right" id="ppl_list_viewing">
</div>
<button class="btn bg-transparent pull-right" title="Toggle focus mode" id="focus_mode" onclick="toggle_focus_mode();return false;">
<span aria-hidden="true"><i class="fas fas fa-coffee"></i></span>
</button>
<button type="button" class="btn bg-transparent btn-xs" onclick="comment_element({{ note.note_id }}, 'notes')" title="Comments">
<span class="btn-label">
<i class="fa-solid fa-comments"></i><span class="notification" id="object_comments_number">{{ comments_map|length }}</span>
</span>
</button>
<div class="dropdown">
<button class="btn bg-transparent pull-right" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span aria-hidden="true"><i class="fas fa-ellipsis-v"></i></span>
</button>
<div class="dropdown-menu pull-right" id="note_modal_quick_actions" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="#" onclick='copy_object_link({{ note.note_id }});return false;'><i class="fa fa-share mr-2"></i>Share</a>
<a class="dropdown-item" href="#" onclick='copy_object_link_md("note",{{ note.note_id }});return false;'><i class="fa-brands fa-markdown mr-2"></i>Markdown Link</a>
</div>
</div>
<button class="float-right btn bg-transparent" title="Minimize" onclick="modal_minimized('modal_note_detail', '{{ note.title }}');"> <i class='fa fa-minus'></i> </button>
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
</div>
</div>
</div>
</div>
<div class="modal-body">
<div role="tabpanel">
<div class="tab-content">
<div class="tab-pane active" id="details">
<form method="post" action="" id="form_note">
<iris_notein style="display: none;">{{ note.note_id }}</iris_notein>
{{ note.hidden_tag() }}
<div class="container col-md-12">
<div class="form-group">
<label>Note title *</label>
{{ note.note_title(class='form-control input') }}
</div>
<div class="row mb-1 mt-3">
<div class="col">
<span class="badge badge-light" id="content_typing"></span>
<span class="badge badge-light" id="content_last_saved_by"></span>
</div>
</div>
<div class="row mb-1 mt-3">
<div class="col-10" id="notes_edition_btn">
<div class="btn btn-sm btn-light mr-1 " title="CTRL-S" id="last_saved" onclick="save_note( this );"><i class="fa-solid fa-file-circle-check"></i></div>
<div class="btn btn-sm btn-light mr-1 " title="CTRL-B" onclick="note_editor.insertSnippet('**${1:$SELECTION}**');note_editor.focus();"><i class="fa-solid fa-bold"></i></div>
<div class="btn btn-sm btn-light mr-1" title="CTRL-I" onclick="note_editor.insertSnippet('*${1:$SELECTION}*');note_editor.focus();"><i class="fa-solid fa-italic"></i></div>
<div class="btn btn-sm btn-light mr-1" title="CTRL-SHIFT-1" onclick="note_editor.insertSnippet('# ${1:$SELECTION}');note_editor.focus();">H1</div>
<div class="btn btn-sm btn-light mr-1" title="CTRL-SHIFT-2" onclick="note_editor.insertSnippet('## ${1:$SELECTION}');note_editor.focus();">H2</div>
<div class="btn btn-sm btn-light mr-1" title="CTRL-SHIFT-3" onclick="note_editor.insertSnippet('### ${1:$SELECTION}');note_editor.focus();">H3</div>
<div class="btn btn-sm btn-light mr-1" title="CTRL-SHIFT-4" onclick="note_editor.insertSnippet('#### ${1:$SELECTION}');note_editor.focus();">H4</div>
<div class="btn btn-sm btn-light mr-1" title="CTRL+\`" onclick="note_editor.insertSnippet('```${1:$SELECTION}```');note_editor.focus();"><i class="fa-solid fa-code"></i></div>
<div class="btn btn-sm btn-light mr-1" title="CTRL-K" onclick="note_editor.insertSnippet('[${1:$SELECTION}](url)');note_editor.focus();"><i class="fa-solid fa-link"></i></div>
<div class="btn btn-sm btn-light mr-1" title="Insert table" onclick="note_editor.insertSnippet('|\t|\t|\t|\n|--|--|--|\n|\t|\t|\t|\n|\t|\t|\t|');note_editor.focus();"><i class="fa-solid fa-table"></i></div>
<div class="btn btn-sm btn-light mr-1" title="Insert bullet list" onclick="note_editor.insertSnippet('\n- \n- \n- ');note_editor.focus();"><i class="fa-solid fa-list"></i></div>
<div class="btn btn-sm btn-light mr-1" title="Insert numbered list" onclick="note_editor.insertSnippet('\n1. a \n2. b \n3. c ');note_editor.focus();"><i class="fa-solid fa-list-ol"></i></div>
<div class="btn btn-sm btn-light mr-1" title="Toggle editor expansion" onclick="toggle_max_editor();"><i class="fa-solid fa-maximize"></i></div>
<div class="btn btn-sm btn-transparent mr-1" title="Help" onclick="get_md_helper_modal();"><i class="fa-solid fa-question-circle"></i></div>
</div>
<div class="col">
<button type="button" class="float-right icon-note btn btn-circle btn-sm" onclick="edit_innote();"></button>
</div>
</div>
<div class="row">
<div class="col-md-6" id="container_note_content">
<div style="display: none" id="fetched_crc"></div>
<div id="editor_detail" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}">{{ note.content }}</div>
<textarea id="note_content" rows="10" cols="82" style="display: none"></textarea>
</div>
<div class="col-md-6" id="ctrd_notesum">
<div id="targetDiv"></div>
</div>
</div>
</div>
</form>
</div>
{% include 'modals/modal_attributes_tabs.html' %}
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-danger mr-auto" onclick="delete_note(this, {{ ncid }});">Delete note</button>
<button type="button" class="btn btn-default" onclick="save_note( this, {{ ncid }} );" id="btn_save_note">Save </button>
</div>

View File

@@ -0,0 +1,24 @@
{% extends "layouts/default.html" %}
{% block title %} Case Error {% endblock title %}
{% block stylesheets %}
{% endblock stylesheets %}
{% block content %}
<br />
<br />
<h2 class="mx-5">No case found for you !</h2><br/><br/>
<h3 class="font-weight-light mx-5">The page youre looking for is only available when a case is selected.</h3><br/>
<h3 class="font-weight-light mx-5">Please press the <b><i class="flaticon-repeat"></i></b> button on the top right to select one.</h3>
{% endblock content %}
{% block javascripts %}
{% endblock javascripts %}

View File

@@ -0,0 +1,25 @@
#!/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 ------------------------------------------------
# VARS ---------------------------------------------------
# CONTENT ------------------------------------------------

View File

@@ -0,0 +1,145 @@
#!/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.
from flask import Blueprint
# IMPORTS ------------------------------------------------
from flask import redirect
from flask import request
from flask_login import current_user
from app import app
from app import cache
from app import db
from app.datamgmt.context.context_db import ctx_get_user_cases
from app.datamgmt.context.context_db import ctx_search_user_cases
from app.models.authorization import Permissions
from app.models.cases import Cases
from app.models.models import Client
from app.util import ac_api_requires
from app.util import not_authenticated_redirection_url
from app.util import response_success
# CONTENT ------------------------------------------------
ctx_blueprint = Blueprint(
'context',
__name__,
template_folder='templates'
)
@ctx_blueprint.route('/context/set', methods=['POST'])
def set_ctx():
"""
Set the context elements of a user i.e the current case
:return: Page
"""
if not current_user.is_authenticated:
return redirect(not_authenticated_redirection_url())
ctx = request.form.get('ctx')
ctx_h = request.form.get('ctx_h')
current_user.ctx_case = ctx
current_user.ctx_human_case = ctx_h
db.session.commit()
update_user_case_ctx()
return response_success(msg="Saved")
@app.context_processor
def iris_version():
return dict(iris_version=app.config.get('IRIS_VERSION'),
organisation_name=app.config.get('ORGANISATION_NAME'),
std_permissions=Permissions,
demo_domain=app.config.get('DEMO_DOMAIN', None))
@app.context_processor
@cache.cached(timeout=3600, key_prefix='iris_has_updates')
def has_updates():
return dict(has_updates=False)
@ctx_blueprint.route('/context/get-cases/<int:max_results>', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def cases_context(max_results, caseid):
# Get all investigations not closed
datao = ctx_get_user_cases(current_user.id, max_results=max_results)
return response_success(data=datao)
@ctx_blueprint.route('/context/search-cases', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def cases_context_search(caseid):
search = request.args.get('q')
if not search:
return response_success(data=[])
# Get all investigations not closed
datao = ctx_search_user_cases(search, current_user.id, max_results=100)
return response_success(data=datao)
def update_user_case_ctx():
"""
Retrieve a list of cases for the case selector
:return:
"""
# Get all investigations not closed
res = Cases.query.with_entities(
Cases.name,
Client.name,
Cases.case_id,
Cases.close_date) \
.join(Cases.client) \
.order_by(Cases.open_date) \
.all()
data = [row for row in res]
if current_user and current_user.ctx_case:
# If the current user have a current case,
# Look for it in the fresh list. If not
# exists then remove from the user context
is_found = False
for row in data:
if row[2] == current_user.ctx_case:
is_found = True
break
if not is_found:
# The case does not exist,
# Removes it from the context
current_user.ctx_case = None
current_user.ctx_human_case = "Not set"
db.session.commit()
# current_user.save()
app.jinja_env.globals.update({
'cases_context_selector': data
})
return data

View File

@@ -0,0 +1,381 @@
#!/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.
import marshmallow
# IMPORTS ------------------------------------------------
from datetime import datetime
from datetime import timedelta
from flask import Blueprint
from flask import redirect
from flask import render_template
from flask import request
from flask import session
from flask import url_for
from flask_login import current_user
from flask_login import logout_user
from flask_wtf import FlaskForm
from sqlalchemy import distinct
from app import app
from app import db
from app.datamgmt.dashboard.dashboard_db import get_global_task, list_user_cases, list_user_reviews
from app.datamgmt.dashboard.dashboard_db import get_tasks_status
from app.datamgmt.dashboard.dashboard_db import list_global_tasks
from app.datamgmt.dashboard.dashboard_db import list_user_tasks
from app.forms import CaseGlobalTaskForm
from app.iris_engine.module_handler.module_handler import call_modules_hook
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import User
from app.models.cases import Cases
from app.models.models import CaseTasks
from app.models.models import GlobalTasks
from app.models.models import TaskStatus
from app.models.models import UserActivity
from app.schema.marshables import CaseTaskSchema, CaseSchema, CaseDetailsSchema
from app.schema.marshables import GlobalTasksSchema
from app.util import ac_api_requires
from app.util import ac_requires
from app.util import not_authenticated_redirection_url
from app.util import response_error
from app.util import response_success
# CONTENT ------------------------------------------------
dashboard_blueprint = Blueprint(
'index',
__name__,
template_folder='templates'
)
# Logout user
@dashboard_blueprint.route('/logout')
def logout():
"""
Logout function. Erase its session and redirect to index i.e login
:return: Page
"""
if session['current_case']:
current_user.ctx_case = session['current_case']['case_id']
current_user.ctx_human_case = session['current_case']['case_name']
db.session.commit()
track_activity("user '{}' has been logged-out".format(current_user.user), ctx_less=True, display_in_ui=False)
logout_user()
return redirect(not_authenticated_redirection_url(request_url='/'))
@dashboard_blueprint.route('/dashboard/case_charts', methods=['GET'])
@ac_api_requires()
def get_cases_charts(caseid):
"""
Get case charts
:return: JSON
"""
res = Cases.query.with_entities(
Cases.open_date
).filter(
Cases.open_date > (datetime.utcnow() - timedelta(days=365))
).order_by(
Cases.open_date
).all()
retr = [[], []]
rk = {}
for case in res:
month = "{}/{}/{}".format(case.open_date.day, case.open_date.month, case.open_date.year)
if month in rk:
rk[month] += 1
else:
rk[month] = 1
retr = [list(rk.keys()), list(rk.values())]
return response_success("", retr)
@dashboard_blueprint.route('/')
def root():
if app.config['DEMO_MODE_ENABLED'] == 'True':
return redirect(url_for('demo-landing.demo_landing'))
return redirect(url_for('index.index'))
@dashboard_blueprint.route('/dashboard')
@ac_requires()
def index(caseid, url_redir):
"""
Index page. Load the dashboard data, create the add customer form
:return: Page
"""
if url_redir:
return redirect(url_for('index.index', cid=caseid if caseid is not None else 1, redirect=True))
msg = None
# Retrieve the dashboard data from multiple sources.
# Quite fast as it is only counts.
user_open_case = Cases.query.filter(
Cases.owner_id == current_user.id,
Cases.close_date == None
).count()
data = {
"user_open_count": user_open_case,
"cases_open_count": Cases.query.filter(Cases.close_date == None).count(),
"cases_count": Cases.query.with_entities(distinct(Cases.case_id)).count(),
}
# Create the customer form to be able to quickly add a customer
form = FlaskForm()
return render_template('index.html', data=data, form=form, msg=msg)
@dashboard_blueprint.route('/global/tasks/list', methods=['GET'])
@ac_api_requires()
def get_gtasks(caseid):
tasks_list = list_global_tasks()
if tasks_list:
output = [c._asdict() for c in tasks_list]
else:
output = []
ret = {
"tasks_status": get_tasks_status(),
"tasks": output
}
return response_success("", data=ret)
@dashboard_blueprint.route('/user/cases/list', methods=['GET'])
@ac_api_requires()
def list_own_cases(caseid):
cases = list_user_cases(
request.args.get('show_closed', 'false', type=str).lower() == 'true'
)
return response_success("", data=CaseDetailsSchema(many=True).dump(cases))
@dashboard_blueprint.route('/global/tasks/<int:cur_id>', methods=['GET'])
@ac_api_requires()
def view_gtask(cur_id, caseid):
task = get_global_task(task_id=cur_id)
if not task:
return response_error(f'Global task ID {cur_id} not found')
return response_success("", data=task._asdict())
@dashboard_blueprint.route('/user/tasks/list', methods=['GET'])
@ac_api_requires()
def get_utasks(caseid):
ct = list_user_tasks()
if ct:
output = [c._asdict() for c in ct]
else:
output = []
ret = {
"tasks_status": get_tasks_status(),
"tasks": output
}
return response_success("", data=ret)
@dashboard_blueprint.route('/user/reviews/list', methods=['GET'])
@ac_api_requires()
def get_reviews(caseid):
ct = list_user_reviews()
if ct:
output = [c._asdict() for c in ct]
else:
output = []
return response_success("", data=output)
@dashboard_blueprint.route('/user/tasks/status/update', methods=['POST'])
@ac_api_requires()
def utask_statusupdate(caseid):
jsdata = request.get_json()
if not jsdata:
return response_error("Invalid request")
jsdata = request.get_json()
if not jsdata:
return response_error("Invalid request")
case_id = jsdata.get('case_id') if jsdata.get('case_id') else caseid
task_id = jsdata.get('task_id')
task = CaseTasks.query.filter(CaseTasks.id == task_id, CaseTasks.task_case_id == case_id).first()
if not task:
return response_error(f"Invalid case task ID {task_id} for case {case_id}")
status_id = jsdata.get('task_status_id')
status = TaskStatus.query.filter(TaskStatus.id == status_id).first()
if not status:
return response_error(f"Invalid task status ID {status_id}")
task.task_status_id = status_id
try:
db.session.commit()
except Exception as e:
return response_error(f"Unable to update task. Error {e}")
task_schema = CaseTaskSchema()
return response_success("Updated", data=task_schema.dump(task))
@dashboard_blueprint.route('/global/tasks/add/modal', methods=['GET'])
@ac_api_requires()
def add_gtask_modal(caseid):
task = GlobalTasks()
form = CaseGlobalTaskForm()
form.task_assignee_id.choices = [(user.id, user.name) for user in User.query.filter(User.active == True).order_by(User.name).all()]
form.task_status_id.choices = [(a.id, a.status_name) for a in get_tasks_status()]
return render_template("modal_add_global_task.html", form=form, task=task, uid=current_user.id, user_name=None)
@dashboard_blueprint.route('/global/tasks/add', methods=['POST'])
@ac_api_requires()
def add_gtask(caseid):
try:
gtask_schema = GlobalTasksSchema()
request_data = call_modules_hook('on_preload_global_task_create', data=request.get_json(), caseid=caseid)
gtask = gtask_schema.load(request_data)
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
gtask.task_userid_update = current_user.id
gtask.task_open_date = datetime.utcnow()
gtask.task_last_update = datetime.utcnow()
gtask.task_last_update = datetime.utcnow()
try:
db.session.add(gtask)
db.session.commit()
except Exception as e:
return response_error(msg="Data error", data=e.__str__(), status=400)
gtask = call_modules_hook('on_postload_global_task_create', data=gtask, caseid=caseid)
track_activity("created new global task \'{}\'".format(gtask.task_title), caseid=caseid)
return response_success('Task added', data=gtask_schema.dump(gtask))
@dashboard_blueprint.route('/global/tasks/update/<int:cur_id>/modal', methods=['GET'])
@ac_api_requires()
def edit_gtask_modal(cur_id, caseid):
form = CaseGlobalTaskForm()
task = GlobalTasks.query.filter(GlobalTasks.id == cur_id).first()
form.task_assignee_id.choices = [(user.id, user.name) for user in
User.query.filter(User.active == True).order_by(User.name).all()]
form.task_status_id.choices = [(a.id, a.status_name) for a in get_tasks_status()]
# Render the task
form.task_title.render_kw = {'value': task.task_title}
form.task_description.data = task.task_description
user_name, = User.query.with_entities(User.name).filter(User.id == task.task_userid_update).first()
return render_template("modal_add_global_task.html", form=form, task=task,
uid=task.task_assignee_id, user_name=user_name)
@dashboard_blueprint.route('/global/tasks/update/<int:cur_id>', methods=['POST'])
@ac_api_requires()
def edit_gtask(cur_id, caseid):
form = CaseGlobalTaskForm()
task = GlobalTasks.query.filter(GlobalTasks.id == cur_id).first()
form.task_assignee_id.choices = [(user.id, user.name) for user in User.query.filter(User.active == True).order_by(User.name).all()]
form.task_status_id.choices = [(a.id, a.status_name) for a in get_tasks_status()]
if not task:
return response_error(msg="Data error", data="Invalid task ID", status=400)
try:
gtask_schema = GlobalTasksSchema()
request_data = call_modules_hook('on_preload_global_task_update', data=request.get_json(),
caseid=caseid)
gtask = gtask_schema.load(request_data, instance=task)
gtask.task_userid_update = current_user.id
gtask.task_last_update = datetime.utcnow()
db.session.commit()
gtask = call_modules_hook('on_postload_global_task_update', data=gtask, caseid=caseid)
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
track_activity("updated global task {} (status {})".format(task.task_title, task.task_status_id), caseid=caseid)
return response_success('Task updated', data=gtask_schema.dump(gtask))
@dashboard_blueprint.route('/global/tasks/delete/<int:cur_id>', methods=['POST'])
@ac_api_requires()
def gtask_delete(cur_id, caseid):
call_modules_hook('on_preload_global_task_delete', data=cur_id, caseid=caseid)
if not cur_id:
return response_error("Missing parameter")
data = GlobalTasks.query.filter(GlobalTasks.id == cur_id).first()
if not data:
return response_error("Invalid global task ID")
GlobalTasks.query.filter(GlobalTasks.id == cur_id).delete()
db.session.commit()
call_modules_hook('on_postload_global_task_delete', data=request.get_json(), caseid=caseid)
track_activity("deleted global task ID {}".format(cur_id), caseid=caseid)
return response_success("Task deleted")

View File

@@ -0,0 +1,397 @@
{% extends "layouts/default.html" %}
{% block title %} Dashboard {% endblock title %}
{% block stylesheets %}
<link rel="stylesheet" href="/static/assets/css/suggestags.css">
<link href="/static/assets/css/dataTables.contextualActions.min.css" rel="stylesheet">
<link href="/static/assets/css/dataTables.select.min.css" rel="stylesheet">
{% endblock stylesheets %}
{% block content %}
<div class="panel-header bg-primary-gradient mt--4">
<div class="page-inner py-5">
<div class="d-flex align-items-left align-items-md-center flex-column flex-md-row ">
<div>
<h2 class="text-white pb-2 fw-bold">Dashboard</h2>
</div>
<div class="ml-md-auto py-2 py-md-0">
<a href="/manage/cases?cid={{session['current_case'].case_id}}" class="btn btn-white btn-border btn-round mr-2">
<span class="btn-label">
<i class="fa fa-plus"></i>
</span>
Add case
</a>
</div>
</div>
<div class="chart-container mt--2 mb--2">
<canvas id="htmlLegendsChart" style="display: block; width: auto; height: 100px;" width="auto" height="100px" class="chartjs-render-monitor"></canvas>
</div>
</div>
</div>
<div class="page-inner mt--5">
<div class="row row-card-no-pd">
<div class="col-sm-6 col-md-4">
<div class="card card-stats card-round">
<div class="card-body ">
<div class="row">
<div class="col-5">
<div class="icon-big text-center">
<i class="flaticon-file-1 text-success"></i>
</div>
</div>
<div class="col-7 col-stats">
<div class="numbers">
<p class="card-category">Cases (open / all)</p>
<h4 class="card-title">{{ data.cases_open_count }} / {{ data.cases_count }}</h4>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4">
<div class="card card-stats card-round">
<div class="card-body ">
<div class="row">
<div class="col-5">
<div class="icon-big text-center">
<i class="flaticon-suitcase text-warning"></i>
</div>
</div>
<div class="col-7 col-stats">
<div class="numbers">
<p class="card-category">Attributed open cases</p>
<h4 class="card-title">{{ data.user_open_count }}</h4>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4">
<div class="card card-stats card-round">
<div class="card-body ">
<div class="row">
<div class="col-5">
<div class="icon-big text-center">
<i id='icon_user_task' class=""></i>
</div>
</div>
<div class="col-7 col-stats">
<div class="numbers">
<p class="card-category">Attributed open tasks</p>
<h4 class="card-title" id="user_attr_count"></h4>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row" id="rowPendingCasesReview" style="display: none;">
<div class="col-md-12">
<section class="card">
<div class="card-header">
<div class="card-title">Attributed cases review
<div class="text-faded float-right">
<small id="ureviews_last_updated"></small>
<button type="button" class="btn btn-xs btn-dark ml-2"
onclick="update_ureviews_list();">Refresh
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive" id="ureviews_table_wrapper">
<table class="table display table-striped table-hover" width="100%" cellspacing="0" id="ureview_table" >
<thead>
<tr>
<th>Case name</th>
<th>Review Status</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Case name</th>
<th>Review Status</th>
</tr>
</tfoot>
</table>
</div>
</div>
</section>
</div>
</div>
<div class="row">
<div class="col-md-12">
{{ form.hidden_tag() }}
<section class="card">
<div class="card-header">
<div class="card-title">Attributed open tasks
<div class="text-faded float-right">
<small id="utasks_last_updated"></small>
<button type="button" class="btn btn-xs btn-dark ml-2"
onclick="update_utasks_list();">Refresh
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive" id="utasks_table_wrapper">
<table class="table display table-striped table-hover" width="100%" cellspacing="0" id="utasks_table" >
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Status</th>
<th>Case</th>
<th>Last update</th>
<th>Tags</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Title</th>
<th>Description</th>
<th>Status</th>
<th>Case</th>
<th>Last update</th>
<th>Tags</th>
</tr>
</tfoot>
</table>
</div>
</div>
</section>
</div>
</div>
<div class="row">
<div class="col-md-12">
<section class="card">
<div class="card-header">
<div class="card-title">Attributed open cases
<div class="text-faded float-right">
<small id="ucases_last_updated"></small>
<button type="button" class="btn btn-xs btn-dark ml-2"
onclick="update_ucases_list(true);">Show closed cases
</button>
<button type="button" class="btn btn-xs btn-dark ml-2"
onclick="update_ucases_list();">Refresh
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive" id="ucases_table_wrapper">
<table class="table display table-striped table-hover" width="100%" cellspacing="0" id="ucases_table" >
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Client</th>
<th>Opening date</th>
<th>Tags</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Title</th>
<th>Description</th>
<th>Client</th>
<th>Opening date</th>
<th>Tags</th>
</tr>
</tfoot>
</table>
</div>
</div>
</section>
</div>
</div>
<div class="row">
<div class="col-md-12">
<section class="card">
<div class="card-header">
<div class="card-title">Global tasks
<div class="text-faded float-right">
<small id="tasks_last_updated"></small>
<button type="button" class="btn btn-xs btn-dark ml-2"
onclick="add_gtask();">
Add global task
</button>
<button type="button" class="btn btn-xs btn-dark ml-2"
onclick="update_gtasks_list();">Refresh
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive" id="gtasks_table_wrapper">
<table class="table display table-striped table-hover" width="100%" cellspacing="0" id="gtasks_table" >
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Status</th>
<th>Assigned to</th>
<th>Last update</th>
<th>Tags</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Title</th>
<th>Description</th>
<th>Status</th>
<th>Assigned to</th>
<th>Last update</th>
<th>Tags</th>
</tr>
</tfoot>
</table>
</div>
</div>
</section>
</div>
</div>
</div>
<div class="modal " tabindex="-1" role="dialog" id="modal_add_gtask" data-backdrop="true">
<div class="modal-xl modal-dialog" role="document">
<div class="modal-content" id="modal_add_gtask_content">
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
{% endblock content %}
{% block javascripts %}
<script src="/static/assets/js/plugin/tagsinput/suggesttag.js"></script>
<script src="/static/assets/js/plugin/select/select2.js"></script>
<script src="/static/assets/js/plugin/datatables/dataTables.cellEdit.js"></script>
<script src="/static/assets/js/plugin/datatables/dataTables.buttons.min.js"></script>
<script src="/static/assets/js/plugin/datatables/dataTables.contextualActions.min.js"></script>
<script src="/static/assets/js/plugin/datatables/dataTables.select.min.js"></script>
<script src="/static/assets/js/plugin/datatables/buttons.html5.min.js"></script>
<script src="/static/assets/js/plugin/datatables/buttons.print.min.js"></script>
<script src="/static/assets/js/iris/dashboard.js"></script>
<script src="/static/assets/js/core/charts.js"></script>
<script>
htmlLegendsChart = document.getElementById('htmlLegendsChart').getContext('2d');
$.ajax({
url: '/dashboard/case_charts' + case_param(),
type: "GET",
dataType: "JSON",
success: function (data) {
jsdata = data;
if (jsdata.status == "success") {
// Chart with HTML Legends
var gradientStroke = htmlLegendsChart.createLinearGradient(500, 0, 100, 0);
gradientStroke.addColorStop(0, '#177dff');
gradientStroke.addColorStop(1, '#80b6f4');
var gradientFill = htmlLegendsChart.createLinearGradient(500, 0, 100, 0);
gradientFill.addColorStop(0, "rgba(23, 125, 255, 0.7)");
gradientFill.addColorStop(1, "rgba(128, 182, 244, 0.3)");
var myHtmlLegendsChart = new Chart(htmlLegendsChart, {
type: 'line',
data: {
labels: jsdata.data[0],
datasets: [ {
label: "Open case",
borderColor: gradientStroke,
pointBackgroundColor: gradientStroke,
pointRadius: 0,
backgroundColor: gradientFill,
legendColor: '#fff',
fill: true,
borderWidth: 1,
data: jsdata.data[1]
}]
},
options : {
responsive: true,
maintainAspectRatio: false,
legend: {
display: false
},
tooltips: {
bodySpacing: 4,
mode:"nearest",
intersect: 0,
position:"nearest",
xPadding:10,
yPadding:10,
caretPadding:10
},
layout:{
padding:{left:15,right:15,top:15,bottom:15}
},
scales: {
yAxes: [{
ticks: {
display: false
},
gridLines: {
drawTicks: false,
display: false
}
}],
xAxes: [{
gridLines: {
zeroLineColor: "transparent",
display: false
},
ticks: {
padding: 20,
fontColor: "rgba(0,0,0,0.5)",
fontStyle: "500",
display: false
}
}]
},
legendCallback: function(chart) {
var text = [];
text.push('<ul class="' + chart.id + '-legend html-legend">');
for (var i = 0; i < chart.data.datasets.length; i++) {
text.push('<li><span style="background-color:' + chart.data.datasets[i].legendColor + '"></span>');
if (chart.data.datasets[i].label) {
text.push(chart.data.datasets[i].label);
}
text.push('</li>');
}
text.push('</ul>');
return text.join('');
}
}
});
//var myLegendContainer = document.getElementById("myChartLegend");
// generate HTML legend
//myLegendContainer.innerHTML = myHtmlLegendsChart.generateLegend();
}
},
error: function (error) {
notify_error(error);
}
});
</script>
{% endblock javascripts %}

View File

@@ -0,0 +1,97 @@
<div class="modal-header">
<div class="col md-12">
<div class="row">
<div class="col">
<h4 class="modal-title mt-1 mr-4">{% if task.id %} Task ID #{{ task.id }}{% else %} Add global task {% endif %}
{% if task.id %}
<i class="fas fa-info-circle ml-3" data-toggle="popover"
title="Task info"
data-content="Last updated {{ task.task_last_update }} by {{ user_name }}."></i>
{% endif %}
</h4>
<small><i class="text-muted">{% if task.task_uuid %}#{{ task.task_uuid }}{% endif %}</i></small>
</div>
<div class="col ">
<div class="row float-right">
<button type="button" class="float-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
</div>
</div>
</div>
</div>
<div class="modal-body">
<div class="container col-md-12">
<form method="post" action="" id="form_new_gtask">
<div class="col-md-12 col-lg-12 col-sm-12">
{{ form.hidden_tag() }}
<div class="form-group mt-3">
<label for="task_assignee" class="placeholder">Assigned to</label>
{{ form.task_assignee_id(class="selectpicker pl--6 col-5") }}
<label for="task_status" class="placeholder">Status</label>
{{ form.task_status_id(class="selectpicker pl--6 col-5") }}
</div>
<div class="form-group">
<label for="task_title" class="placeholder">{{ form.task_title.label.text }} *</label>
{{ form.task_title(class='form-control col-md-12 col-sm-12', autocomplete="off") }}
</div>
<div class="form-group mt-3">
<label for="task_description" class="placeholder">{{ form.task_description.label.text }}</label>
{{ form.task_description(class='form-control col-md-12 col-sm-12 sizable-textarea', autocomplete="off") }}
</div>
<div class="form-group mt-3">
<label for="task_tags">Task tags
</label>
<input type="text" id="task_tags"
class="form-control col-md-12" {% if task.task_tags %} value="{{ task.task_tags }}" {% endif %}/>
</div>
</div>
{% if task.id %}
<button type="button" class="btn btn-outline-danger mt-5"
onclick="delete_gtask({{ task.id }});">Delete</button>
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
onclick="update_gtask({{ task.id }});">Update</button>
{% else %}
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
id="submit_new_gtask">Save</button>
{% endif %}
</form>
</div>
</div>
<script>
$('#task_tags').amsifySuggestags({
printValues: false,
suggestions: [ {% for tag in tags %} '{{ tag }}', {% endfor %} ]
});
$('#task_assignee_id').selectpicker({
liveSearch: true,
title: "None",
style: "Bootstrap 4: 'btn-outline-primary'",
});
$('#task_status_id').selectpicker({
liveSearch: true,
title: "None",
style: "Bootstrap 4: 'btn-outline-primary'",
});
{% if uid %}
$('#task_assignee_id').selectpicker('val', '{{uid}}');
{% endif %}
{% if task.task_status_id %}
$('#task_status_id').selectpicker('val', '{{task.task_status_id}}');
{% else %}
$('#task_status_id').selectpicker('val', '1');
{% endif %}
$('[data-toggle="popover"]').popover();
</script>

View File

@@ -0,0 +1,455 @@
#!/usr/bin/env python3
#
#
# IRIS Source Code
# Copyright (C) 2022 - DFIR IRIS Team
# 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 base64
import datetime
import json
import marshmallow.exceptions
import urllib.parse
from flask import Blueprint
from flask import render_template
from flask import request
from flask import send_file
from flask import url_for
from flask_login import current_user
from pathlib import Path
from werkzeug.utils import redirect
import app
from app import db
from app.datamgmt.datastore.datastore_db import datastore_add_child_node
from app.datamgmt.datastore.datastore_db import datastore_add_file_as_evidence
from app.datamgmt.datastore.datastore_db import datastore_add_file_as_ioc
from app.datamgmt.datastore.datastore_db import datastore_delete_file
from app.datamgmt.datastore.datastore_db import datastore_delete_node
from app.datamgmt.datastore.datastore_db import datastore_filter_tree
from app.datamgmt.datastore.datastore_db import datastore_get_file
from app.datamgmt.datastore.datastore_db import datastore_get_interactive_path_node
from app.datamgmt.datastore.datastore_db import datastore_get_local_file_path
from app.datamgmt.datastore.datastore_db import datastore_get_path_node
from app.datamgmt.datastore.datastore_db import datastore_get_standard_path
from app.datamgmt.datastore.datastore_db import datastore_rename_node
from app.datamgmt.datastore.datastore_db import ds_list_tree
from app.forms import ModalDSFileForm
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import CaseAccessLevel
from app.schema.marshables import DSFileSchema, DSPathSchema
from app.util import ac_api_case_requires
from app.util import ac_case_requires
from app.util import add_obj_history_entry
from app.util import response_error
from app.util import response_success
datastore_blueprint = Blueprint(
'datastore',
__name__,
template_folder='templates'
)
logger = app.logger
@datastore_blueprint.route('/datastore/list/tree', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def datastore_list_tree(caseid):
data = ds_list_tree(caseid)
return response_success("", data=data)
@datastore_blueprint.route('/datastore/list/filter', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def datastore_list_filter(caseid):
args = request.args.to_dict()
query_filter = args.get('q')
try:
filter_d = dict(json.loads(urllib.parse.unquote_plus(query_filter)))
except Exception as e:
logger.error('Error parsing filter: {}'.format(query_filter))
logger.error(e)
return response_error('Invalid query')
data, log = datastore_filter_tree(filter_d, caseid)
if data is None:
logger.error('Error parsing filter: {}'.format(query_filter))
logger.error(log)
return response_error('Invalid query')
return response_success("", data=data)
@datastore_blueprint.route('/datastore/file/add/<int:cur_id>/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.full_access)
def datastore_add_file_modal(cur_id: int, caseid: int, url_redir: bool):
if url_redir:
return redirect(url_for('index.index', cid=caseid, redirect=True))
dsp = datastore_get_path_node(cur_id, caseid)
if not dsp:
return response_error('Invalid path node for this case')
form = ModalDSFileForm()
return render_template("modal_ds_file.html", form=form, file=None, dsp=dsp)
@datastore_blueprint.route('/datastore/filter-help/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def datastore_filter_help_modal(caseid, url_redir):
if url_redir:
return redirect(url_for('index.index', cid=caseid, redirect=True))
return render_template("modal_help_filter_ds.html")
@datastore_blueprint.route('/datastore/file/update/<int:cur_id>/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def datastore_update_file_modal(cur_id: int, caseid: int, url_redir: bool):
if url_redir:
return redirect(url_for('index.index', cid=caseid, redirect=True))
file = datastore_get_file(cur_id, caseid)
if not file:
return response_error('Invalid file ID for this case')
dsp = datastore_get_path_node(file.file_parent_id, caseid)
form = ModalDSFileForm()
form.file_is_ioc.data = file.file_is_ioc
form.file_original_name.data = file.file_original_name
form.file_password.data = file.file_password
form.file_password.render_kw = {'disabled': 'disabled'}
form.file_description.data = file.file_description
form.file_is_evidence.data = file.file_is_evidence
return render_template("modal_ds_file.html", form=form, file=file, dsp=dsp)
@datastore_blueprint.route('/datastore/file/info/<int:cur_id>/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def datastore_info_file_modal(cur_id: int, caseid: int, url_redir: bool):
if url_redir:
return redirect(url_for('index.index', cid=caseid, redirect=True))
file = datastore_get_file(cur_id, caseid)
if not file:
return response_error('Invalid file ID for this case')
dsp = datastore_get_path_node(file.file_parent_id, caseid)
return render_template("modal_ds_file_info.html", file=file, dsp=dsp)
@datastore_blueprint.route('/datastore/file/info/<int:cur_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def datastore_info_file(cur_id: int, caseid: int):
file = datastore_get_file(cur_id, caseid)
if not file:
return response_error('Invalid file ID for this case')
file_schema = DSFileSchema()
file = file_schema.dump(file)
del file['file_local_name']
return response_success("", data=file)
@datastore_blueprint.route('/datastore/file/update/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_update_file(cur_id: int, caseid: int):
dsf = datastore_get_file(cur_id, caseid)
if not dsf:
return response_error('Invalid file ID for this case')
dsf_schema = DSFileSchema()
try:
dsf_sc = dsf_schema.load(request.form, instance=dsf, partial=True)
add_obj_history_entry(dsf_sc, 'updated')
dsf.file_is_ioc = request.form.get('file_is_ioc') is not None or request.form.get('file_is_ioc') is True
dsf.file_is_evidence = request.form.get('file_is_evidence') is not None or request.form.get('file_is_evidence') is True
db.session.commit()
if request.files.get('file_content'):
ds_location = datastore_get_standard_path(dsf_sc, caseid)
dsf_sc.file_local_name, dsf_sc.file_size, dsf_sc.file_sha256 = dsf_schema.ds_store_file(
request.files.get('file_content'),
ds_location,
dsf_sc.file_is_ioc,
dsf_sc.file_password)
if dsf_sc.file_is_ioc and not dsf_sc.file_password:
dsf_sc.file_password = 'infected'
db.session.commit()
msg_added_as = ''
if dsf.file_is_ioc:
datastore_add_file_as_ioc(dsf, caseid)
msg_added_as += 'and added in IOC'
if dsf.file_is_evidence:
datastore_add_file_as_evidence(dsf, caseid)
msg_added_as += ' and evidence' if len(msg_added_as) > 0 else 'and added in evidence'
track_activity(f'File \"{dsf.file_original_name}\" updated in DS', caseid=caseid)
return response_success('File updated in datastore', data=dsf_schema.dump(dsf_sc))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages)
@datastore_blueprint.route('/datastore/file/move/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_move_file(cur_id: int, caseid: int):
if not request.json:
return response_error("Invalid data")
dsf = datastore_get_file(cur_id, caseid)
if not dsf:
return response_error('Invalid file ID for this case')
dsp = datastore_get_path_node(request.json.get('destination-node'), caseid)
if not dsp:
return response_error('Invalid destination node ID for this case')
dsf.file_parent_id = dsp.path_id
db.session.commit()
track_activity(f'File \"{dsf.file_original_name}\" moved to \"{dsp.path_name}\" in DS', caseid=caseid)
return response_success(f"File successfully moved to {dsp.path_name}")
@datastore_blueprint.route('/datastore/folder/move/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_move_folder(cur_id: int, caseid: int):
if not request.json:
return response_error("Invalid data")
dsp = datastore_get_path_node(cur_id, caseid)
if not dsp:
return response_error('Invalid file ID for this case')
dsp_dst = datastore_get_path_node(request.json.get('destination-node'), caseid)
if not dsp_dst:
return response_error('Invalid destination node ID for this case')
if dsp.path_id == dsp_dst.path_id:
return response_error("If that's true, then I've made a mistake, and you should kill me now.")
dsp.path_parent_id = dsp_dst.path_id
db.session.commit()
dsf_folder_schema = DSPathSchema()
msg = f"Folder \"{dsp.path_name}\" successfully moved to \"{dsp_dst.path_name}\""
track_activity(msg, caseid=caseid)
return response_success(msg, data=dsf_folder_schema.dump(dsp))
@datastore_blueprint.route('/datastore/file/view/<int:cur_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def datastore_view_file(cur_id: int, caseid: int):
has_error, dsf = datastore_get_local_file_path(cur_id, caseid)
if has_error:
return response_error('Unable to get requested file ID', data=dsf)
if dsf.file_is_ioc or dsf.file_password:
dsf.file_original_name += ".zip"
if not Path(dsf.file_local_name).is_file():
return response_error(f'File {dsf.file_local_name} does not exists on the server. '
f'Update or delete virtual entry')
resp = send_file(dsf.file_local_name, as_attachment=True,
download_name=dsf.file_original_name)
track_activity(f"File \"{dsf.file_original_name}\" downloaded", caseid=caseid, display_in_ui=False)
return resp
@datastore_blueprint.route('/datastore/file/add/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_add_file(cur_id: int, caseid: int):
dsp = datastore_get_path_node(cur_id, caseid)
if not dsp:
return response_error('Invalid path node for this case')
dsf_schema = DSFileSchema()
try:
dsf_sc = dsf_schema.load(request.form, partial=True)
dsf_sc.file_parent_id = dsp.path_id
dsf_sc.added_by_user_id = current_user.id
dsf_sc.file_date_added = datetime.datetime.now()
dsf_sc.file_local_name = 'tmp_xc'
dsf_sc.file_case_id = caseid
add_obj_history_entry(dsf_sc, 'created')
if dsf_sc.file_is_ioc and not dsf_sc.file_password:
dsf_sc.file_password = 'infected'
db.session.add(dsf_sc)
db.session.commit()
ds_location = datastore_get_standard_path(dsf_sc, caseid)
dsf_sc.file_local_name, dsf_sc.file_size, dsf_sc.file_sha256 = dsf_schema.ds_store_file(
request.files.get('file_content'),
ds_location,
dsf_sc.file_is_ioc,
dsf_sc.file_password)
db.session.commit()
msg_added_as = ''
if dsf_sc.file_is_ioc:
datastore_add_file_as_ioc(dsf_sc, caseid)
msg_added_as += 'and added in IOC'
if dsf_sc.file_is_evidence:
datastore_add_file_as_evidence(dsf_sc, caseid)
msg_added_as += ' and evidence' if len(msg_added_as) > 0 else 'and added in evidence'
track_activity(f"File \"{dsf_sc.file_original_name}\" added to DS", caseid=caseid)
return response_success(f'File saved in datastore {msg_added_as}', data=dsf_schema.dump(dsf_sc))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages)
@datastore_blueprint.route('/datastore/file/add-interactive', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_add_interactive_file(caseid: int):
dsp = datastore_get_interactive_path_node(caseid)
if not dsp:
return response_error('Invalid path node for this case')
dsf_schema = DSFileSchema()
try:
js_data = request.json
try:
file_content = base64.b64decode(js_data.get('file_content'))
filename = js_data.get('file_original_name')
except Exception as e:
return response_error(msg=str(e))
dsf_sc, existed = dsf_schema.ds_store_file_b64(filename, file_content, dsp, caseid)
if not existed:
msg = "File saved in datastore"
else:
msg = "File already existing in datastore. Using it."
track_activity(f"File \"{dsf_sc.file_original_name}\" added to DS", caseid=caseid)
return response_success(msg, data={"file_url": f"/datastore/file/view/{dsf_sc.file_id}"})
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages)
@datastore_blueprint.route('/datastore/folder/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_add_folder(caseid: int):
data = request.json
if not data:
return response_error('Invalid data')
parent_node = data.get('parent_node')
folder_name = data.get('folder_name')
if not parent_node or not folder_name:
return response_error('Invalid data')
has_error, logs, node = datastore_add_child_node(parent_node, folder_name, caseid)
dsf_folder_schema = DSPathSchema()
if not has_error:
track_activity(f"Folder \"{folder_name}\" added to DS", caseid=caseid)
return response_success(msg=logs, data=dsf_folder_schema.dump(node))
return response_error(msg=logs)
@datastore_blueprint.route('/datastore/folder/rename/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_rename_folder(cur_id: int, caseid: int):
data = request.json
if not data:
return response_error('Invalid data')
parent_node = data.get('parent_node')
folder_name = data.get('folder_name')
if not parent_node or not folder_name:
return response_error('Invalid data')
if int(parent_node) != cur_id:
return response_error('Invalid data')
has_error, logs, dsp_base = datastore_rename_node(parent_node, folder_name, caseid)
dsf_folder_schema = DSPathSchema()
if has_error:
return response_error(logs)
track_activity(f"Folder \"{parent_node}\" renamed to \"{folder_name}\" in DS", caseid=caseid)
return response_success(logs, data=dsf_folder_schema.dump(dsp_base))
@datastore_blueprint.route('/datastore/folder/delete/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_delete_folder_route(cur_id: int, caseid: int):
has_error, logs = datastore_delete_node(cur_id, caseid)
if has_error:
return response_error(logs)
track_activity(f"Folder \"{cur_id}\" deleted from DS", caseid=caseid)
return response_success(logs)
@datastore_blueprint.route('/datastore/file/delete/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_delete_file_route(cur_id: int, caseid: int):
has_error, logs = datastore_delete_file(cur_id, caseid)
if has_error:
return response_error(logs)
track_activity(f"File \"{cur_id}\" deleted from DS", caseid=caseid)
return response_success(logs)

View File

@@ -0,0 +1,119 @@
<div class="modal-header">
<h4 class="modal-title mt-2 mr-4">Datastore File</h4>
<div class="row text-right">
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
</div>
<div class="modal-body">
<div class="container col-md-12">
<form method="post" action="" id="form_new_ds_file">
<div class="col-md-12 col-lg-12 col-sm-12">
{{ form.hidden_tag() }}
{% if file.file_id %}
<div class="row ml-2">
<p>The file is currently saved in virtual folder <code>{{ dsp.path_name }}</code>.</p>
</div>
{% else %}
<div class="row ml-2">
<p>The file will be saved in virtual folder <code>{{ dsp.path_name }}</code>.</p>
</div>
{% endif %}
<div class="form-row ml-2">
<div class="form-group col-12">
<label for="input_upload_ds_file" class="form-label">Choose file to upload : </label>
<input id="input_upload_ds_file" class="form-control" type="file">
</div>
</div>
<div class="form-row ml-2">
<div class="form-group col-12">
<label for="file_original_name" class="placeholder">Filename *</label>
{{ form.file_original_name(class='form-control', autocomplete="off") }}
</div>
</div>
<div class="form-row ml-2">
<div class="form-group col-12">
<label for="file_description" class="placeholder">Description</label>
{{ form.file_description(class='form-control col-md-12 col-sm-12 sizable-textarea', autocomplete="off") }}
</div>
</div>
<div class="form-row ml-2">
<div class="form-group col-6 col-xs-12">
<label for="file_password" class="placeholder">Password<i class="ml-1 mt-1 fa-regular text-dark fa-circle-question"
title="Help" data-toggle="popover" data-html="true"
data-trigger="hover" style="cursor: pointer;"
data-content="If set, the file is locally encrypted with this password.<br/><b class='text-danger'>Passwords are stored in clear text server side. Do not put sensitive password here.</b><br/>Encrypted files cannot be used in notes.<br/>IOC are automatically encrypted with password <code>infected</code> unless specified otherwise here.">
</i></label>
<div class="input-group mb-2 mr-sm-2">
{{ form.file_password(class='form-control', autocomplete="off", type="password") }}
<div class="input-group-append">
<div class="input-group-text" id="toggle_file_password"><i class="fa-solid fa-eye"></i></div>
</div>
</div>
</div>
<div class="form-group col-6 col-xs-12">
<label for="file_tags">File tags</label>
<input type="text" id="file_tags" name="file_tags"
class="form-control col-md-12" {% if file.file_tags %} value="{{ file.file_tags }}" {% endif %}/>
</div>
</div>
<div class="form-row ml-2">
<div class="form-group col-6 col-xs-12">
<div class="form-check">
<label class="form-check-label">
{{ form.file_is_ioc(class="form-check-input", type="checkbox") }}
<span class="form-check-sign"> File is IOC <i class="ml-1 mt-1 fa-regular text-dark fa-circle-question"
title="Help" data-toggle="popover" data-html="true"
data-trigger="hover" style="cursor: pointer;"
data-content="If set, the file is stored in a dedicated IOC folder on the server and is encrypted with password <code>infected</code> unless specified otherwise in the password field.<br/> The file is also added to the case IOC.">
</i></span>
</label>
</div>
</div>
<div class="form-group col-6 col-xs-12">
<div class="form-check">
<label class="form-check-label">
{{ form.file_is_evidence(class="form-check-input", type="checkbox") }}
<span class="form-check-sign"> File is Evidence <i class="ml-1 mt-1 fa-regular text-dark fa-circle-question"
title="Help" data-toggle="popover" data-html="true"
data-trigger="hover" style="cursor: pointer;"
data-content="If set, the file is stored in a dedicated Evidence folder on the server and added to the case Evidences.">
</i></span>
</label>
</div>
</div>
</div>
</div>
{% if file.file_id %}
<button type="button" class="btn btn-outline-danger ml-4 mt-5"
onclick="delete_ds_file({{ file.file_id }});">Delete</button>
{% endif %}
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right" onclick="save_ds_file({{dsp.path_id}}, {{ file.file_id }});return false;">Save</button>
</form>
</div>
</div>
<script>
$('[data-toggle="popover"]').popover();
$('#toggle_file_password').on('click', function (e) {
const type = $('#file_password').attr('type') === 'password' ? 'text' : 'password';
$('#file_password').attr('type', type);
$('#toggle_file_password > i').attr('class', type === 'password' ? 'fa-solid fa-eye' : 'fa-solid fa-eye-slash');
});
$('#file_tags').amsifySuggestags({
printValues: true,
{% if file.file_tags %}
suggestions: [ {% for tag in file.file_tags %} '{{ tag }}', {% endfor %} ],
{% endif %}
whiteList: false,
selectOnHover: false,
});
$("#input_upload_ds_file").on("change", function(e) {
var file = e.target.files[0].name;
$('#file_original_name').val(file);
});
</script>

View File

@@ -0,0 +1,74 @@
<div class="modal-header">
<h4 class="modal-title mt-2 mr-4">Datastore File {{ file.file_original_name }}</h4>
<div class="row text-right">
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
</div>
<div class="modal-body">
<div class="container col-md-12">
<div class="col-md-12 col-lg-12 col-sm-12">
<h3>File information</h3>
<dl class="row mt-2">
<dt class="col-sm-3">Virtual location: </dt>
<dd class="col-sm-8">{{ dsp.path_name }}</dd>
<dt class="col-sm-3">Original file name: </dt>
<dd class="col-sm-8">{{ file.file_original_name }}</dd>
<dt class="col-sm-3">File description: </dt>
<dd class="col-sm-8">{{ file.file_description }}</dd>
<dt class="col-sm-3">Storage UUID: </dt>
<dd class="col-sm-8">dsf-{{ file.file_uuid }}</dd>
<dt class="col-sm-3">Storage ID: </dt>
<dd class="col-sm-8">dsf-{{ file.file_id }}</dd>
<dt class="col-sm-3">Tags: </dt>
<dd class="col-sm-8">{% for tag in file.file_tags.split(',') %} <div class="badge badge-light">{{ tag }}</div> {% endfor %}</dd>
<dt class="col-sm-3">SHA256: </dt>
<dd class="col-sm-8">{{ file.file_sha256 }}</dd>
<dt class="col-sm-3">Size (bytes): </dt>
<dd class="col-sm-8">{{ file.file_size }}</dd>
<dt class="col-sm-3">Is evidence: </dt>
<dd class="col-sm-8">{{ file.file_is_evidence }}</dd>
<dt class="col-sm-3">Is IOC: </dt>
<dd class="col-sm-8">{{ file.file_is_ioc }}</dd>
<dt class="col-sm-3">Is password protected: </dt>
<dd class="col-sm-8">{% if file.file_password %} True {% else %} False {% endif %}</dd>
<dt class="col-sm-3">Password: </dt>
<dd class="col-sm-8">
<div class="row">
<input class="form_control ml-3" style="border:none;" type="password" value="{{ file.file_password }}" id="ds_file_password" disabled>
<div class="file_show_password" id="toggle_file_password"><i class="fa-solid fa-eye"></i></div>
</div>
</dd>
<dt class="col-sm-3 mt-4">Modification history: </dt>
<dd class="mt-4">
<ul>
{% if file.modification_history %}
{% for mod in file.modification_history %}
<li>{{ mod|format_datetime('%Y-%m-%d %H:%M') }} - {{ file.modification_history[mod].action }} by {{ file.modification_history[mod].user }} </li>
{% endfor %}
{% endif %}
</ul>
</dd>
</dl>
</div>
</div>
</div>
<script>
$('#toggle_file_password').on('click', function (e) {
const type = $('#ds_file_password').attr('type') === 'password' ? 'text' : 'password';
$('#ds_file_password').attr('type', type);
$('#toggle_file_password > i').attr('class', type === 'password' ? 'fa-solid fa-eye' : 'fa-solid fa-eye-slash');
});
</script>

View File

@@ -0,0 +1,32 @@
<div class="modal-xl modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title mt-1 mr-4">Datastore filtering help</h4>
<button type="button" class="float-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
<div class="modal-body mb-2">
<div class="row">
<div class="col-12">
<p>Files can be filtered thanks to simple queries. The query schema is : <code>target_element:search_value AND target_element2:search_value2</code>.<br/>
There is no <code>OR</code> condition and searching without target does not work.
<p>The following target elements can be used to filter :</p>
<ul>
<li><code>name</code>: Name of the file</li>
<li><code>id</code>: ID of the file</li>
<li><code>uuid</code>: UUID of the file</li>
<li><code>storage_name</code>: Name of the file on the FS</li>
<li><code>tag</code>: Tag of the file</li>
<li><code>description</code>: Description of the file</li>
<li><code>is_ioc</code> : Set to any value to filter files which are IOCs</li>
<li><code>is_evidence</code>: Set to any value to filter files which are evidences</li>
<li><code>has_password</code>: Set to any value to filter files which have passwords</li>
<li><code>sha256</code>: SHA256 to filter files with</li>
</ul>
Example of filter :
<code>name: .exe AND is_ioc: true</code>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,59 @@
#!/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.
# IMPORTS ------------------------------------------------
from flask import Blueprint
from flask import render_template
from app import app
from app.iris_engine.demo_builder import gen_demo_admins
from app.iris_engine.demo_builder import gen_demo_users
demo_blueprint = Blueprint(
'demo-landing',
__name__,
template_folder='templates'
)
log = app.logger
if app.config.get('DEMO_MODE_ENABLED') == 'True':
@demo_blueprint.route('/welcome', methods=['GET'])
def demo_landing():
iris_version = app.config.get('IRIS_VERSION')
demo_domain = app.config.get('DEMO_DOMAIN')
seed_user = app.config.get('DEMO_USERS_SEED')
seed_adm = app.config.get('DEMO_ADM_SEED')
adm_count = int(app.config.get('DEMO_ADM_COUNT', 4))
users_count = int(app.config.get('DEMO_USERS_COUNT', 10))
demo_users = [
{'username': username, 'password': pwd, 'role': 'Admin'} for _, username, pwd, _ in gen_demo_admins(adm_count, seed_adm)
]
demo_users += [
{'username': username, 'password': pwd, 'role': 'User'} for _, username, pwd, _ in gen_demo_users(users_count, seed_user)
]
return render_template(
'demo-landing.html',
iris_version=iris_version,
demo_domain=demo_domain,
demo_users=demo_users
)

View File

@@ -0,0 +1,160 @@
<html class="" lang="en"><head>
<meta charset="UTF-8">
<title>IRIS Demonstration</title>
<meta name="robots" content="noindex">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Barlow:wght@100&amp;display=swap">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">
<link rel="stylesheet" href="/static/assets/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/assets/css/atlantis.css">
<link rel="stylesheet" href="/static/assets/css/demo.css">
<link rel="icon" href="/static/assets/img/logo.ico" type="image/x-icon"/>
<script defer data-domain="v200.beta.dfir-iris.org" src="https://analytics.dfir-iris.org/js/plausible.js"></script>
</head>
<body class="landing-demo">
<div class="ml-1 row justify-content-center mr-1">
<div class="col-xl-8">
<div class="card mt-3">
<div class="mt-4">
<div class="col d-flex justify-content-center">
<a href="/" class="logo ml-2 text-center">
<img src="/static/assets/img/logo-full-blue.png" alt="navbar brand" width="300rem">
</a>
</div>
</div>
<div class="row">
<h4 class="ml-auto mr-auto"><span class="text-danger">shared</span> demonstration instance {{ iris_version }}</h4>
</div>
<div class="row">
<h5 class="text-muted ml-auto mr-auto"><i>Try out IRIS, find bugs and security vulnerabilities</i></h5><br/>
</div>
<div class="row mt-4">
</div>
<div class="row mt-4">
</div>
<div class="row mt-2 mb-4">
<div class="col-md-1 col-lg-2"></div>
<div class="col-md-10 col-lg-8 ml-4">
<h3 class=" ml-auto mr-auto">Hey there, please read the following carefully</h3><br/>
<ul>
<li><b>Do not upload any illegal or confidential materials</b></li>
<li><b>Do not download and open files from other users blindly</b></li>
<li><b>Respect a <a class="text-muted" target="_blank" rel="noopener noreferrer" href="https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html#responsible-or-coordinated-disclosure">responsible disclosure</a> of 30 days if you find a vulnerability</b></li>
</ul>
<b>Not sure what IRIS is about? You'll find more info on the <a target="_blank" rel="noopener" href="https://dfir-iris.org">main website</a></b>
</div>
<div class="col-md-1 col-lg-2"></div>
</div>
<div class="row mt-3">
<div class="col-md-1 col-lg-2"></div>
<div class="col-md-10 col-lg-8 ml-4 mr-3">
<p class="">Accounts to access the instance are available at the bottom of the page. <br/>
IRIS is not optimized to be used on phones. We recommend accessing it from a computer.<br/>
If you notice anything suspicious or have any question, please <a href="mailto:contact@dfir-iris.org">contact us</a>. <br/>Note that the instance might be reset at any moment.</p>
<p><i>By accessing this instance you confirm you read, understand and agree with all the information on this page.</i></p>
</div>
<div class="col-md-1 col-lg-2"></div>
</div>
<div class="row mt-4 mb-4 mr-2">
<a class="btn btn-outline-success ml-auto mr-auto" target="_blank" rel="noopener" href="/login">
Access IRIS
</a>
</div>
<div class="row mt-4 mb-4 mr-2 justify-content-center">
<div class="ml-mr-auto">
<button class="btn btn-primary" type="button" data-toggle="collapse" data-target="#collapseSecRules" aria-expanded="false" aria-controls="collapseSecRules">
Rules of engagement
</button>
<button class="btn btn-primary" type="button" data-toggle="collapse" data-target="#collapseLiability" aria-expanded="false" aria-controls="collapseLiability">
Disclaimer
</button>
<button class="btn btn-primary" type="button" data-toggle="collapse" data-target="#collapseAccounts" aria-expanded="false" aria-controls="collapseAccounts">
Accounts
</button>
</div>
</div>
<div class="row mt-4 mb-4 mr-2 justify-content-center">
<div class="col ml-4">
<div class="collapse" id="collapseLiability">
<div class="card card-body">
<h3 class="mt-2">Disclaimer</h3>
DFIR-IRIS is a non-profit organization. It is not responsible for any damage caused by the use of this site and any material contained in it, or from any action or decision taken as a result of using this site.<br/>
It is not responsible for the content of any external sites linked to this site.<br/> By using this site, you acknowledge that content posted on this site is public and DFIR-IRIS cannot guarantee the security of any information disclose on it; you make such disclosures at your own risk.
<h4 class="mt-2">Privacy</h4><br/>
<p>This demonstration instance is shared and we cannot guarantee the privacy of data you might upload on it. We are not responsible for any data loss or data leak. </p>
<p>To better understand the use of this instance, DFIR-IRIS uses a privacy-friendly cookie-less analytics. DFIR-IRIS does not collect any personal data. DFIR-IRIS does not use any third-party analytics and uses a self-hosted <a target="_blank" rel="noopener" href="https://plausible.io/">Plausible</a> instance.</p>
</div>
</div>
<div class="collapse" id="collapseSecRules">
<div class="card card-body">
<h3 class="mt-2">Rules of engagement</h3>
<p class=""><b>If you find a vulnerability</b>, <a href="mailto:contact@dfir-iris.org">contact us</a> before going public as it may impact systems already in production.<br/>
In other words, please respect a responsible disclosure of 30 days. We will patch and then publish the vulnerability. Depending on the finding a CVE might be requested, and will have your name - except if you don't want to.<br/>
You can report anything you find at <a href="mailto:contact@dfir-iris.org">contact@dfir-iris.org</a>.</p>
<p class=""><b>The scope of the security tests</b> is limited to the Web Application IRIS hosted on <a class="" target="_blank" rel="noopener" href="{{ demo_domain }}">{{ demo_domain }}</a>.<br/>
<span class="text-danger">Subdomains, SSH, scanning of the IP, BF, and other flavors are <b>out of scope.</b></span></p>
We are mostly interested in the following:
<ul>
<li><b>authentication bypass</b>: achieve any action requiring an authentication without being authenticated. <span class="text-danger">Brute-force is not what we are looking for</span></li>
<li><b>privilege escalations within the application</b>: from a standard user (<code>user_std_XX</code>) to administrative rights (<code>adm_XX</code>) on IRIS</li>
<li><b>privilege escalations on the host server</b>: from a standard user (<code>user_std_XX</code>) to code execution on the server</li>
<li><b>data leakage</b>: from a standard user (<code>user_std_XX</code>) read data of non-accessible cases (titled <code>Restricted Case XXX</code>)</li>
</ul>
<h3>Important Remarks</h3>
<ul>
<li>If you can, use a local instance of IRIS instead of this one. It only takes a few minutes to <a target="_blank" rel="noopener" href="https://docs.dfir-iris.org/getting_started/">get it on docker.</a></li>
<li>The administrators account can publish stored XSS on the platform via <a target="_blank" rel="noopener" href="https://docs.dfir-iris.org/operations/custom_attributes/">Custom Attributes</a>. This is an operational requirement and not recognized as a vulnerability.</li>
<li><b>Try not to be destructive.</b> If you manage to run code on the host server, do not try to go further.</li>
</ul>
<h3>Restrictions</h3>
To keep this demo instance alive, there are some restrictions put in place.
<ul>
<li>The <code>administrator</code> account cannot be updated nor deleted.</li>
<li>The accounts available on this page cannot be updated nor deleted.</li>
<li>File upload in datastore is limited to 200KB per file.</li>
</ul>
<h3>Resources</h3>
<p>You can read more about IRIS on the <a target="_blank" rel="noopener" href="https://docs.dfir-iris.org">official documentation website</a>.<br/>
IRIS is an open source app, so you can directly access the code on <a target="_blank" rel="noopener" href="https://github.com">GitHub</a>.</p>
</div>
</div>
<div class="collapse" id="collapseAccounts">
<div class="card card-body">
<h3 class="mt-2">Accounts</h3>
The following accounts are available on the instance. These users cannot be updated or deleted. However, new users and groups can be created.<br/>
<b>If the passwords are not working, please double check spaces were not added while copying.</b>
<table class="table table-striped table-hover responsive">
<thead>
<tr>
<th>Username</th>
<th>Password</th>
<th>Role</th>
</tr>
</thead>
<tbody>
{% for user in demo_users %}
<tr>
<td>{{ user.username }}</td>
<td>{{ user.password }}</td>
<td>{{ user.role }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
<script src="/static/assets/js/core/jquery.3.2.1.min.js"></script>
<script src="/static/assets/js/core/bootstrap.min.js"></script>
</html>

View File

@@ -0,0 +1,319 @@
#!/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/<hook_type>/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/<int:count>', 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/<task_id>', 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)

View File

@@ -0,0 +1,69 @@
{% extends "layouts/default.html" %}
{% block title %} DIM Tasks {% endblock title %}
{% block stylesheets %}
<link href="/static/assets/css/dataTables.buttons.min.css" rel="stylesheet">
{% endblock stylesheets %}
{% block content %}
{% if current_user.is_authenticated %}
<div class="page-inner">
<div class="row">
<div class="col-md-12">
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
<div class="card" id="card_main_load" style="display:none;">
<div class="card-header">
<div class="card-title">DFIR-IRIS Modules Tasks
<button type="button" class="btn btn-icon btn-round btn-outline-dark float-right ml-2" onclick="get_activities();">
<i class="fas fa-sync rotate"></i></button></div>
</div>
<div class="card-body">
<div class="table-responsive" id="activities_table_wrapper">
<div class="selectgroup">
<span id="table_buttons"></span>
</div>
<table class="table display table-striped table-hover" width="100%" cellspacing="0" id="activities_table" >
<thead>
<tr>
<th>Task ID</th>
<th>State</th>
<th>Date ended</th>
<th>Case</th>
<th>Processing module</th>
<th>Initiating user</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Task ID</th>
<th>State</th>
<th>Date ended</th>
<th>Case</th>
<th>Processing module</th>
<th>Initiating user</th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{{ form.hidden_tag() }}
{% endif %}
{% endblock content %}
{% block javascripts %}
<script src="/static/assets/js/plugin/datatables/dataTables.cellEdit.js"></script>
<script src="/static/assets/js/plugin/datatables/dataTables.buttons.min.js"></script>
<script src="/static/assets/js/plugin/datatables/buttons.html5.min.js"></script>
<script src="/static/assets/js/plugin/datatables/buttons.print.min.js"></script>
<script src="/static/assets/js/iris/datatablesUtils.js"></script>
<script src="/static/assets/js/iris/dim_tasks.js"></script>
{% endblock javascripts %}

View File

@@ -0,0 +1,27 @@
<div class="container col-md-12">
<div class="form-group">
{% for element in data %}
{% if element != "Logs" %}
{% if data[element] != None %}
<div class="input-group">
<label for="en">{{ element }}: </label>
<span type="text" class="text-faded ml-1" id="en"> {{ data[element] }}</span>
</div>
{% endif %}
{% endif %}
{% endfor %}
</div>
{% if data['Logs'] %}
<div class="form-group mt--3">
<label>Logs</label>
<ul>
{% for log in data['Logs'] %}
<li>{{ log }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>

View File

@@ -0,0 +1,177 @@
#!/usr/bin/env python3
#
# IRIS Source Code
# Copyright (C) 2023 - 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.
from flask import Blueprint, request
from flask_login import current_user
from werkzeug import Response
from app import db
from app.datamgmt.filters.filters_db import get_filter_by_id, list_filters_by_type
from app.iris_engine.utils.tracker import track_activity
from app.schema.marshables import SavedFilterSchema
from app.util import ac_api_requires, response_success, response_error
saved_filters_blueprint = Blueprint('saved_filters', __name__,
template_folder='templates')
@saved_filters_blueprint.route('/filters/add', methods=['POST'])
@ac_api_requires(no_cid_required=True)
def filters_add_route(caseid) -> Response:
"""
Add a new saved filter
args:
None
returns:
The new saved filter
"""
saved_filter_schema = SavedFilterSchema()
try:
# Load the JSON data from the request
data = request.get_json()
data['created_by'] = current_user.id
new_saved_filter = saved_filter_schema.load(data)
db.session.add(new_saved_filter)
db.session.commit()
track_activity(f'Search filter added', ctx_less=True)
return response_success(data=saved_filter_schema.dump(new_saved_filter))
except Exception as e:
# Handle any errors during deserialization or DB operations
return response_error(str(e))
@saved_filters_blueprint.route('/filters/update/<int:filter_id>', methods=['POST'])
@ac_api_requires(no_cid_required=True)
def filters_update_route(filter_id, caseid) -> Response:
"""
Update a saved filter
args:
filter_id: The ID of the saved filter to update
returns:
The updated saved filter
"""
saved_filter_schema = SavedFilterSchema()
try:
data = request.get_json()
saved_filter = get_filter_by_id(filter_id)
if not saved_filter:
return response_error('Filter not found')
saved_filter_schema.load(data, instance=saved_filter, partial=True)
db.session.commit()
track_activity(f'Search filter {filter_id} updated', ctx_less=True)
return response_success(data=saved_filter_schema.dump(saved_filter))
except Exception as e:
# Handle any errors during deserialization or DB operations
return response_error(str(e))
@saved_filters_blueprint.route('/filters/delete/<int:filter_id>', methods=['POST'])
@ac_api_requires(no_cid_required=True)
def filters_delete_route(filter_id, caseid) -> Response:
"""
Delete a saved filter
args:
filter_id: The ID of the saved filter to delete
returns:
Response object
"""
try:
saved_filter = get_filter_by_id(filter_id)
if not saved_filter:
return response_error('Filter not found')
db.session.delete(saved_filter)
db.session.commit()
track_activity(f'Search filter {filter_id} deleted', ctx_less=True)
return response_success()
except Exception as e:
# Handle any errors during DB operations
return response_error(str(e))
@saved_filters_blueprint.route('/filters/<int:filter_id>', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def filters_get_route(filter_id, caseid) -> Response:
"""
Get a saved filter
args:
filter_id: The ID of the saved filter to get
returns:
Response object
"""
saved_filter_schema = SavedFilterSchema()
try:
saved_filter = get_filter_by_id(filter_id)
if not saved_filter:
return response_error('Filter not found')
return response_success(data=saved_filter_schema.dump(saved_filter))
except Exception as e:
# Handle any errors during DB operations
return response_error(str(e))
@saved_filters_blueprint.route('/filters/<string:filter_type>/list', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def filters_list_route(filter_type, caseid) -> Response:
"""
List saved filters
args:
filter_type: The type of filter to list (e.g. 'search')
returns:
Response object
"""
saved_filter_schema = SavedFilterSchema(many=True)
try:
saved_filters = list_filters_by_type(str(filter_type).lower())
return response_success(data=saved_filter_schema.dump(saved_filters))
except Exception as e:
# Handle any errors during DB operations
return response_error(str(e))

View File

@@ -0,0 +1,163 @@
#!/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.
from urllib.parse import urlsplit
# IMPORTS ------------------------------------------------
from flask import Blueprint
from flask import redirect
from flask import render_template
from flask import request
from flask import session
from flask import url_for
from flask_login import current_user
from flask_login import login_user
from app import app
from app import bc
from app import db
from app.forms import LoginForm
from app.iris_engine.access_control.ldap_handler import ldap_authenticate
from app.iris_engine.access_control.utils import ac_get_effective_permissions_of_user
from app.iris_engine.utils.tracker import track_activity
from app.models.cases import Cases
from app.util import is_authentication_ldap
from app.datamgmt.manage.manage_users_db import get_active_user_by_login
login_blueprint = Blueprint(
'login',
__name__,
template_folder='templates'
)
log = app.logger
# filter User out of database through username
def _retrieve_user_by_username(username):
user = get_active_user_by_login(username)
if not user:
track_activity("someone tried to log in with user '{}', which does not exist".format(username),
ctx_less=True, display_in_ui=False)
return user
def _render_template_login(form, msg):
organisation_name = app.config.get('ORGANISATION_NAME')
login_banner = app.config.get('LOGIN_BANNER_TEXT')
ptfm_contact = app.config.get('LOGIN_PTFM_CONTACT')
return render_template('login.html', form=form, msg=msg, organisation_name=organisation_name,
login_banner=login_banner, ptfm_contact=ptfm_contact)
def _authenticate_ldap(form, username, password, local_fallback=True):
try:
if ldap_authenticate(username, password) is False:
if local_fallback is True:
track_activity("wrong login password for user '{}' using LDAP auth - falling back to local based on settings".format(username),
ctx_less=True, display_in_ui=False)
return _authenticate_password(form, username, password)
track_activity("wrong login password for user '{}' using LDAP auth".format(username),
ctx_less=True, display_in_ui=False)
return _render_template_login(form, 'Wrong credentials. Please try again.')
user = _retrieve_user_by_username(username)
if not user:
return _render_template_login(form, 'Wrong credentials. Please try again.')
return wrap_login_user(user)
except Exception as e:
log.error(e.__str__())
return _render_template_login(form, 'LDAP authentication unavailable. Check server logs')
def _authenticate_password(form, username, password):
user = _retrieve_user_by_username(username)
if not user or user.is_service_account:
return _render_template_login(form, 'Wrong credentials. Please try again.')
if bc.check_password_hash(user.password, password):
return wrap_login_user(user)
track_activity("wrong login password for user '{}' using local auth".format(username), ctx_less=True,
display_in_ui=False)
return _render_template_login(form, 'Wrong credentials. Please try again.')
# CONTENT ------------------------------------------------
# Authenticate user
if app.config.get("AUTHENTICATION_TYPE") in ["local", "ldap"]:
@login_blueprint.route('/login', methods=['GET', 'POST'])
def login():
session.permanent = True
if current_user.is_authenticated:
return redirect(url_for('index.index'))
form = LoginForm(request.form)
# check if both http method is POST and form is valid on submit
if not form.is_submitted() and not form.validate():
return _render_template_login(form, None)
# assign form data to variables
username = request.form.get('username', '', type=str)
password = request.form.get('password', '', type=str)
if is_authentication_ldap() is True:
return _authenticate_ldap(form, username, password, app.config.get('AUTHENTICATION_LOCAL_FALLBACK'))
return _authenticate_password(form, username, password)
def wrap_login_user(user):
login_user(user)
track_activity("user '{}' successfully logged-in".format(user.user), ctx_less=True, display_in_ui=False)
caseid = user.ctx_case
session['permissions'] = ac_get_effective_permissions_of_user(user)
if caseid is None:
case = Cases.query.order_by(Cases.case_id).first()
user.ctx_case = case.case_id
user.ctx_human_case = case.name
db.session.commit()
session['current_case'] = {
'case_name': user.ctx_human_case,
'case_info': "",
'case_id': user.ctx_case
}
track_activity("user '{}' successfully logged-in".format(user), ctx_less=True, display_in_ui=False)
next_url = None
if request.args.get('next'):
next_url = request.args.get('next') if 'cid=' in request.args.get('next') else request.args.get('next') + '?cid=' + str(user.ctx_case)
if not next_url or urlsplit(next_url).netloc != '':
next_url = url_for('index.index', cid=user.ctx_case)
return redirect(next_url)

View File

@@ -0,0 +1,73 @@
{% extends "layouts/static-default.html" %}
{% block title %} {{ organisation_name }} Login {% endblock title %}
{% block stylesheets %}
<link rel="stylesheet" href="/static/assets/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/assets/css/atlantis.css">
<link rel="stylesheet" href="/static/assets/css/login/login.css">
{% endblock stylesheets %}
{% block content %}
<body class="login">
<div class="login__wrapper row">
<div class="login__left_part col-xs-12 col-md-8">
<div class="login__logo">
<img src="/static/assets/img/logo-white.png">
</div>
<h3 class="text-white">{{ organisation_name }}</h3>
<span class="text-white text-center mt-4">{{ login_banner|replace('\n', '<br>')|safe }}</span>
</div>
<div class="login__right_part col-xs-12 col-md-4">
<h3 class="login__form_title">Sign In</h3>
<form method="post" action="" class="login__form">
{{ form.hidden_tag() }}
<div class="login__form">
<div class="login__form_field_container">
<label for="username"><b>Username</b></label>
<input id="username" name="username" type="text" class="form-control" required="">
</div>
<div class="login__form_field_container">
<label for="password"><b>Password</b></label>
<div class="login__form_input">
<input id="password" name="password" type="password" class="form-control" required="">
<div class="login__show_password" id="togglePassword">
<i class="icon-eye"></i>
</div>
</div>
</div>
{% if msg %}
<div class="alert alert-danger"> <b>Error:</b> {{ msg }} </div>
{% endif %}
<div class="login__form_field_container">
<button type="submit" class="btn btn-primary login__submit_button">Sign In</button>
</div>
</div>
</form>
<div class="login__contact_container">
{% if ptfm_contact %}
<span>Don't have an account yet ?</span><br/>
{{ ptfm_contact }}
{% endif %}
</div>
</div>
</div>
<script>
const togglePassword = document.querySelector('#togglePassword');
const password = document.querySelector('#password');
togglePassword.addEventListener('click', function (e) {
// toggle the type attribute
const type = password.getAttribute('type') === 'password' ? 'text' : 'password';
password.setAttribute('type', type);
// toggle the eye / eye slash icon
this.classList.toggle('login__show_password--disabled');
});
</script>
</body>
{% endblock content %}

View File

@@ -0,0 +1,97 @@
#!/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 flask import Blueprint
from flask import render_template
from flask import url_for
from flask_wtf import FlaskForm
from werkzeug.utils import redirect
from app.iris_engine.access_control.utils import ac_recompute_all_users_effective_ac
from app.iris_engine.access_control.utils import ac_recompute_effective_ac
from app.iris_engine.access_control.utils import ac_trace_effective_user_permissions
from app.iris_engine.access_control.utils import ac_trace_user_effective_cases_access_2
from app.models.authorization import Permissions
from app.util import ac_api_requires
from app.util import ac_requires
from app.util import response_success
manage_ac_blueprint = Blueprint(
'access_control',
__name__,
template_folder='templates/access_control'
)
@manage_ac_blueprint.route('/manage/access-control', methods=['GET'])
@ac_requires(Permissions.server_administrator)
def manage_ac_index(caseid, url_redir):
if url_redir:
return redirect(url_for('access_control.manage_ac_index', cid=caseid))
form = FlaskForm()
return render_template("manage_access-control.html", form=form)
@manage_ac_blueprint.route('/manage/access-control/recompute-effective-users-ac', methods=['GET'])
@ac_api_requires(Permissions.server_administrator)
def manage_ac_compute_effective_all_ac(caseid):
ac_recompute_all_users_effective_ac()
return response_success('Updated')
@manage_ac_blueprint.route('/manage/access-control/recompute-effective-user-ac/<int:cur_id>', methods=['GET'])
@ac_api_requires(Permissions.server_administrator)
def manage_ac_compute_effective_ac(cur_id, caseid):
ac_recompute_effective_ac(cur_id)
return response_success('Updated')
@manage_ac_blueprint.route('/manage/access-control/audit/users/<int:cur_id>', methods=['GET'])
@ac_api_requires(Permissions.server_administrator)
def manage_ac_audit_user(cur_id, caseid):
user_audit = {
'access_audit': ac_trace_user_effective_cases_access_2(cur_id),
'permissions_audit': ac_trace_effective_user_permissions(cur_id)
}
return response_success(data=user_audit)
@manage_ac_blueprint.route('/manage/access-control/audit/users/<int:cur_id>/modal', methods=['GET'])
@ac_api_requires(Permissions.server_administrator)
def manage_ac_audit_user_modal(cur_id, caseid):
access_audit = ac_trace_user_effective_cases_access_2(cur_id)
permissions_audit = ac_trace_effective_user_permissions(cur_id)
return render_template("modal_user_audit.html", access_audit=access_audit, permissions_audit=permissions_audit)
@manage_ac_blueprint.route('/manage/access-control/audit/users', methods=['GET'])
@ac_requires(Permissions.server_administrator)
def manage_ac_audit_users_page(caseid, url_redir):
form = FlaskForm()
return render_template("manage_user_audit.html", form=form)

View File

@@ -0,0 +1,143 @@
#!/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 flask import Blueprint, Response, request
from app.datamgmt.alerts.alerts_db import get_alert_status_list, get_alert_status_by_id, search_alert_status_by_name, \
get_alert_resolution_by_id, get_alert_resolution_list, search_alert_resolution_by_name
from app.schema.marshables import AlertStatusSchema, AlertResolutionSchema
from app.util import ac_api_requires, response_error
from app.util import response_success
manage_alerts_status_blueprint = Blueprint('manage_alerts_status',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@manage_alerts_status_blueprint.route('/manage/alert-status/list', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def list_alert_status(caseid: int) -> Response:
"""
Get the list of alert status
Args:
caseid (int): case id
Returns:
Flask Response object
"""
l_cl = get_alert_status_list()
schema = AlertStatusSchema()
return response_success("", data=schema.dump(l_cl, many=True))
@manage_alerts_status_blueprint.route('/manage/alert-status/<int:classification_id>', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def get_case_alert_status(classification_id: int, caseid: int) -> Response:
"""
Get the alert status
Args:
status_id (int): status id
caseid (int): case id
"""
cl = get_alert_status_by_id(classification_id)
schema = AlertStatusSchema()
return response_success("", data=schema.dump(cl))
@manage_alerts_status_blueprint.route('/manage/alert-status/search', methods=['POST'])
@ac_api_requires(no_cid_required=True)
def search_alert_status(caseid):
if not request.is_json:
return response_error("Invalid request")
alert_status = request.json.get('alert_status')
if alert_status is None:
return response_error("Invalid alert status. Got None")
exact_match = request.json.get('exact_match', False)
# Search for alerts status with a name that contains the specified search term
alert_status = search_alert_status_by_name(alert_status, exact_match=exact_match)
if not alert_status:
return response_error("No alert status found")
# Serialize the alert status and return them in a JSON response
schema = AlertStatusSchema(many=True)
return response_success("", data=schema.dump(alert_status))
@manage_alerts_status_blueprint.route('/manage/alert-resolutions/list', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def list_alert_resolution(caseid: int) -> Response:
"""
Get the list of alert resolution
Args:
caseid (int): case id
Returns:
Flask Response object
"""
l_cl = get_alert_resolution_list()
schema = AlertResolutionSchema()
return response_success("", data=schema.dump(l_cl, many=True))
@manage_alerts_status_blueprint.route('/manage/alert-resolutions/<int:resolution_id>', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def get_case_alert_resolution(resolution_id: int, caseid: int) -> Response:
"""
Get the alert resolution
Args:
resolution_id (int): resolution id
caseid (int): case id
"""
cl = get_alert_resolution_by_id(resolution_id)
schema = AlertResolutionSchema()
return response_success("", data=schema.dump(cl))
@manage_alerts_status_blueprint.route('/manage/alert-resolutions/search', methods=['POST'])
@ac_api_requires(no_cid_required=True)
def search_alert_resolution(caseid):
if not request.is_json:
return response_error("Invalid request")
alert_resolution = request.json.get('alert_resolution_name')
if alert_resolution is None:
return response_error("Invalid alert resolution. Got None")
exact_match = request.json.get('exact_match', False)
# Search for alerts resolution with a name that contains the specified search term
alert_res = search_alert_resolution_by_name(alert_resolution, exact_match=exact_match)
if not alert_res:
return response_error("No alert resolution found")
# Serialize the alert_res and return them in a JSON response
schema = AlertResolutionSchema(many=True)
return response_success("", data=schema.dump(alert_res))

View File

@@ -0,0 +1,113 @@
#!/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.
from flask import Blueprint, request
from werkzeug import Response
from app.datamgmt.case.case_assets_db import get_compromise_status_dict, get_case_outcome_status_dict
from app.datamgmt.manage.manage_case_objs import search_analysis_status_by_name
from app.models.models import AnalysisStatus
from app.schema.marshables import AnalysisStatusSchema
from app.util import api_login_required, ac_api_requires
from app.util import response_error
from app.util import response_success
manage_anastatus_blueprint = Blueprint('manage_anastatus',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@manage_anastatus_blueprint.route('/manage/analysis-status/list', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def list_anastatus(caseid):
lstatus = AnalysisStatus.query.with_entities(
AnalysisStatus.id,
AnalysisStatus.name
).all()
data = [row._asdict() for row in lstatus]
return response_success("", data=data)
@manage_anastatus_blueprint.route('/manage/compromise-status/list', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def list_compr_status(caseid):
compro_status = get_compromise_status_dict()
return response_success("", data=compro_status)
@manage_anastatus_blueprint.route('/manage/outcome-status/list', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def list_outcome_status(caseid) -> Response:
""" Returns a list of outcome status
Args:
caseid (int): Case ID
Returns:
Response: Flask response object
"""
outcome_status = get_case_outcome_status_dict()
return response_success("", data=outcome_status)
@manage_anastatus_blueprint.route('/manage/analysis-status/<int:cur_id>', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def view_anastatus(cur_id, caseid):
lstatus = AnalysisStatus.query.with_entities(
AnalysisStatus.id,
AnalysisStatus.name
).filter(
AnalysisStatus.id == cur_id
).first()
if not lstatus:
return response_error(f"Analysis status ID {cur_id} not found")
return response_success("", data=lstatus._asdict())
@manage_anastatus_blueprint.route('/manage/analysis-status/search', methods=['POST'])
@ac_api_requires(no_cid_required=True)
def search_analysis_status(caseid):
if not request.is_json:
return response_error("Invalid request")
analysis_status = request.json.get('analysis_status')
if analysis_status is None:
return response_error("Invalid analysis status. Got None")
exact_match = request.json.get('exact_match', False)
# Search for analysis status with a name that contains the specified search term
analysis_status = search_analysis_status_by_name(analysis_status, exact_match=exact_match)
if not analysis_status:
return response_error("No analysis status found")
# Serialize the analysis status and return them in a JSON response
schema = AnalysisStatusSchema(many=True)
return response_success("", data=schema.dump(analysis_status))
# TODO : Add management of analysis status

View File

@@ -0,0 +1,249 @@
#!/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 logging
import marshmallow
import os
from flask import Blueprint
from flask import redirect
from flask import render_template
from flask import request
from flask import url_for
from app import app
from app import db
from app.datamgmt.manage.manage_case_objs import search_asset_type_by_name
from app.forms import AddAssetForm
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import Permissions
from app.models.models import AssetsType
from app.models.models import CaseAssets
from app.schema.marshables import AssetTypeSchema
from app.util import ac_api_requires
from app.util import ac_requires
from app.util import response_error
from app.util import response_success
manage_assets_blueprint = Blueprint('manage_assets',
__name__,
template_folder='templates')
@manage_assets_blueprint.route('/manage/asset-type/list')
@ac_api_requires(no_cid_required=True)
def list_assets(caseid):
# Get all assets
assets = AssetsType.query.with_entities(
AssetsType.asset_name,
AssetsType.asset_description,
AssetsType.asset_id,
AssetsType.asset_icon_compromised,
AssetsType.asset_icon_not_compromised,
).all()
data = []
for row in assets:
row_dict = row._asdict()
row_dict['asset_icon_compromised_path'] = os.path.join(app.config['ASSET_SHOW_PATH'],row_dict['asset_icon_compromised'])
row_dict['asset_icon_not_compromised_path'] = os.path.join(app.config['ASSET_SHOW_PATH'],row_dict['asset_icon_not_compromised'])
data.append(row_dict)
# data = [row._asdict() for row in assets]
# Return the assets
return response_success("", data=data)
@manage_assets_blueprint.route('/manage/asset-type/<int:cur_id>', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def view_asset_api(cur_id, caseid):
# Get all assets
asset_type = AssetsType.query.with_entities(
AssetsType.asset_name,
AssetsType.asset_description,
AssetsType.asset_id
).filter(
AssetsType.asset_id == cur_id
).first()
if not asset_type:
return response_error(f'Invalid asset type ID {cur_id}')
# Return the assets
return response_success("", data=asset_type._asdict())
@manage_assets_blueprint.route('/manage/asset-type/update/<int:cur_id>/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def view_assets_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('manage_assets.manage_assets', cid=caseid))
form = AddAssetForm()
asset = AssetsType.query.filter(AssetsType.asset_id == cur_id).first()
if not asset:
return response_error("Invalid asset type ID")
form.asset_name.render_kw = {'value': asset.asset_name}
form.asset_description.render_kw = {'value': asset.asset_description}
form.asset_icon_compromised.render_kw = {'value': asset.asset_icon_compromised}
form.asset_icon_not_compromised.render_kw = {'value': asset.asset_icon_not_compromised}
setattr(asset, 'asset_icon_compromised_path', os.path.join(app.config['ASSET_SHOW_PATH'], asset.asset_icon_compromised))
setattr(asset, 'asset_icon_not_compromised_path', os.path.join(app.config['ASSET_SHOW_PATH'], asset.asset_icon_not_compromised))
return render_template("modal_add_asset_type.html", form=form, assettype=asset)
@manage_assets_blueprint.route('/manage/asset-type/update/<int:cur_id>', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def view_assets(cur_id, caseid):
asset_type = AssetsType.query.filter(AssetsType.asset_id == cur_id).first()
if not asset_type:
return response_error("Invalid asset type ID")
asset_schema = AssetTypeSchema()
try:
asset_sc = asset_schema.load(request.form, instance=asset_type)
fpath_nc = asset_schema.load_store_icon(request.files.get('asset_icon_not_compromised'),
'asset_icon_not_compromised')
fpath_c = asset_schema.load_store_icon(request.files.get('asset_icon_compromised'), 'asset_icon_compromised')
if fpath_nc is not None:
asset_sc.asset_icon_not_compromised = fpath_nc
if fpath_c is not None:
asset_sc.asset_icon_compromised = fpath_c
if asset_sc:
track_activity("updated asset type {}".format(asset_sc.asset_name), caseid=caseid)
return response_success("Asset type updated", asset_sc)
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
return response_error("Unexpected error server-side. Nothing updated")
@manage_assets_blueprint.route('/manage/asset-type/add/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def add_assets_modal(caseid, url_redir):
if url_redir:
return redirect(url_for('manage_assets.manage_assets', cid=caseid))
form = AddAssetForm()
return render_template("modal_add_asset_type.html", form=form, assettype=None)
@manage_assets_blueprint.route('/manage/asset-type/add', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def add_assets(caseid):
asset_schema = AssetTypeSchema()
try:
asset_sc = asset_schema.load(request.form)
fpath_nc = asset_schema.load_store_icon(request.files.get('asset_icon_not_compromised'),
'asset_icon_not_compromised')
fpath_c = asset_schema.load_store_icon(request.files.get('asset_icon_compromised'), 'asset_icon_compromised')
if fpath_nc is not None:
asset_sc.asset_icon_not_compromised = fpath_nc
if fpath_c is not None:
asset_sc.asset_icon_compromised = fpath_c
if asset_sc:
db.session.add(asset_sc)
db.session.commit()
track_activity("updated asset type {}".format(asset_sc.asset_name), caseid=caseid)
return response_success("Asset type updated", asset_sc)
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
return response_error("Unexpected error server-side. Nothing updated")
@manage_assets_blueprint.route('/manage/asset-type/delete/<int:cur_id>', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def delete_assets(cur_id, caseid):
asset = AssetsType.query.filter(AssetsType.asset_id == cur_id).first()
if not asset:
return response_error("Invalid asset ID")
case_linked = CaseAssets.query.filter(CaseAssets.asset_type_id == cur_id).first()
if case_linked:
return response_error("Cannot delete a referenced asset type. Please delete any assets of this type first.")
try:
#not deleting icons for now because multiple asset_types might rely on the same icon
#only delete icons if there is only one AssetType linked to it
if len(AssetsType.query.filter(AssetsType.asset_icon_compromised == asset.asset_icon_compromised).all()) == 1:
os.unlink(os.path.join(app.config['ASSET_STORE_PATH'], asset.asset_icon_compromised))
if len(AssetsType.query.filter(AssetsType.asset_icon_not_compromised == asset.asset_icon_not_compromised).all()) == 1:
os.unlink(os.path.join(app.config['ASSET_STORE_PATH'], asset.asset_icon_not_compromised))
except Exception as e:
logging.error(f"Unable to delete {e}")
db.session.delete(asset)
track_activity("Deleted asset type ID {asset_id}".format(asset_id=cur_id), caseid=caseid, ctx_less=True)
return response_success("Deleted asset type ID {cur_id} successfully".format(cur_id=cur_id))
@manage_assets_blueprint.route('/manage/asset-types/search', methods=['POST'])
@ac_api_requires(no_cid_required=True)
def search_assets_type(caseid):
"""Searches for assets types in the database.
This function searches for assets types in the database with a name that contains the specified search term.
It returns a JSON response containing the matching assets types.
Args:
caseid: The ID of the case associated with the request.
Returns:
A JSON response containing the matching assets types.
"""
if not request.is_json:
return response_error("Invalid request")
asset_type = request.json.get('asset_type')
if asset_type is None:
return response_error("Invalid asset type. Got None")
exact_match = request.json.get('exact_match', False)
# Search for assets types with a name that contains the specified search term
assets_type = search_asset_type_by_name(asset_type, exact_match=exact_match)
if not assets_type:
return response_error("No asset types found")
# Serialize the assets types and return them in a JSON response
assetst_schema = AssetTypeSchema(many=True)
return response_success("", data=assetst_schema.dump(assets_type))

View File

@@ -0,0 +1,149 @@
#!/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
from flask import Blueprint
from flask import redirect
from flask import render_template
from flask import request
from flask import url_for
from app import db
from app.datamgmt.manage.manage_attribute_db import update_all_attributes
from app.datamgmt.manage.manage_attribute_db import validate_attribute
from app.forms import AddAssetForm
from app.forms import AttributeForm
from app.models.authorization import Permissions
from app.models.models import CustomAttribute
from app.util import ac_api_requires
from app.util import ac_requires
from app.util import response_error
from app.util import response_success
manage_attributes_blueprint = Blueprint('manage_attributes',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@manage_attributes_blueprint.route('/manage/attributes')
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def manage_attributes(caseid, url_redir):
if url_redir:
return redirect(url_for('manage_attributes.manage_attributes', cid=caseid))
form = AddAssetForm()
return render_template('manage_attributes.html', form=form)
@manage_attributes_blueprint.route('/manage/attributes/list')
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def list_attributes(caseid):
# Get all attributes
attributes = CustomAttribute.query.with_entities(
CustomAttribute.attribute_id,
CustomAttribute.attribute_content,
CustomAttribute.attribute_display_name,
CustomAttribute.attribute_description,
CustomAttribute.attribute_for
).all()
data = [row._asdict() for row in attributes]
# Return the attributes
return response_success("", data=data)
@manage_attributes_blueprint.route('/manage/attributes/<int:cur_id>/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def attributes_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('manage_attributes.manage_attributes', cid=caseid))
form = AttributeForm()
attribute = CustomAttribute.query.filter(CustomAttribute.attribute_id == cur_id).first()
if not attribute:
return response_error(f"Invalid Attribute ID {cur_id}")
form.attribute_content.data = attribute.attribute_content
return render_template("modal_add_attribute.html", form=form, attribute=attribute)
@manage_attributes_blueprint.route('/manage/attributes/preview', methods=['POST'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def attributes_preview(caseid, url_redir):
if url_redir:
return redirect(url_for('manage_attributes.manage_attributes', cid=caseid))
data = request.get_json()
if not data:
return response_error(f"Invalid request")
attribute = data.get('attribute_content')
if not attribute:
return response_error(f"Invalid request")
try:
attribute = json.loads(attribute)
except Exception as e:
return response_error("Invalid JSON", data=str(e))
templated = render_template("modal_preview_attribute.html", attributes=attribute)
return response_success(data=templated)
@manage_attributes_blueprint.route('/manage/attributes/update/<int:cur_id>', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def update_attribute(cur_id, caseid):
if not request.is_json:
return response_error("Invalid request")
attribute = CustomAttribute.query.filter(CustomAttribute.attribute_id == cur_id).first()
if not attribute:
return response_error(f"Invalid Attribute ID {cur_id}")
data = request.get_json()
attr_content = data.get('attribute_content')
if not attr_content:
return response_error("Invalid request")
attr_contents, logs = validate_attribute(attr_content)
if len(logs) > 0:
return response_error("Found errors in attribute", data=logs)
previous_attribute = attribute.attribute_content
attribute.attribute_content = attr_contents
db.session.commit()
# Now try to update every attributes by merging the updated ones
complete_overwrite = data.get('complete_overwrite')
complete_overwrite = complete_overwrite if complete_overwrite else False
partial_overwrite = data.get('partial_overwrite')
partial_overwrite = partial_overwrite if partial_overwrite else False
update_all_attributes(attribute.attribute_for, partial_overwrite=partial_overwrite,
complete_overwrite=complete_overwrite, previous_attribute=previous_attribute)
return response_success("Attribute updated")

View File

@@ -0,0 +1,243 @@
#!/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.
import marshmallow
from typing import Union
from flask import Blueprint, Response, url_for, render_template, request
from werkzeug.utils import redirect
from app import db
from app.datamgmt.manage.manage_case_classifications_db import get_case_classifications_list, \
get_case_classification_by_id, search_classification_by_name
from app.forms import CaseClassificationForm
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import Permissions
from app.schema.marshables import CaseClassificationSchema
from app.util import ac_api_requires, response_error, ac_requires
from app.util import response_success
manage_case_classification_blueprint = Blueprint('manage_case_classifications',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@manage_case_classification_blueprint.route('/manage/case-classifications/list', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def list_case_classifications(caseid: int) -> Response:
"""Get the list of case classifications
Args:
caseid (int): case id
Returns:
Flask Response object
"""
l_cl = get_case_classifications_list()
return response_success("", data=l_cl)
@manage_case_classification_blueprint.route('/manage/case-classifications/<int:classification_id>', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def get_case_classification(classification_id: int, caseid: int) -> Response:
"""Get a case classification
Args:
classification_id (int): case classification id
caseid (int): case id
Returns:
Flask Response object
"""
schema = CaseClassificationSchema()
case_classification = get_case_classification_by_id(classification_id)
if not case_classification:
return response_error(f"Invalid case classification ID {classification_id}")
return response_success("", data=schema.dump(case_classification))
@manage_case_classification_blueprint.route('/manage/case-classifications/update/<int:classification_id>/modal',
methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def update_case_classification_modal(classification_id: int, caseid: int, url_redir: bool) -> Union[str, Response]:
"""Update a case classification
Args:
classification_id (int): case classification id
caseid (int): case id
url_redir (bool): redirect to url
Returns:
Flask Response object or str
"""
if url_redir:
return redirect(url_for('manage_case_classification_blueprint.update_case_classification_modal',
classification_id=classification_id, caseid=caseid))
classification_form = CaseClassificationForm()
case_classification = get_case_classification_by_id(classification_id)
if not case_classification:
return response_error(f"Invalid case classification ID {classification_id}")
classification_form.name.render_kw = {'value': case_classification.name}
classification_form.name_expanded.render_kw = {'value': case_classification.name_expanded}
classification_form.description.render_kw = {'value': case_classification.description}
return render_template("modal_case_classification.html", form=classification_form,
case_classification=case_classification)
@manage_case_classification_blueprint.route('/manage/case-classifications/update/<int:classification_id>',
methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def update_case_classification(classification_id: int, caseid: int) -> Response:
"""Update a case classification
Args:
classification_id (int): case classification id
caseid (int): case id
Returns:
Flask Response object
"""
if not request.is_json:
return response_error("Invalid request")
case_classification = get_case_classification_by_id(classification_id)
if not case_classification:
return response_error(f"Invalid case classification ID {classification_id}")
ccl = CaseClassificationSchema()
try:
ccls = ccl.load(request.get_json(), instance=case_classification)
if ccls:
track_activity(f"updated case classification {ccls.id}", caseid=caseid)
return response_success("Case classification updated", ccl.dump(ccls))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
return response_error("Unexpected error server-side. Nothing updated", data=case_classification)
@manage_case_classification_blueprint.route('/manage/case-classifications/add/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def add_case_classification_modal(caseid: int, url_redir: bool) -> Union[str, Response]:
"""Add a case classification
Args:
caseid (int): case id
url_redir (bool): redirect to url
Returns:
Flask Response object or str
"""
if url_redir:
return redirect(url_for('manage_case_classification_blueprint.add_case_classification_modal',
caseid=caseid))
classification_form = CaseClassificationForm()
return render_template("modal_case_classification.html", form=classification_form, case_classification=None)
@manage_case_classification_blueprint.route('/manage/case-classifications/add', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def add_case_classification(caseid: int) -> Response:
"""Add a case classification
Args:
caseid (int): case id
Returns:
Flask Response object
"""
if not request.is_json:
return response_error("Invalid request")
ccl = CaseClassificationSchema()
try:
ccls = ccl.load(request.get_json())
if ccls:
db.session.add(ccls)
db.session.commit()
track_activity(f"added case classification {ccls.name}", caseid=caseid)
return response_success("Case classification added", ccl.dump(ccls))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
return response_error("Unexpected error server-side. Nothing added", data=None)
@manage_case_classification_blueprint.route('/manage/case-classifications/delete/<int:classification_id>',
methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def delete_case_classification(classification_id: int, caseid: int) -> Response:
"""Delete a case classification
Args:
classification_id (int): case classification id
caseid (int): case id
Returns:
Flask Response object
"""
case_classification = get_case_classification_by_id(classification_id)
if not case_classification:
return response_error(f"Invalid case classification ID {classification_id}")
db.session.delete(case_classification)
db.session.commit()
track_activity(f"deleted case classification {case_classification.name}", caseid=caseid)
return response_success("Case classification deleted")
@manage_case_classification_blueprint.route('/manage/case-classifications/search', methods=['POST'])
@ac_api_requires(no_cid_required=True)
def search_alert_status(caseid):
if not request.is_json:
return response_error("Invalid request")
classification_name = request.json.get('classification_name')
if classification_name is None:
return response_error("Invalid classification name. Got None")
exact_match = request.json.get('exact_match', False)
# Search for classifications with a name that contains the specified search term
classification = search_classification_by_name(classification_name, exact_match=exact_match)
if not classification:
return response_error("No classification found")
# Serialize the case classification and return them in a JSON response
schema = CaseClassificationSchema(many=True)
return response_success("", data=schema.dump(classification))

View File

@@ -0,0 +1,231 @@
#!/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.
import marshmallow
from typing import Union
from flask import Blueprint, Response, url_for, render_template, request
from werkzeug.utils import redirect
from app import db
from app.datamgmt.manage.manage_case_state_db import get_case_states_list, \
get_case_state_by_id, get_cases_using_state
from app.forms import CaseStateForm
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import Permissions
from app.schema.marshables import CaseStateSchema
from app.util import ac_api_requires, response_error, ac_requires
from app.util import response_success
manage_case_state_blueprint = Blueprint('manage_case_state',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@manage_case_state_blueprint.route('/manage/case-states/list', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def list_case_state(caseid: int) -> Response:
"""Get the list of case state
Args:
caseid (int): case id
Returns:
Flask Response object
"""
l_cl = get_case_states_list()
return response_success("", data=l_cl)
@manage_case_state_blueprint.route('/manage/case-states/<int:state_id>', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def get_case_state(state_id: int, caseid: int) -> Response:
"""Get a case state
Args:
state_id (int): case state id
caseid (int): case id
Returns:
Flask Response object
"""
schema = CaseStateSchema()
case_state = get_case_state_by_id(state_id)
if not case_state:
return response_error(f"Invalid case state ID {state_id}")
return response_success("", data=schema.dump(case_state))
@manage_case_state_blueprint.route('/manage/case-states/update/<int:state_id>/modal',
methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def update_case_state_modal(state_id: int, caseid: int, url_redir: bool) -> Union[str, Response]:
"""Update a case state
Args:
state_id (int): case state id
caseid (int): case id
url_redir (bool): redirect to url
Returns:
Flask Response object or str
"""
if url_redir:
return redirect(url_for('manage_case_state_blueprint.update_case_state_modal',
state_id=state_id, caseid=caseid))
state_form = CaseStateForm()
case_state = get_case_state_by_id(state_id)
if not case_state:
return response_error(f"Invalid case state ID {state_id}")
state_form.state_name.render_kw = {'value': case_state.state_name}
state_form.state_description.render_kw = {'value': case_state.state_description}
return render_template("modal_case_state.html", form=state_form,
case_state=case_state)
@manage_case_state_blueprint.route('/manage/case-states/update/<int:state_id>',
methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def update_case_state(state_id: int, caseid: int) -> Response:
"""Update a case state
Args:
state_id (int): case state id
caseid (int): case id
Returns:
Flask Response object
"""
if not request.is_json:
return response_error("Invalid request")
case_state = get_case_state_by_id(state_id)
if not case_state:
return response_error(f"Invalid case state ID {state_id}")
if case_state.protected:
return response_error(f"Case state {case_state.state_name} is protected")
ccl = CaseStateSchema()
try:
ccls = ccl.load(request.get_json(), instance=case_state)
if ccls:
track_activity(f"updated case state {ccls.state_id}", caseid=caseid)
return response_success("Case state updated", ccl.dump(ccls))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
return response_error("Unexpected error server-side. Nothing updated", data=case_state)
@manage_case_state_blueprint.route('/manage/case-states/add/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def add_case_state_modal(caseid: int, url_redir: bool) -> Union[str, Response]:
"""Add a case state
Args:
caseid (int): case id
url_redir (bool): redirect to url
Returns:
Flask Response object or str
"""
if url_redir:
return redirect(url_for('manage_case_state_blueprint.add_case_state_modal',
caseid=caseid))
state_form = CaseStateForm()
return render_template("modal_case_state.html", form=state_form, case_state=None)
@manage_case_state_blueprint.route('/manage/case-states/add', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def add_case_state(caseid: int) -> Response:
"""Add a case state
Args:
caseid (int): case id
Returns:
Flask Response object
"""
if not request.is_json:
return response_error("Invalid request")
ccl = CaseStateSchema()
try:
ccls = ccl.load(request.get_json())
if ccls:
db.session.add(ccls)
db.session.commit()
track_activity(f"added case state {ccls.state_name}", caseid=caseid)
return response_success("Case state added", ccl.dump(ccls))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
return response_error("Unexpected error server-side. Nothing added", data=None)
@manage_case_state_blueprint.route('/manage/case-states/delete/<int:state_id>',
methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def delete_case_state(state_id: int, caseid: int) -> Response:
"""Delete a case state
Args:
state_id (int): case state id
caseid (int): case id
Returns:
Flask Response object
"""
case_state = get_case_state_by_id(state_id)
if not case_state:
return response_error(f"Invalid case state ID {state_id}")
if case_state.protected:
return response_error(f"Case state {case_state.state_name} is protected")
cases = get_cases_using_state(case_state.state_id)
if cases:
return response_error(f"Case state {case_state.state_name} is in use by case(s)"
f" {','.join([str(c.case_id) for c in cases])}")
db.session.delete(case_state)
db.session.commit()
track_activity(f"deleted case state {case_state.state_name}", caseid=caseid)
return response_success("Case state deleted")

View File

@@ -0,0 +1,251 @@
#!/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.
# IMPORTS ------------------------------------------------
import json
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 marshmallow import ValidationError
from app import db
from app.datamgmt.manage.manage_case_templates_db import get_case_templates_list
from app.datamgmt.manage.manage_case_templates_db import get_case_template_by_id
from app.datamgmt.manage.manage_case_templates_db import validate_case_template
from app.datamgmt.manage.manage_case_templates_db import delete_case_template_by_id
from app.forms import CaseTemplateForm, AddAssetForm
from app.models import CaseTemplate
from app.models.authorization import Permissions
from app.iris_engine.utils.tracker import track_activity
from app.schema.marshables import CaseTemplateSchema
from app.util import ac_api_requires
from app.util import ac_requires
from app.util import response_error
from app.util import response_success
manage_case_templates_blueprint = Blueprint('manage_case_templates',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@manage_case_templates_blueprint.route('/manage/case-templates', methods=['GET'])
@ac_requires(Permissions.case_templates_read)
def manage_case_templates(caseid, url_redir):
if url_redir:
return redirect(url_for('manage_case_templates.manage_case_templates', cid=caseid))
form = AddAssetForm()
return render_template('manage_case_templates.html', form=form)
@manage_case_templates_blueprint.route('/manage/case-templates/list', methods=['GET'])
@ac_api_requires()
def list_case_templates(caseid):
"""Show a list of case templates
Returns:
Response: List of case templates
"""
case_templates = get_case_templates_list()
# Return the attributes
return response_success("", data=case_templates)
@manage_case_templates_blueprint.route('/manage/case-templates/<int:cur_id>/modal', methods=['GET'])
@ac_requires(Permissions.case_templates_read)
def case_template_modal(cur_id, caseid, url_redir):
"""Get a case template
Args:
cur_id (int): case template id
Returns:
HTML Template: Case template modal
"""
if url_redir:
return redirect(url_for('manage_case_templates.manage_case_templates', cid=caseid))
form = CaseTemplateForm()
case_template = get_case_template_by_id(cur_id)
if not case_template:
return response_error(f"Invalid Case template ID {cur_id}")
# Temporary : for now we build the full JSON form object based on case templates attributes
# Next step : add more fields to the form
case_template_dict = {
"name": case_template.name,
"display_name": case_template.display_name,
"description": case_template.description,
"author": case_template.author,
"title_prefix": case_template.title_prefix,
"summary": case_template.summary,
"tags": case_template.tags,
"tasks": case_template.tasks,
"note_groups": case_template.note_groups,
"classification": case_template.classification
}
form.case_template_json.data = case_template_dict
return render_template("modal_case_template.html", form=form, case_template=case_template)
@manage_case_templates_blueprint.route('/manage/case-templates/add/modal', methods=['GET'])
@ac_api_requires(Permissions.case_templates_write)
def add_template_modal(caseid):
case_template = CaseTemplate()
form = CaseTemplateForm()
form.case_template_json.data = {
"name": "Template name",
"display_name": "Template Display Name",
"description": "Template description",
"author": "YOUR NAME",
"classification": "known-template-classification",
"title_prefix": "[PREFIX]",
"summary": "Summary to be set",
"tags": ["ransomware","malware"],
"tasks": [
{
"title": "Task 1",
"description": "Task 1 description",
"tags": ["tag1", "tag2"]
}
],
"note_groups": [
{
"title": "Note group 1",
"notes": [
{
"title": "Note 1",
"content": "Note 1 content"
}
]
}
]
}
return render_template("modal_case_template.html", form=form, case_template=case_template)
@manage_case_templates_blueprint.route('/manage/case-templates/upload/modal', methods=['GET'])
@ac_api_requires(Permissions.case_templates_write)
def upload_template_modal(caseid):
return render_template("modal_upload_case_template.html")
@manage_case_templates_blueprint.route('/manage/case-templates/add', methods=['POST'])
@ac_api_requires(Permissions.case_templates_write)
def add_case_template(caseid):
data = request.get_json()
if not data:
return response_error("Invalid request")
case_template_json = data.get('case_template_json')
if not case_template_json:
return response_error("Invalid request")
try:
case_template_dict = json.loads(case_template_json)
except Exception as e:
return response_error("Invalid JSON", data=str(e))
try:
logs = validate_case_template(case_template_dict, update=False)
if logs is not None:
return response_error("Found errors in case template", data=logs)
except Exception as e:
return response_error("Found errors in case template", data=str(e))
try:
case_template_dict["created_by_user_id"] = current_user.id
case_template_data = CaseTemplateSchema().load(case_template_dict)
case_template = CaseTemplate(**case_template_data)
db.session.add(case_template)
db.session.commit()
except Exception as e:
return response_error("Could not add case template into DB", data=str(e))
track_activity(f"Case template '{case_template.name}' added", caseid=caseid, ctx_less=True)
return response_success("Added successfully", data=CaseTemplateSchema().dump(case_template))
@manage_case_templates_blueprint.route('/manage/case-templates/update/<int:cur_id>', methods=['POST'])
@ac_api_requires(Permissions.case_templates_write)
def update_case_template(cur_id, caseid):
if not request.is_json:
return response_error("Invalid request")
case_template = get_case_template_by_id(cur_id)
if not case_template:
return response_error(f"Invalid Case template ID {cur_id}")
data = request.get_json()
updated_case_template_json = data.get('case_template_json')
if not updated_case_template_json:
return response_error("Invalid request")
try:
updated_case_template_dict = json.loads(updated_case_template_json)
except Exception as e:
return response_error("Invalid JSON", data=str(e))
try:
logs = validate_case_template(updated_case_template_dict, update=True)
if logs is not None:
return response_error("Found errors in case template", data=logs)
except Exception as e:
return response_error("Found errors in case template", data=str(e))
case_template_schema = CaseTemplateSchema()
try:
# validate the request data and load it into an instance of the `CaseTemplate` object
case_template_data = case_template_schema.load(updated_case_template_dict, partial=True)
# update the existing `case_template` object with the new data
case_template.update_from_dict(case_template_data)
# commit the changes to the database
db.session.commit()
except ValidationError as error:
return response_error("Could not validate case template", data=str(error))
return response_success("Case template updated")
@manage_case_templates_blueprint.route('/manage/case-templates/delete/<int:case_template_id>', methods=['POST'])
@ac_api_requires(Permissions.case_templates_write)
def delete_case_template(case_template_id, caseid):
case_template = get_case_template_by_id(case_template_id)
if case_template is None:
return response_error('Case template not found')
case_template_name = case_template.name
delete_case_template_by_id(case_template_id)
db.session.commit()
track_activity(f"Case template '{case_template_name}' deleted", caseid=caseid, ctx_less=True)
return response_success("Deleted successfully")

View File

@@ -0,0 +1,568 @@
#!/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)

View File

@@ -0,0 +1,404 @@
#!/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 datetime
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_wtf import FlaskForm
from marshmallow import ValidationError
from app.datamgmt.client.client_db import create_client
from app.datamgmt.client.client_db import create_contact
from app.datamgmt.client.client_db import delete_client
from app.datamgmt.client.client_db import delete_contact
from app.datamgmt.client.client_db import get_client
from app.datamgmt.client.client_db import get_client_api
from app.datamgmt.client.client_db import get_client_cases
from app.datamgmt.client.client_db import get_client_contact
from app.datamgmt.client.client_db import get_client_contacts
from app.datamgmt.client.client_db import get_client_list
from app.datamgmt.client.client_db import update_client
from app.datamgmt.client.client_db import update_contact
from app.datamgmt.exceptions.ElementExceptions import ElementInUseException
from app.datamgmt.exceptions.ElementExceptions import ElementNotFoundException
from app.datamgmt.manage.manage_attribute_db import get_default_custom_attributes
from app.forms import AddCustomerForm
from app.forms import ContactForm
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import Permissions
from app.schema.marshables import ContactSchema
from app.schema.marshables import CustomerSchema
from app.util import ac_api_requires
from app.util import ac_requires
from app.util import page_not_found
from app.util import response_error
from app.util import response_success
manage_customers_blueprint = Blueprint(
'manage_customers',
__name__,
template_folder='templates'
)
# CONTENT ------------------------------------------------
@manage_customers_blueprint.route('/manage/customers')
@ac_requires(Permissions.customers_read, no_cid_required=True)
def manage_customers(caseid, url_redir):
if url_redir:
return redirect(url_for('manage_customers.manage_customers', cid=caseid))
form = AddCustomerForm()
# Return default page of case management
return render_template('manage_customers.html', form=form)
@manage_customers_blueprint.route('/manage/customers/list')
@ac_api_requires(Permissions.customers_read, no_cid_required=True)
def list_customers(caseid):
client_list = get_client_list()
return response_success("", data=client_list)
@manage_customers_blueprint.route('/manage/customers/<int:cur_id>', methods=['GET'])
@ac_api_requires(Permissions.customers_read, no_cid_required=True)
def view_customer(cur_id, caseid):
customer = get_client_api(cur_id)
if not customer:
return page_not_found(None)
contacts = get_client_contacts(cur_id)
contacts = ContactSchema().dump(contacts, many=True)
customer['contacts'] = contacts
return response_success(data=customer)
@manage_customers_blueprint.route('/manage/customers/<int:cur_id>/view', methods=['GET'])
@ac_requires(Permissions.customers_read, no_cid_required=True)
def view_customer_page(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('manage_customers.manage_customers', cid=caseid))
customer = get_client_api(cur_id)
if not customer:
return page_not_found(None)
form = FlaskForm()
contacts = get_client_contacts(cur_id)
contacts = ContactSchema().dump(contacts, many=True)
return render_template('manage_customer_view.html', customer=customer, form=form, contacts=contacts)
@manage_customers_blueprint.route('/manage/customers/<int:cur_id>/contacts/add/modal', methods=['GET'])
@ac_requires(Permissions.customers_write, no_cid_required=True)
def customer_add_contact_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('manage_customers.manage_customers', cid=caseid))
form = ContactForm()
return render_template('modal_customer_add_contact.html', form=form, contact=None)
@manage_customers_blueprint.route('/manage/customers/<int:cur_id>/contacts/<int:contact_id>/modal', methods=['GET'])
@ac_requires(Permissions.customers_read, no_cid_required=True)
def customer_edit_contact_modal(cur_id, contact_id, caseid, url_redir):
if url_redir:
return redirect(url_for('manage_customers.manage_customers', cid=caseid))
contact = get_client_contact(cur_id, contact_id)
if not contact:
return response_error(f"Invalid Contact ID {contact_id}")
form = ContactForm()
form.contact_name.render_kw = {'value': contact.contact_name}
form.contact_email.render_kw = {'value': contact.contact_email}
form.contact_mobile_phone.render_kw = {'value': contact.contact_mobile_phone}
form.contact_work_phone.render_kw = {'value': contact.contact_work_phone}
form.contact_note.data = contact.contact_note
form.contact_role.render_kw = {'value': contact.contact_role}
return render_template('modal_customer_add_contact.html', form=form, contact=contact)
@manage_customers_blueprint.route('/manage/customers/<int:cur_id>/contacts/<int:contact_id>/update', methods=['POST'])
@ac_api_requires(Permissions.customers_write, no_cid_required=True)
def customer_update_contact(cur_id, contact_id, caseid):
if not request.is_json:
return response_error("Invalid request")
if not get_client(cur_id):
return response_error(f"Invalid Customer ID {cur_id}")
try:
contact = update_contact(request.json, contact_id, cur_id)
except ValidationError as e:
return response_error(msg='Error update contact', data=e.messages, status=400)
except Exception as e:
print(traceback.format_exc())
return response_error(f'An error occurred during contact update. {e}')
track_activity(f"Updated contact {contact.contact_name}", caseid=caseid, ctx_less=True)
# Return the customer
contact_schema = ContactSchema()
return response_success("Added successfully", data=contact_schema.dump(contact))
@manage_customers_blueprint.route('/manage/customers/<int:cur_id>/contacts/add', methods=['POST'])
@ac_api_requires(Permissions.customers_write, no_cid_required=True)
def customer_add_contact(cur_id, caseid):
if not request.is_json:
return response_error("Invalid request")
if not get_client(cur_id):
return response_error(f"Invalid Customer ID {cur_id}")
try:
contact = create_contact(request.json, cur_id)
except ValidationError as e:
return response_error(msg='Error adding contact', data=e.messages, status=400)
except Exception as e:
print(traceback.format_exc())
return response_error(f'An error occurred during contact addition. {e}')
track_activity(f"Added contact {contact.contact_name}", caseid=caseid, ctx_less=True)
# Return the customer
contact_schema = ContactSchema()
return response_success("Added successfully", data=contact_schema.dump(contact))
@manage_customers_blueprint.route('/manage/customers/<int:cur_id>/cases', methods=['GET'])
@ac_api_requires(Permissions.customers_read, no_cid_required=True)
def get_customer_case_stats(cur_id, caseid):
cases = get_client_cases(cur_id)
cases_list = []
now = datetime.date.today()
cases_stats = {
'cases_last_month': 0,
'cases_last_year': 0,
'open_cases': 0,
'last_year': now.year - 1,
'last_month': now.month - 1,
'cases_rolling_week': 0,
'cases_current_month': 0,
'cases_current_year': 0,
'ratio_year': 0,
'ratio_month': 0,
'average_case_duration': 0,
'cases_total': len(cases)
}
last_month_start = datetime.date.today() - datetime.timedelta(days=30)
last_month_end = datetime.date(now.year, now.month, 1)
last_year_start = datetime.date(now.year - 1, 1, 1)
last_year_end = datetime.date(now.year - 1, 12, 31)
this_year_start = datetime.date(now.year, 1, 1)
this_month_start = datetime.date(now.year, now.month, 1)
for case in cases:
cases_list.append(case._asdict())
if now - datetime.timedelta(days=7) <= case.open_date <= now:
cases_stats['cases_rolling_week'] += 1
if this_month_start <= case.open_date <= now:
cases_stats['cases_current_month'] += 1
if this_year_start <= case.open_date <= now:
cases_stats['cases_current_year'] += 1
if last_month_start < case.open_date < last_month_end:
cases_stats['cases_last_month'] += 1
if last_year_start <= case.open_date <= last_year_end:
cases_stats['cases_last_year'] += 1
if case.close_date is None:
cases_stats['open_cases'] += 1
if cases_stats['cases_last_year'] == 0:
st = 1
et = cases_stats['cases_current_year'] + 1
else:
st = cases_stats['cases_last_year']
et = cases_stats['cases_current_year']
cases_stats['ratio_year'] = ((et - st)/(st)) * 100
if cases_stats['cases_last_month'] == 0:
st = 1
et = cases_stats['cases_current_month'] + 1
else:
st = cases_stats['cases_last_month']
et = cases_stats['cases_current_month']
cases_stats['ratio_month'] = ((et - st)/(st)) * 100
if (case.close_date is not None) and (case.open_date is not None):
cases_stats['average_case_duration'] += (case.close_date - case.open_date).days
if cases_stats['cases_total'] > 0 and cases_stats['open_cases'] > 0 and cases_stats['average_case_duration'] > 0:
cases_stats['average_case_duration'] = cases_stats['average_case_duration'] / (cases_stats['cases_total'] - cases_stats['open_cases'])
cases = {
'cases': cases_list,
'stats': cases_stats
}
return response_success(data=cases)
@manage_customers_blueprint.route('/manage/customers/update/<int:cur_id>/modal', methods=['GET'])
@ac_requires(Permissions.customers_read, no_cid_required=True)
def view_customer_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('manage_customers.manage_customers', cid=caseid))
form = AddCustomerForm()
customer = get_client(cur_id)
if not customer:
return response_error("Invalid Customer ID")
form.customer_name.render_kw = {'value': customer.name}
form.customer_description.data = customer.description
form.customer_sla.data = customer.sla
return render_template("modal_add_customer.html", form=form, customer=customer,
attributes=customer.custom_attributes)
@manage_customers_blueprint.route('/manage/customers/update/<int:cur_id>', methods=['POST'])
@ac_api_requires(Permissions.customers_write, no_cid_required=True)
def view_customers(cur_id, caseid):
if not request.is_json:
return response_error("Invalid request")
try:
client = update_client(cur_id, request.json)
except ElementNotFoundException:
return response_error('Invalid Customer ID')
except ValidationError as e:
return response_error("", data=e.messages)
except Exception as e:
print(traceback.format_exc())
return response_error(f'An error occurred during Customer update. {e}')
client_schema = CustomerSchema()
return response_success("Customer updated", client_schema.dump(client))
@manage_customers_blueprint.route('/manage/customers/add/modal', methods=['GET'])
@ac_requires(Permissions.customers_read, no_cid_required=True)
def add_customers_modal(caseid, url_redir):
if url_redir:
return redirect(url_for('manage_customers.manage_customers', cid=caseid))
form = AddCustomerForm()
attributes = get_default_custom_attributes('client')
return render_template("modal_add_customer.html", form=form, customer=None, attributes=attributes)
@manage_customers_blueprint.route('/manage/customers/add', methods=['POST'])
@ac_api_requires(Permissions.customers_write, no_cid_required=True)
def add_customers(caseid):
if not request.is_json:
return response_error("Invalid request")
try:
client = create_client(request.json)
except ValidationError as e:
return response_error(msg='Error adding customer', data=e.messages, status=400)
except Exception as e:
print(traceback.format_exc())
return response_error(f'An error occurred during customer addition. {e}')
track_activity(f"Added customer {client.name}", caseid=caseid, ctx_less=True)
# Return the customer
client_schema = CustomerSchema()
return response_success("Added successfully", data=client_schema.dump(client))
@manage_customers_blueprint.route('/manage/customers/delete/<int:cur_id>', methods=['POST'])
@ac_api_requires(Permissions.customers_write, no_cid_required=True)
def delete_customers(cur_id, caseid):
try:
delete_client(cur_id)
except ElementNotFoundException:
return response_error('Invalid Customer ID')
except ElementInUseException:
return response_error('Cannot delete a referenced customer')
except Exception:
return response_error('An error occurred during customer deletion')
track_activity("Deleted Customer with ID {asset_id}".format(asset_id=cur_id), caseid=caseid, ctx_less=True)
return response_success("Deleted successfully")
@manage_customers_blueprint.route('/manage/customers/<int:cur_id>/contacts/<int:contact_id>/delete', methods=['POST'])
@ac_api_requires(Permissions.customers_write, no_cid_required=True)
def delete_contact_route(cur_id, contact_id, caseid):
try:
delete_contact(contact_id)
except ElementNotFoundException:
return response_error('Invalid contact ID')
except ElementInUseException:
return response_error('Cannot delete a referenced contact')
except Exception:
return response_error('An error occurred during contact deletion')
track_activity("Deleted Customer with ID {contact_id}".format(contact_id=cur_id), caseid=caseid, ctx_less=True)
return response_success("Deleted successfully")

View File

@@ -0,0 +1,87 @@
#!/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.
from flask import Blueprint, request
from app.datamgmt.manage.manage_case_objs import search_event_category_by_name
from app.models.models import EventCategory
from app.schema.marshables import EventCategorySchema
from app.util import api_login_required, ac_api_requires
from app.util import response_error
from app.util import response_success
manage_event_cat_blueprint = Blueprint('manage_event_cat',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@manage_event_cat_blueprint.route('/manage/event-categories/list', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def list_event_categories(caseid):
lcat= EventCategory.query.with_entities(
EventCategory.id,
EventCategory.name
).all()
data = [row._asdict() for row in lcat]
return response_success("", data=data)
@manage_event_cat_blueprint.route('/manage/event-categories/<int:cur_id>', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def get_event_category(cur_id, caseid):
lcat = EventCategory.query.with_entities(
EventCategory.id,
EventCategory.name
).filter(
EventCategory.id == cur_id
).first()
if not lcat:
return response_error(f"Event category ID {cur_id} not found")
data = lcat._asdict()
return response_success("", data=data)
@manage_event_cat_blueprint.route('/manage/event-categories/search', methods=['POST'])
@ac_api_requires(no_cid_required=True)
def search_event_category(caseid):
if not request.is_json:
return response_error("Invalid request")
event_category = request.json.get('event_category')
if event_category is None:
return response_error("Invalid category. Got None")
exact_match = request.json.get('exact_match', False)
# Search for event category with a name that contains the specified search term
event_category = search_event_category_by_name(event_category, exact_match=exact_match)
if not event_category:
return response_error("No category found")
# Serialize the event category and return them in a JSON response
schema = EventCategorySchema(many=True)
return response_success("", data=schema.dump(event_category))

View File

@@ -0,0 +1,383 @@
#!/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.
import traceback
import marshmallow
from flask import Blueprint
from flask import render_template
from flask import request
from flask import url_for
from flask_login import current_user
from werkzeug.utils import redirect
from app import db, app
from app.datamgmt.manage.manage_cases_db import list_cases_dict
from app.datamgmt.manage.manage_groups_db import add_all_cases_access_to_group
from app.datamgmt.manage.manage_groups_db import add_case_access_to_group
from app.datamgmt.manage.manage_groups_db import delete_group
from app.datamgmt.manage.manage_groups_db import get_group
from app.datamgmt.manage.manage_groups_db import get_group_details
from app.datamgmt.manage.manage_groups_db import get_group_with_members
from app.datamgmt.manage.manage_groups_db import get_groups_list_hr_perms
from app.datamgmt.manage.manage_groups_db import remove_cases_access_from_group
from app.datamgmt.manage.manage_groups_db import remove_user_from_group
from app.datamgmt.manage.manage_groups_db import update_group_members
from app.datamgmt.manage.manage_users_db import get_user
from app.datamgmt.manage.manage_users_db import get_users_list_restricted
from app.forms import AddGroupForm
from app.iris_engine.access_control.utils import ac_get_all_access_level, ac_ldp_group_removal, ac_flag_match_mask, \
ac_ldp_group_update
from app.iris_engine.access_control.utils import ac_get_all_permissions
from app.iris_engine.access_control.utils import ac_recompute_effective_ac_from_users_list
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import Permissions
from app.schema.marshables import AuthorizationGroupSchema
from app.util import ac_api_requires
from app.util import ac_api_return_access_denied
from app.util import ac_requires
from app.util import response_error
from app.util import response_success
from app.iris_engine.demo_builder import protect_demo_mode_group
manage_groups_blueprint = Blueprint(
'manage_groups',
__name__,
template_folder='templates/access_control'
)
log = app.logger
@manage_groups_blueprint.route('/manage/groups/list', methods=['GET'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def manage_groups_index(caseid):
groups = get_groups_list_hr_perms()
return response_success('', data=groups)
@manage_groups_blueprint.route('/manage/groups/<int:cur_id>/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def manage_groups_view_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('manage_groups.manage_groups_index', cid=caseid))
form = AddGroupForm()
group = get_group_details(cur_id)
if not group:
return response_error("Invalid group ID")
all_perms = ac_get_all_permissions()
form.group_name.render_kw = {'value': group.group_name}
form.group_description.render_kw = {'value': group.group_description}
return render_template("modal_add_group.html", form=form, group=group, all_perms=all_perms)
@manage_groups_blueprint.route('/manage/groups/add/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def manage_groups_add_modal(caseid, url_redir):
if url_redir:
return redirect(url_for('manage_groups.manage_groups_index', cid=caseid))
form = AddGroupForm()
all_perms = ac_get_all_permissions()
return render_template("modal_add_group.html", form=form, group=None, all_perms=all_perms)
@manage_groups_blueprint.route('/manage/groups/add', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def manage_groups_add(caseid):
if not request.is_json:
return response_error("Invalid request, expecting JSON")
data = request.get_json()
if not data:
return response_error("Invalid request, expecting JSON")
ags = AuthorizationGroupSchema()
try:
ags_c = ags.load(data)
ags.verify_unique(data)
db.session.add(ags_c)
db.session.commit()
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
track_activity(message=f"added group {ags_c.group_name}", caseid=caseid, ctx_less=True)
return response_success('', data=ags.dump(ags_c))
@manage_groups_blueprint.route('/manage/groups/update/<int:cur_id>', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def manage_groups_update(cur_id, caseid):
if not request.is_json:
return response_error("Invalid request, expecting JSON")
data = request.get_json()
if not data:
return response_error("Invalid request, expecting JSON")
group = get_group(cur_id)
if not group:
return response_error("Invalid group ID")
if protect_demo_mode_group(group):
return ac_api_return_access_denied(caseid=caseid)
ags = AuthorizationGroupSchema()
try:
data['group_id'] = cur_id
ags_c = ags.load(data, instance=group, partial=True)
if not ac_flag_match_mask(data['group_permissions'],
Permissions.server_administrator.value):
if ac_ldp_group_update(current_user.id):
db.session.rollback()
return response_error(msg="That might not be a good idea Dave",
data="Update the group permissions will lock you out")
db.session.commit()
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
return response_success('', data=ags.dump(ags_c))
@manage_groups_blueprint.route('/manage/groups/delete/<int:cur_id>', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def manage_groups_delete(cur_id, caseid):
group = get_group(cur_id)
if not group:
return response_error("Invalid group ID")
if protect_demo_mode_group(group):
return ac_api_return_access_denied(caseid=caseid)
if ac_ldp_group_removal(current_user.id, group_id=group.group_id):
return response_error("I can't let you do that Dave", data="Removing this group will lock you out")
delete_group(group)
return response_success('Group deleted')
@manage_groups_blueprint.route('/manage/groups/<int:cur_id>', methods=['GET'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def manage_groups_view(cur_id, caseid):
group = get_group_details(cur_id)
if not group:
return response_error("Invalid group ID")
ags = AuthorizationGroupSchema()
return response_success('', data=ags.dump(group))
@manage_groups_blueprint.route('/manage/groups/<int:cur_id>/members/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def manage_groups_members_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('manage_groups_blueprint.manage_groups_index', cid=caseid))
group = get_group_with_members(cur_id)
if not group:
return response_error("Invalid group ID")
users = get_users_list_restricted()
return render_template("modal_add_group_members.html", group=group, users=users)
@manage_groups_blueprint.route('/manage/groups/<int:cur_id>/members/update', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def manage_groups_members_update(cur_id, caseid):
group = get_group_with_members(cur_id)
if not group:
return response_error("Invalid group ID")
if protect_demo_mode_group(group):
return ac_api_return_access_denied(caseid=caseid)
if not request.is_json:
return response_error("Invalid request, expecting JSON")
data = request.get_json()
if not data:
return response_error("Invalid request, expecting JSON")
if not isinstance(data.get('group_members'), list):
return response_error("Expecting a list of IDs")
update_group_members(group, data.get('group_members'))
group = get_group_with_members(cur_id)
return response_success('', data=group)
@manage_groups_blueprint.route('/manage/groups/<int:cur_id>/members/delete/<int:cur_id_2>', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def manage_groups_members_delete(cur_id, cur_id_2, caseid):
group = get_group_with_members(cur_id)
if not group:
return response_error("Invalid group ID")
if protect_demo_mode_group(group):
return ac_api_return_access_denied(caseid=caseid)
user = get_user(cur_id_2)
if not user:
return response_error("Invalid user ID")
if ac_ldp_group_removal(user_id=user.id, group_id=group.group_id):
return response_error('I cannot let you do that Dave', data="Removing you from the group will make you "
"loose your access rights")
remove_user_from_group(group, user)
group = get_group_with_members(cur_id)
return response_success('Member deleted from group', data=group)
@manage_groups_blueprint.route('/manage/groups/<int:cur_id>/cases-access/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def manage_groups_cac_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('manage_groups.manage_groups_index', cid=caseid))
group = get_group_details(cur_id)
if not group:
return response_error("Invalid group ID")
cases_list = list_cases_dict(current_user.id)
group_cases_access = [case.get('case_id') for case in group.group_cases_access]
outer_cases_list = []
for case in cases_list:
if case.get('case_id') not in group_cases_access:
outer_cases_list.append({
"case_id": case.get('case_id'),
"case_name": case.get('case_name')
})
access_levels = ac_get_all_access_level()
return render_template("modal_add_group_cac.html", group=group, outer_cases=outer_cases_list,
access_levels=access_levels)
@manage_groups_blueprint.route('/manage/groups/<int:cur_id>/cases-access/update', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def manage_groups_cac_add_case(cur_id, caseid):
if not request.is_json:
return response_error("Invalid request, expecting JSON")
data = request.get_json()
if not data:
return response_error("Invalid request, expecting JSON")
group = get_group_with_members(cur_id)
if not group:
return response_error("Invalid group ID")
if protect_demo_mode_group(group):
return ac_api_return_access_denied(caseid=caseid)
if not isinstance(data.get('access_level'), int):
try:
data['access_level'] = int(data.get('access_level'))
except:
return response_error("Expecting access_level as int")
if not isinstance(data.get('cases_list'), list) and data.get('auto_follow_cases') is False:
return response_error("Expecting cases_list as list")
if data.get('auto_follow_cases') is True:
group, logs = add_all_cases_access_to_group(group, data.get('access_level'))
group.group_auto_follow = True
group.group_auto_follow_access_level = data.get('access_level')
db.session.commit()
else:
group, logs = add_case_access_to_group(group, data.get('cases_list'), data.get('access_level'))
group.group_auto_follow = False
db.session.commit()
if not group:
return response_error(msg=logs)
group = get_group_details(cur_id)
ac_recompute_effective_ac_from_users_list(group.group_members)
return response_success(data=group)
@manage_groups_blueprint.route('/manage/groups/<int:cur_id>/cases-access/delete', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def manage_groups_cac_delete_case(cur_id, caseid):
group = get_group_with_members(cur_id)
if not group:
return response_error("Invalid group ID")
if protect_demo_mode_group(group):
return ac_api_return_access_denied(caseid=caseid)
if not request.is_json:
return response_error("Invalid request")
data = request.get_json()
if not data:
return response_error("Invalid request")
if not isinstance(data.get('cases'), list):
return response_error("Expecting cases as list")
try:
success, logs = remove_cases_access_from_group(group.group_id, data.get('cases'))
db.session.commit()
except Exception as e:
log.error("Error while removing cases access from group: {}".format(e))
log.error(traceback.format_exc())
return response_error(msg=str(e))
if success:
ac_recompute_effective_ac_from_users_list(group.group_members)
return response_success(msg="Cases access removed from group")
return response_error(msg=logs)

View File

@@ -0,0 +1,199 @@
#!/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.
import marshmallow
from flask import Blueprint
from flask import render_template
from flask import request
from flask import url_for
from werkzeug.utils import redirect
from app import db
from app.datamgmt.case.case_iocs_db import get_ioc_types_list
from app.datamgmt.manage.manage_case_objs import search_ioc_type_by_name
from app.forms import AddIocTypeForm
from app.iris_engine.utils.tracker import track_activity
from app.models import Ioc
from app.models import IocType
from app.models.authorization import Permissions
from app.schema.marshables import IocTypeSchema
from app.util import ac_api_requires
from app.util import ac_requires
from app.util import response_error
from app.util import response_success
manage_ioc_type_blueprint = Blueprint('manage_ioc_types',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@manage_ioc_type_blueprint.route('/manage/ioc-types/list', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def list_ioc_types(caseid):
lstatus = get_ioc_types_list()
return response_success("", data=lstatus)
@manage_ioc_type_blueprint.route('/manage/ioc-types/<int:cur_id>', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def get_ioc_type(cur_id, caseid):
ioc_type = IocType.query.filter(IocType.type_id == cur_id).first()
if not ioc_type:
return response_error("Invalid ioc type ID {type_id}".format(type_id=cur_id))
return response_success("", data=ioc_type)
@manage_ioc_type_blueprint.route('/manage/ioc-types/update/<int:cur_id>/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def view_ioc_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('manage_ioc_types.view_ioc_modal', cid=caseid))
form = AddIocTypeForm()
ioct = IocType.query.filter(IocType.type_id == cur_id).first()
if not ioct:
return response_error("Invalid asset type ID")
form.type_name.render_kw = {'value': ioct.type_name}
form.type_description.render_kw = {'value': ioct.type_description}
form.type_taxonomy.data = ioct.type_taxonomy
form.type_validation_regex.data = ioct.type_validation_regex
form.type_validation_expect.data = ioct.type_validation_expect
return render_template("modal_add_ioc_type.html", form=form, ioc_type=ioct)
@manage_ioc_type_blueprint.route('/manage/ioc-types/add/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def add_ioc_modal(caseid, url_redir):
if url_redir:
return redirect(url_for('manage_ioc_types.view_ioc_modal', cid=caseid))
form = AddIocTypeForm()
return render_template("modal_add_ioc_type.html", form=form, ioc_type=None)
@manage_ioc_type_blueprint.route('/manage/ioc-types/add', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def add_ioc_type_api(caseid):
if not request.is_json:
return response_error("Invalid request")
ioct_schema = IocTypeSchema()
try:
ioct_sc = ioct_schema.load(request.get_json())
db.session.add(ioct_sc)
db.session.commit()
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
track_activity("Added ioc type {ioc_type_name}".format(ioc_type_name=ioct_sc.type_name), caseid=caseid, ctx_less=True)
# Return the assets
return response_success("Added successfully", data=ioct_sc)
@manage_ioc_type_blueprint.route('/manage/ioc-types/delete/<int:cur_id>', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def remove_ioc_type(cur_id, caseid):
type_id = IocType.query.filter(
IocType.type_id == cur_id
).first()
is_referenced = Ioc.query.filter(Ioc.ioc_type_id == cur_id).first()
if is_referenced:
return response_error("Cannot delete a referenced ioc type. Please delete any ioc of this type first.")
if type_id:
db.session.delete(type_id)
track_activity("Deleted ioc type ID {type_id}".format(type_id=cur_id), caseid=caseid, ctx_less=True)
return response_success("Deleted ioc type ID {type_id}".format(type_id=cur_id))
track_activity("Attempted to delete ioc type ID {type_id}, but was not found".format(type_id=cur_id),
caseid=caseid, ctx_less=True)
return response_error("Attempted to delete ioc type ID {type_id}, but was not found".format(type_id=cur_id))
@manage_ioc_type_blueprint.route('/manage/ioc-types/update/<int:cur_id>', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def update_ioc(cur_id, caseid):
if not request.is_json:
return response_error("Invalid request")
ioc_type = IocType.query.filter(IocType.type_id == cur_id).first()
if not ioc_type:
return response_error("Invalid ioc type ID {type_id}".format(type_id=cur_id))
ioct_schema = IocTypeSchema()
try:
ioct_sc = ioct_schema.load(request.get_json(), instance=ioc_type)
if ioct_sc:
track_activity("updated ioc type type {}".format(ioct_sc.type_name), caseid=caseid)
return response_success("IOC type updated", ioct_sc)
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
return response_error("Unexpected error server-side. Nothing updated", data=ioc_type)
@manage_ioc_type_blueprint.route('/manage/ioc-types/search', methods=['POST'])
@ac_api_requires(no_cid_required=True)
def search_ioc_type(caseid):
"""Searches for IOC types in the database.
This function searches for IOC types in the database with a name that contains the specified search term.
It returns a JSON response containing the matching IOC types.
Args:
caseid: The ID of the case associated with the request.
Returns:
A JSON response containing the matching IOC types.
"""
if not request.is_json:
return response_error("Invalid request")
ioc_type = request.json.get('ioc_type')
if ioc_type is None:
return response_error("Invalid ioc type. Got None")
exact_match = request.json.get('exact_match', False)
# Search for IOC types with a name that contains the specified search term
ioc_type = search_ioc_type_by_name(ioc_type, exact_match=exact_match)
if not ioc_type:
return response_error("No ioc types found")
# Serialize the IOC types and return them in a JSON response
ioct_schema = IocTypeSchema(many=True)
return response_success("", data=ioct_schema.dump(ioc_type))

View File

@@ -0,0 +1,321 @@
#!/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 logging as log
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_wtf import FlaskForm
from app import app
from app.datamgmt.iris_engine.modules_db import delete_module_from_id, parse_module_parameter
from app.datamgmt.iris_engine.modules_db import get_module_config_from_id
from app.datamgmt.iris_engine.modules_db import get_module_from_id
from app.datamgmt.iris_engine.modules_db import iris_module_disable_by_id
from app.datamgmt.iris_engine.modules_db import iris_module_enable_by_id
from app.datamgmt.iris_engine.modules_db import iris_module_name_from_id
from app.datamgmt.iris_engine.modules_db import iris_module_save_parameter
from app.datamgmt.iris_engine.modules_db import iris_modules_list
from app.datamgmt.iris_engine.modules_db import is_mod_configured
from app.datamgmt.iris_engine.modules_db import module_list_hooks_view
from app.forms import AddModuleForm
from app.forms import UpdateModuleParameterForm
from app.iris_engine.module_handler.module_handler import check_module_health
from app.iris_engine.module_handler.module_handler import instantiate_module_from_name
from app.iris_engine.module_handler.module_handler import iris_update_hooks
from app.iris_engine.module_handler.module_handler import register_module
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import Permissions
from app.util import ac_api_requires
from app.util import ac_requires
from app.util import response_error
from app.util import response_success
from app.schema.marshables import IrisModuleSchema
manage_modules_blueprint = Blueprint(
'manage_module',
__name__,
template_folder='templates'
)
def has_no_empty_params(rule):
defaults = rule.defaults if rule.defaults is not None else ()
arguments = rule.arguments if rule.arguments is not None else ()
return len(defaults) >= len(arguments)
# CONTENT ------------------------------------------------
@manage_modules_blueprint.route("/sitemap")
@ac_requires()
def site_map(caseid, url_redir):
links = []
for rule in app.url_map.iter_rules():
# Filter out rules we can't navigate to in a browser
# and rules that require parameters
if "GET" in rule.methods and has_no_empty_params(rule):
url = url_for(rule.endpoint, **(rule.defaults or {}))
links.append((url, rule.endpoint))
return response_success('', data=links)
@manage_modules_blueprint.route('/manage/modules', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def manage_modules_index(caseid, url_redir):
if url_redir:
return redirect(url_for('manage_module.manage_modules_index', cid=caseid))
form = FlaskForm()
return render_template("manage_modules.html", form=form)
@manage_modules_blueprint.route('/manage/modules/list', methods=['GET'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def manage_modules_list(caseid):
output = iris_modules_list()
return response_success('', data=output)
@manage_modules_blueprint.route('/manage/modules/add', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def add_module(caseid):
if request.json is None:
return response_error('Invalid request')
module_name = request.json.get('module_name')
# Try to import the module
try:
# Try to instantiate the module
log.info(f'Trying to add module {module_name}')
class_, logs = instantiate_module_from_name(module_name)
if not class_:
return response_error(f"Cannot import module. {logs}")
# Check the health of the module
is_ready, logs = check_module_health(class_)
if not is_ready:
return response_error("Cannot import module. Health check didn't pass. Please check logs below", data=logs)
# Registers into Iris DB for further calls
module, message = register_module(module_name)
if module is None:
track_activity(f"addition of IRIS module {module_name} was attempted and failed",
caseid=caseid, ctx_less=True)
return response_error(f'Unable to register module: {message}')
track_activity(f"IRIS module {module_name} was added", caseid=caseid, ctx_less=True)
module_schema = IrisModuleSchema()
return response_success(message, data=module_schema.dump(module))
except Exception as e:
traceback.print_exc()
return response_error(e.__str__())
@manage_modules_blueprint.route('/manage/modules/add/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def add_module_modal(caseid, url_redir):
if url_redir:
return redirect(url_for('manage_modules.add_module', cid=caseid))
module = None
form = AddModuleForm()
return render_template("modal_add_module.html", form=form, module=module)
@manage_modules_blueprint.route('/manage/modules/get-parameter/<param_name>', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def getmodule_param(param_name, caseid, url_redir):
if url_redir:
return redirect(url_for('manage_modules.add_module', cid=caseid))
form = UpdateModuleParameterForm()
mod_config, mod_id, mod_name, _, parameter = parse_module_parameter(param_name)
if mod_config is None:
return response_error('Invalid parameter')
return render_template("modal_update_parameter.html", parameter=parameter, mod_name=mod_name, mod_id=mod_id,
form=form)
@manage_modules_blueprint.route('/manage/modules/set-parameter/<param_name>', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def update_module_param(param_name, caseid):
if request.json is None:
return response_error('Invalid request')
mod_config, mod_id, mod_name, mod_iname, parameter = parse_module_parameter(param_name)
if mod_config is None:
return response_error('Invalid parameter')
parameter_value = request.json.get('parameter_value')
if iris_module_save_parameter(mod_id, mod_config, parameter['param_name'], parameter_value):
track_activity(f"parameter {parameter['param_name']} of mod ({mod_name}) #{mod_id} was updated",
caseid=caseid, ctx_less=True)
success, logs = iris_update_hooks(mod_iname, mod_id)
if not success:
return response_error("Unable to update hooks", data=logs)
return response_success("Saved", logs)
return response_error('Malformed request', status=400)
@manage_modules_blueprint.route('/manage/modules/update/<int:mod_id>/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def view_module(mod_id, caseid, url_redir):
if url_redir:
return redirect(url_for('manage_modules.view_module', cid=caseid, mod_id=mod_id))
form = AddModuleForm()
if mod_id:
module = get_module_from_id(mod_id)
config = module.module_config
is_configured, missing_params = is_mod_configured(config)
return render_template("modal_module_info.html", form=form, data=module,
config=config, is_configured=is_configured, missing_params=missing_params)
return response_error('Malformed request', status=400)
@manage_modules_blueprint.route('/manage/modules/enable/<int:mod_id>', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def enable_module(mod_id, caseid):
module_name = iris_module_name_from_id(mod_id)
if module_name is None:
return response_error('Invalid module ID', status=400)
if not iris_module_enable_by_id(mod_id):
return response_error('Unable to enable module')
success, logs = iris_update_hooks(module_name, mod_id)
if not success:
return response_error("Unable to update hooks when enabling module", data=logs)
track_activity(f"IRIS module ({module_name}) #{mod_id} enabled",
caseid=caseid, ctx_less=True)
return response_success('Module enabled', data=logs)
@manage_modules_blueprint.route('/manage/modules/disable/<int:module_id>', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def disable_module(module_id, caseid):
if iris_module_disable_by_id(module_id):
track_activity(f"IRIS module #{module_id} disabled",
caseid=caseid, ctx_less=True)
return response_success('Module disabled')
return response_error('Unable to disable module')
@manage_modules_blueprint.route('/manage/modules/remove/<int:module_id>', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def view_delete_module(module_id, caseid):
try:
delete_module_from_id(module_id=module_id)
track_activity(f"IRIS module #{module_id} deleted",
caseid=caseid, ctx_less=True)
return response_success("Deleted")
except Exception as e:
log.error(e.__str__())
return response_error(f"Cannot delete module. Error {e.__str__()}")
@manage_modules_blueprint.route('/manage/modules/export-config/<int:module_id>', methods=['GET'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def export_mod_config(module_id, caseid):
mod_config, mod_name, _ = get_module_config_from_id(module_id)
if mod_name:
data = {
"module_name": mod_name,
"module_configuration": mod_config
}
return response_success(data=data)
return response_error(f"Module ID {module_id} not found")
@manage_modules_blueprint.route('/manage/modules/import-config/<int:module_id>', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def import_mod_config(module_id, caseid):
mod_config, mod_name, _ = get_module_config_from_id(module_id)
logs = []
parameters_data = request.get_json().get('module_configuration')
if type(parameters_data) is not list:
try:
parameters = json.loads(parameters_data)
except Exception as e:
return response_error('Invalid data', data="Not a JSON file")
else:
parameters = parameters_data
for param in parameters:
param_name = param.get('param_name')
parameter_value = param.get('value')
if not iris_module_save_parameter(module_id, mod_config, param_name, parameter_value):
logs.append(f'Unable to save parameter {param_name}')
track_activity(f"parameters of mod #{module_id} were updated from config file",
caseid=caseid, ctx_less=True)
if len(logs) == 0:
msg = "Successfully imported data."
else:
msg = "Configuration is partially imported, we got errors with the followings:\n- " + "\n- ".join(logs)
return response_success(msg)
@manage_modules_blueprint.route('/manage/modules/hooks/list', methods=['GET'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def view_modules_hook(caseid):
output = module_list_hooks_view()
data = [item._asdict() for item in output]
return response_success('', data=data)

View File

@@ -0,0 +1,47 @@
#!/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 ------------------------------------------------
from flask import Blueprint
from flask import redirect
from flask import render_template
from flask import url_for
from app.forms import AddAssetForm
from app.models.authorization import Permissions
from app.util import ac_requires
manage_objects_blueprint = Blueprint('manage_objects',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@manage_objects_blueprint.route('/manage/objects')
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def manage_objects(caseid, url_redir):
if url_redir:
return redirect(url_for('manage_objects.manage_objects', cid=caseid))
form = AddAssetForm()
# Return default page of case management
return render_template('manage_objects.html', form=form)

View File

@@ -0,0 +1,86 @@
#!/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 flask import Blueprint, Response, request
from app.datamgmt.manage.manage_common import get_severity_by_id, get_severities_list, search_severity_by_name
from app.schema.marshables import SeveritySchema
from app.util import ac_api_requires, response_error
from app.util import response_success
manage_severities_blueprint = Blueprint('manage_severities',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@manage_severities_blueprint.route('/manage/severities/list', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def list_severities(caseid: int) -> Response:
"""
Get the list of severities
Args:
caseid (int): case id
Returns:
Flask Response object
"""
l_cl = get_severities_list()
schema = SeveritySchema()
return response_success("", data=schema.dump(l_cl, many=True))
@manage_severities_blueprint.route('/manage/severities/<int:severity_id>', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def get_case_alert_status(severity_id: int, caseid: int) -> Response:
"""
Get the alert status
Args:
severity_id (int): severity id
caseid (int): case id
"""
cl = get_severity_by_id(severity_id)
schema = SeveritySchema()
return response_success("", data=schema.dump(cl))
@manage_severities_blueprint.route('/manage/severities/search', methods=['POST'])
@ac_api_requires(no_cid_required=True)
def search_analysis_status(caseid):
if not request.is_json:
return response_error("Invalid request")
severity_name = request.json.get('severity_name')
if severity_name is None:
return response_error("Invalid severity. Got None")
exact_match = request.json.get('exact_match', False)
# Search for severity with a name that contains the specified search term
severity = search_severity_by_name(severity_name, exact_match=exact_match)
if not severity:
return response_error("No severity found")
# Serialize the severity and return them in a JSON response
schema = SeveritySchema(many=True)
return response_success("", data=schema.dump(severity))

View File

@@ -0,0 +1,141 @@
#!/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 marshmallow
from flask import Blueprint
from flask import render_template
from flask import request
from flask import url_for
from flask_wtf import FlaskForm
from werkzeug.utils import redirect
from app import app
from app import celery
from app import db
from app.datamgmt.manage.manage_srv_settings_db import get_alembic_revision
from app.datamgmt.manage.manage_srv_settings_db import get_srv_settings
from app.iris_engine.backup.backup import backup_iris_db
from app.iris_engine.updater.updater import inner_init_server_update
from app.iris_engine.updater.updater import is_updates_available
from app.iris_engine.updater.updater import remove_periodic_update_checks
from app.iris_engine.updater.updater import setup_periodic_update_checks
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import Permissions
from app.schema.marshables import ServerSettingsSchema
from app.util import ac_api_requires
from app.util import ac_requires
from app.util import response_error
from app.util import response_success
manage_srv_settings_blueprint = Blueprint(
'manage_srv_settings_blueprint',
__name__,
template_folder='templates'
)
@manage_srv_settings_blueprint.route('/manage/server/make-update', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def manage_update(caseid, url_redir):
if url_redir:
return redirect(url_for('manage_srv_settings_blueprint.manage_settings', cid=caseid))
# Return default page of case management
return render_template('manage_make_update.html')
@manage_srv_settings_blueprint.route('/manage/server/backups/make-db', methods=['GET'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def manage_make_db_backup(caseid):
has_error, logs = backup_iris_db()
if has_error:
rep = response_error('Backup failed', data=logs)
else:
rep = response_success('Backup done', data=logs)
return rep
@manage_srv_settings_blueprint.route('/manage/server/check-updates/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def manage_check_updates_modal(caseid, url_redir):
if url_redir:
return redirect(url_for('manage_srv_settings_blueprint.manage_settings', cid=caseid))
has_updates, updates_content, _ = is_updates_available()
# Return default page of case management
return render_template('modal_server_updates.html', has_updates=has_updates, updates_content=updates_content)
@manage_srv_settings_blueprint.route('/manage/settings', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def manage_settings(caseid, url_redir):
if url_redir:
return redirect(url_for('manage_srv_settings_blueprint.manage_settings', cid=caseid))
form = FlaskForm()
server_settings = get_srv_settings()
versions = {
"iris_current": app.config.get('IRIS_VERSION'),
"api_min": app.config.get('API_MIN_VERSION'),
"api_current": app.config.get('API_MAX_VERSION'),
"interface_min": app.config.get('MODULES_INTERFACE_MIN_VERSION'),
"interface_current": app.config.get('MODULES_INTERFACE_MAX_VERSION'),
"db_revision": get_alembic_revision()
}
# Return default page of case management
return render_template('manage_srv_settings.html', form=form, settings=server_settings, versions=versions)
@manage_srv_settings_blueprint.route('/manage/settings/update', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def manage_update_settings(caseid):
if not request.is_json:
return response_error('Invalid request')
srv_settings_schema = ServerSettingsSchema()
server_settings = get_srv_settings()
original_update_check = server_settings.enable_updates_check
try:
srv_settings_sc = srv_settings_schema.load(request.get_json(), instance=server_settings)
db.session.commit()
if original_update_check != srv_settings_sc.enable_updates_check:
if srv_settings_sc.enable_updates_check:
setup_periodic_update_checks(celery)
else:
remove_periodic_update_checks()
if srv_settings_sc:
track_activity("Server settings updated", caseid=caseid)
return response_success("Server settings updated", srv_settings_sc)
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)

View File

@@ -0,0 +1,65 @@
#!/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.
from flask import Blueprint
from app.models.models import TaskStatus
from app.util import api_login_required, ac_api_requires
from app.util import response_error
from app.util import response_success
manage_task_status_blueprint = Blueprint('manage_task_status',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@manage_task_status_blueprint.route('/manage/task-status/list', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def list_task_status(caseid):
lstatus = TaskStatus.query.with_entities(
TaskStatus.id,
TaskStatus.status_name,
TaskStatus.status_bscolor,
TaskStatus.status_description
).all()
data = [row._asdict() for row in lstatus]
return response_success("", data=data)
# CONTENT ------------------------------------------------
@manage_task_status_blueprint.route('/manage/task-status/<int:cur_id>', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def view_task_status(cur_id, caseid):
lstatus = TaskStatus.query.with_entities(
TaskStatus.id,
TaskStatus.status_name,
TaskStatus.status_bscolor,
TaskStatus.status_description
).filter(
TaskStatus.id == cur_id
).first()
if not lstatus:
return response_error(f"Task status ID #{cur_id} not found")
return response_success(data=lstatus._asdict())

View File

@@ -0,0 +1,213 @@
#!/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 os
import random
import string
from datetime import datetime
from flask import Blueprint
from flask import flash
from flask import redirect
from flask import render_template
from flask import request
from flask import send_file
from flask import url_for
from flask_login import current_user
from app import app
from app import db
from app.forms import AddReportTemplateForm
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import Permissions
from app.models.authorization import User
from app.models.models import CaseTemplateReport
from app.models.models import Languages
from app.models.models import ReportType
from app.util import ac_api_requires
from app.util import ac_requires
from app.util import response_error
from app.util import response_success
manage_templates_blueprint = Blueprint(
'manage_templates',
__name__,
template_folder='templates'
)
ALLOWED_EXTENSIONS = {'md', 'html', 'doc', 'docx'}
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def get_random_string(length):
letters = string.ascii_lowercase
result_str = ''.join(random.choice(letters) for i in range(length))
return result_str
# CONTENT ------------------------------------------------
@manage_templates_blueprint.route('/manage/templates')
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def manage_report_templates(caseid, url_redir):
if url_redir:
redirect(url_for('manage_templates.manage_report_templates', cid=caseid))
form = AddReportTemplateForm()
form.report_language.choices = [(c.id, c.name.capitalize()) for c in Languages.query.all()]
# Return default page of case management
return render_template('manage_templates.html', form=form)
@manage_templates_blueprint.route('/manage/templates/list')
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def report_templates_list(caseid):
# Get all templates
templates = CaseTemplateReport.query.with_entities(
CaseTemplateReport.name,
CaseTemplateReport.description,
CaseTemplateReport.naming_format,
CaseTemplateReport.date_created,
User.name.label('created_by'),
Languages.code,
ReportType.name.label('type_name'),
CaseTemplateReport.id
).join(
CaseTemplateReport.created_by_user,
CaseTemplateReport.language,
CaseTemplateReport.report_type
).all()
data = [row._asdict() for row in templates]
# Return the assets
return response_success("", data=data)
@manage_templates_blueprint.route('/manage/templates/add/modal', methods=['GET'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def add_template_modal(caseid):
report_template = CaseTemplateReport()
form = AddReportTemplateForm()
form.report_language.choices = [(c.id, c.name.capitalize()) for c in Languages.query.all()]
form.report_type.choices = [(c.id, c.name) for c in ReportType.query.all()]
return render_template("modal_add_report_template.html", form=form, report_template=report_template)
@manage_templates_blueprint.route('/manage/templates/add', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def add_template(caseid):
report_template = CaseTemplateReport()
report_template.name = request.form.get('report_name', '', type=str)
report_template.description = request.form.get('report_description', '', type=str)
report_template.naming_format = request.form.get('report_name_format', '', type=str)
report_template.language_id = request.form.get('report_language', '', type=int)
report_template.report_type_id = request.form.get('report_type', '', type=int)
report_template.created_by_user_id = current_user.id
report_template.date_created = datetime.utcnow()
template_file = request.files['file']
if template_file.filename == '':
flash('No selected file')
return redirect(request.url)
if template_file and allowed_file(template_file.filename):
_, extension = os.path.splitext(template_file.filename)
filename = get_random_string(18) + extension
try:
template_file.save(os.path.join(app.config['TEMPLATES_PATH'], filename))
except Exception as e:
return response_error(f"Unable to add template. {e}")
report_template.internal_reference = filename
db.session.add(report_template)
db.session.commit()
track_activity(f"report template '{report_template.name}' added", caseid=caseid, ctx_less=True)
ret = {
"report_id": report_template.id,
"report_name": report_template.name,
"report_description": report_template.description,
"report_language_id": report_template.language_id,
"report_name_format": report_template.naming_format,
"report_type_id": report_template.report_type_id
}
return response_success("Added successfully", data=ret)
return response_error("File is invalid")
@manage_templates_blueprint.route('/manage/templates/download/<report_id>', methods=['GET'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def download_template(report_id, caseid):
if report_id != 0:
report_template = CaseTemplateReport.query.filter(CaseTemplateReport.id == report_id).first()
fpath = os.path.join(app.config['TEMPLATES_PATH'], report_template.internal_reference)
_, extension = os.path.splitext(report_template.internal_reference)
resp = send_file(fpath, as_attachment=True, download_name=f"{report_template.name}.{extension}")
return resp
else:
return response_error("Unable to download file")
@manage_templates_blueprint.route('/manage/templates/delete/<report_id>', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def delete_template(report_id, caseid):
error = None
report_template = CaseTemplateReport.query.filter(CaseTemplateReport.id == report_id).first()
if report_template is None:
return response_error('Template not found')
report_name = report_template.name
try:
os.unlink(os.path.join(app.config['TEMPLATES_PATH'], report_template.internal_reference))
except Exception as e:
error = f"Template reference will be deleted but there has been some errors. {e}"
finally:
CaseTemplateReport.query.filter(CaseTemplateReport.id == report_id).delete()
db.session.commit()
if error:
return response_error(error)
track_activity(f"report template '{report_name}' deleted", caseid=caseid, ctx_less=True)
return response_success("Deleted successfully", data=error)

View File

@@ -0,0 +1,51 @@
#!/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.
from flask import Blueprint
from app.models import Tlp
from app.util import api_login_required, ac_api_requires
from app.util import response_error
from app.util import response_success
manage_tlp_type_blueprint = Blueprint('manage_tlp_types',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@manage_tlp_type_blueprint.route('/manage/tlp/list', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def list_tlp_types(caseid):
lstatus = Tlp.query.all()
return response_success("", data=lstatus)
@manage_tlp_type_blueprint.route('/manage/tlp/<int:cur_id>', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def get_tlp_type(cur_id, caseid):
tlp_type = Tlp.query.filter(Tlp.tlp_id == cur_id).first()
if not tlp_type:
return response_error("Invalid TLP ID {type_id}".format(type_id=cur_id))
return response_success("", data=tlp_type)

View File

@@ -0,0 +1,489 @@
#!/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.
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 app import app
from app import db
from app.datamgmt.manage.manage_cases_db import list_cases_dict
from app.datamgmt.manage.manage_groups_db import get_groups_list
from app.datamgmt.manage.manage_srv_settings_db import get_srv_settings
from app.datamgmt.manage.manage_users_db import add_case_access_to_user
from app.datamgmt.manage.manage_users_db import create_user
from app.datamgmt.manage.manage_users_db import delete_user
from app.datamgmt.manage.manage_users_db import get_user
from app.datamgmt.manage.manage_users_db import get_user_by_username
from app.datamgmt.manage.manage_users_db import get_user_details
from app.datamgmt.manage.manage_users_db import get_user_effective_permissions
from app.datamgmt.manage.manage_users_db import get_users_list
from app.datamgmt.manage.manage_users_db import get_users_list_restricted
from app.datamgmt.manage.manage_users_db import remove_case_access_from_user
from app.datamgmt.manage.manage_users_db import remove_cases_access_from_user
from app.datamgmt.manage.manage_users_db import update_user
from app.datamgmt.manage.manage_users_db import update_user_groups
from app.forms import AddUserForm
from app.iris_engine.access_control.utils import ac_get_all_access_level
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import Permissions
from app.schema.marshables import UserSchema
from app.util import ac_api_requires
from app.util import ac_api_return_access_denied
from app.util import ac_requires
from app.util import is_authentication_local
from app.util import response_error
from app.util import response_success
from app.iris_engine.demo_builder import protect_demo_mode_user
manage_users_blueprint = Blueprint(
'manage_users',
__name__,
template_folder='templates'
)
log = app.logger
@manage_users_blueprint.route('/manage/users/list', methods=['GET'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def manage_users_list(caseid):
users = get_users_list()
return response_success('', data=users)
@manage_users_blueprint.route('/manage/users/add/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def add_user_modal(caseid, url_redir):
if url_redir:
return redirect(url_for('manage_users.add_user', cid=caseid))
user = None
form = AddUserForm()
server_settings = get_srv_settings()
return render_template("modal_add_user.html", form=form, user=user, server_settings=server_settings)
@manage_users_blueprint.route('/manage/users/add', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def add_user(caseid):
try:
# validate before saving
user_schema = UserSchema()
jsdata = request.get_json()
jsdata['user_id'] = 0
jsdata['active'] = jsdata.get('active', True)
cuser = user_schema.load(jsdata, partial=True)
user = create_user(user_name=cuser.name,
user_login=cuser.user,
user_email=cuser.email,
user_password=cuser.password,
user_active=jsdata.get('active'),
user_is_service_account=cuser.is_service_account)
udata = user_schema.dump(user)
udata['user_api_key'] = user.api_key
del udata['user_password']
if cuser:
track_activity("created user {}".format(user.user), caseid=caseid, ctx_less=True)
return response_success("user created", data=udata)
return response_error("Unable to create user for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@manage_users_blueprint.route('/manage/users/<int:cur_id>', methods=['GET'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def view_user(cur_id, caseid):
user = get_user_details(user_id=cur_id)
if not user:
return response_error("Invalid user ID")
return response_success(data=user)
@manage_users_blueprint.route('/manage/users/<int:cur_id>/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def view_user_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('manage_users.add_user', cid=caseid))
form = AddUserForm()
user = get_user_details(cur_id, include_api_key=True)
if not user:
return response_error("Invalid user ID")
permissions = get_user_effective_permissions(cur_id)
form.user_login.render_kw = {'value': user.get('user_login')}
form.user_name.render_kw = {'value': user.get('user_name')}
form.user_email.render_kw = {'value': user.get('user_email')}
form.user_is_service_account.render_kw = {'checked': user.get('user_is_service_account')}
server_settings = get_srv_settings()
return render_template("modal_add_user.html", form=form, user=user, server_settings=server_settings,
permissions=permissions)
@manage_users_blueprint.route('/manage/users/<int:cur_id>/groups/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def manage_user_group_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('manage_users.add_user', cid=caseid))
user = get_user_details(cur_id)
if not user:
return response_error("Invalid user ID")
groups = get_groups_list()
return render_template("modal_manage_user_groups.html", groups=groups, user=user)
@manage_users_blueprint.route('/manage/users/<int:cur_id>/groups/update', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def manage_user_group_(cur_id, caseid):
if not request.is_json:
return response_error("Invalid request", status=400)
if not request.json.get('groups_membership'):
return response_error("Invalid request", status=400)
if type(request.json.get('groups_membership')) is not list:
return response_error("Expected list of groups ID", status=400)
user = get_user_details(cur_id)
if not user:
return response_error("Invalid user ID")
update_user_groups(user_id=cur_id,
groups=request.json.get('groups_membership'))
track_activity(f"groups membership of user {user.get('user')} updated", caseid=caseid, ctx_less=True)
return response_success("User groups updated", data=user)
@manage_users_blueprint.route('/manage/users/<int:cur_id>/cases-access/modal', methods=['GET'])
@ac_requires(Permissions.server_administrator, no_cid_required=True)
def manage_user_cac_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('manage_users.add_user', cid=caseid))
user = get_user_details(cur_id)
if not user:
return response_error("Invalid user ID")
cases_list = list_cases_dict(current_user.id)
user_cases_access = [case.get('case_id') for case in user.get('user_cases_access')]
outer_cases_list = []
for case in cases_list:
if case.get('case_id') not in user_cases_access:
outer_cases_list.append({
"case_id": case.get('case_id'),
"case_name": case.get('case_name')
})
access_levels = ac_get_all_access_level()
return render_template("modal_add_user_cac.html", user=user, outer_cases=outer_cases_list,
access_levels=access_levels)
@manage_users_blueprint.route('/manage/users/<int:cur_id>/cases-access/update', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def manage_user_cac_add_case(cur_id, caseid):
if not request.is_json:
return response_error("Invalid request, expecting JSON")
data = request.get_json()
if not data:
return response_error("Invalid request, expecting JSON")
user = get_user(cur_id)
if not user:
return response_error("Invalid user ID")
if not isinstance(data.get('access_level'), int):
try:
data['access_level'] = int(data.get('access_level'))
except:
return response_error("Expecting access_level as int")
if not isinstance(data.get('cases_list'), list):
return response_error("Expecting cases_list as list")
user, logs = add_case_access_to_user(user, data.get('cases_list'), data.get('access_level'))
if not user:
return response_error(msg=logs)
track_activity(f"case access level {data.get('access_level')} for case(s) {data.get('cases_list')} "
f"set for user {user.user}", caseid=caseid, ctx_less=True)
group = get_user_details(cur_id)
return response_success(data=group)
@manage_users_blueprint.route('/manage/users/<int:cur_id>/cases-access/delete', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def manage_user_cac_delete_cases(cur_id, caseid):
user = get_user(cur_id)
if not user:
return response_error("Invalid user ID")
if not request.is_json:
return response_error("Invalid request")
data = request.get_json()
if not data:
return response_error("Invalid request")
if not isinstance(data.get('cases'), list):
return response_error("Expecting cases as list")
try:
success, logs = remove_cases_access_from_user(user.id, data.get('cases'))
db.session.commit()
except Exception as e:
log.error("Error while removing cases access from user: {}".format(e))
log.error(traceback.format_exc())
return response_error(msg=str(e))
if success:
track_activity(f"cases access for case(s) {data.get('cases')} deleted for user {user.user}", caseid=caseid,
ctx_less=True)
user = get_user_details(cur_id)
return response_success(msg="User case access updated", data=user)
return response_error(msg=logs)
@manage_users_blueprint.route('/manage/users/<int:cur_id>/case-access/delete', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def manage_user_cac_delete_case(cur_id, caseid):
user = get_user(cur_id)
if not user:
return response_error("Invalid user ID")
if not request.is_json:
return response_error("Invalid request")
data = request.get_json()
if not data:
return response_error("Invalid request")
if not isinstance(data.get('case'), int):
return response_error("Expecting cases as int")
try:
success, logs = remove_case_access_from_user(user.id, data.get('case'))
db.session.commit()
except Exception as e:
log.error("Error while removing cases access from user: {}".format(e))
log.error(traceback.format_exc())
return response_error(msg=str(e))
if success:
track_activity(f"case access for case {data.get('case')} deleted for user {user.user}", caseid=caseid,
ctx_less=True)
user = get_user_details(cur_id)
return response_success(msg="User case access updated", data=user)
return response_error(msg=logs)
@manage_users_blueprint.route('/manage/users/update/<int:cur_id>', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def update_user_api(cur_id, caseid):
try:
user = get_user(cur_id)
if not user:
return response_error("Invalid user ID for this case")
if protect_demo_mode_user(user):
return ac_api_return_access_denied(caseid=caseid)
# validate before saving
user_schema = UserSchema()
jsdata = request.get_json()
jsdata['user_id'] = cur_id
cuser = user_schema.load(jsdata, instance=user, partial=True)
update_user(password=jsdata.get('user_password'),
user=user)
db.session.commit()
if cuser:
track_activity("updated user {}".format(user.user), caseid=caseid, ctx_less=True)
return response_success("User updated", data=user_schema.dump(user))
return response_error("Unable to update user for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@manage_users_blueprint.route('/manage/users/deactivate/<int:cur_id>', methods=['GET'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def deactivate_user_api(cur_id, caseid):
user = get_user(cur_id)
if not user:
return response_error("Invalid user ID for this case")
if protect_demo_mode_user(user):
return ac_api_return_access_denied(caseid=caseid)
if current_user.id == cur_id:
return response_error('We do not recommend deactivating yourself for obvious reasons')
user.active = False
db.session.commit()
user_schema = UserSchema()
track_activity(f"user {user.user} deactivated", caseid=caseid, ctx_less=True)
return response_success("User deactivated", data=user_schema.dump(user))
@manage_users_blueprint.route('/manage/users/activate/<int:cur_id>', methods=['GET'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def activate_user_api(cur_id, caseid):
user = get_user(cur_id)
if not user:
return response_error("Invalid user ID for this case")
if protect_demo_mode_user(user):
return ac_api_return_access_denied(caseid=caseid)
user.active = True
db.session.commit()
user_schema = UserSchema()
track_activity(f"user {user.user} activated", caseid=caseid, ctx_less=True)
return response_success("User activated", data=user_schema.dump(user))
if is_authentication_local():
@manage_users_blueprint.route('/manage/users/delete/<int:cur_id>', methods=['POST'])
@ac_api_requires(Permissions.server_administrator, no_cid_required=True)
def view_delete_user(cur_id, caseid):
try:
user = get_user(cur_id)
if not user:
return response_error("Invalid user ID")
if protect_demo_mode_user(user):
return ac_api_return_access_denied(caseid=caseid)
if user.active is True:
track_activity(message="tried to delete active user ID {}".format(cur_id), caseid=caseid, ctx_less=True)
return response_error("Cannot delete active user")
delete_user(user.id)
track_activity(message="deleted user ID {}".format(cur_id), caseid=caseid, ctx_less=True)
return response_success("Deleted user ID {}".format(cur_id))
except Exception:
db.session.rollback()
track_activity(message="tried to delete active user ID {}".format(cur_id), caseid=caseid, ctx_less=True)
return response_error("Cannot delete active user")
# Unrestricted section - non admin available
@manage_users_blueprint.route('/manage/users/lookup/id/<int:cur_id>', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def exists_user_restricted(cur_id, caseid):
user = get_user(cur_id)
if not user:
return response_error("Invalid user ID")
output = {
"user_login": user.user,
"user_id": user.id,
"user_name": user.name
}
return response_success(data=output)
@manage_users_blueprint.route('/manage/users/lookup/login/<string:login>', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def lookup_name_restricted(login, caseid):
user = get_user_by_username(login)
if not user:
return response_error("Invalid login")
output = {
"user_login": user.user,
"user_id": user.id,
"user_uuid": user.uuid,
"user_name": user.name,
"user_email": user.email,
"user_active": user.active
}
return response_success(data=output)
@manage_users_blueprint.route('/manage/users/restricted/list', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def manage_users_list_restricted(caseid):
users = get_users_list_restricted()
return response_success('', data=users)

View File

@@ -0,0 +1,125 @@
{% extends "layouts/default.html" %}
{% block title %} Access Control {% endblock title %}
{% block stylesheets %}
<link rel="stylesheet" href="/static/assets/css/suggestags.css">
<link rel="stylesheet" href="/static/assets/css/bootstrap-multiselect.min.css">
{% endblock stylesheets %}
{% block content %}
{% if current_user.is_authenticated %}
{{ form.hidden_tag() }}
<div class="page-inner">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<div class="row">
<div class="col col-heading collapsed" href="#collapse_user_mgmt" title="Click to unfold" data-toggle="collapse" role="button" aria-expanded="false" aria-controls="collapse_user_mgmt">
<span class="accicon float-left mr-3"><i class="fas fa-angle-right rotate-icon"></i></span>
<div class="card-title">Users</div>
</div>
<div class="col">
<button type="button" class="btn btn-sm btn-dark float-right ml-2" onclick="refresh_users(true);">Refresh</button>
<a class="btn btn-sm btn-dark float-right ml-2" href="access-control/audit/users?cid={{ session['current_case'].case_id }}">Audit users</a>
<button class="btn btn-sm btn-dark float-right ml-2" onclick="add_user();">Add user</button>
</div>
</div>
</div>
<div class="card-body collapse" id="collapse_user_mgmt">
<div class="table-responsive" id="users_table_wrapper">
<div class="selectgroup">
<span id="table_buttons"></span>
</div>
<table class="table display table table-striped table-hover" width="100%"
cellspacing="0" id="users_table">
<thead>
<tr>
<th>#ID</th>
<th>Name</th>
<th>Login Name</th>
<th>Email</th>
<th>Active</th>
<th>Service Account</th>
</tr>
</thead>
<tfoot>
<tr>
<th>#ID</th>
<th>Name</th>
<th>Login Name</th>
<th>Email</th>
<th>Active</th>
<th>Service Account</th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<div class="row">
<div class="col col-heading collapsed" href="#collapse_groups_mgmt" title="Click to unfold" data-toggle="collapse" role="button" aria-expanded="false" aria-controls="collapse_groups_mgmt">
<span class="accicon float-left mr-3"><i class="fas fa-angle-right rotate-icon"></i></span>
<div class="card-title">Groups</div>
</div>
<div class="col">
<button type="button" class="btn btn-sm btn-dark float-right ml-2" onclick="refresh_groups(true);">Refresh</button>
<button class="btn btn-sm btn-dark float-right" onclick="add_group();">Add group</button>
</div>
</div>
</div>
<div class="card-body collapse" id="collapse_groups_mgmt">
<div class="table-responsive" id="groups_table_wrapper">
<div class="selectgroup">
<span id="groups_table_buttons"></span>
</div>
<table class="table display table table-striped table-hover" width="100%"
cellspacing="0" id="groups_table">
<thead>
<tr>
<th>#ID</th>
<th>Name</th>
<th>Description</th>
<th>Permissions</th>
<th>#Members</th>
</tr>
</thead>
<tfoot>
<tr>
<th>#ID</th>
<th>Name</th>
<th>Description</th>
<th>Permissions</th>
<th>#Members</th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
{% endif %}
</div>
<div class="modal" tabindex="-1" role="dialog" id="modal_access_control" data-backdrop="true">
</div>
<div class="modal bg-shadow-gradient" tabindex="-1" role="dialog" id="modal_ac_additional" data-backdrop="true">
</div>
{% endblock content %}
{% block javascripts %}
<script src="/static/assets/js/plugin/select/bootstrap-multiselect.min.js"></script>
<script src="/static/assets/js/plugin/datatables/dataTables.select.min.js"></script>
<script src="/static/assets/js/plugin/datatables/dataTables.contextualActions.min.js"></script>
<script src="/static/assets/js/iris/manage.users.js"></script>
<script src="/static/assets/js/iris/manage.cases.common.js"></script>
<script src="/static/assets/js/iris/manage.groups.js"></script>
{% endblock javascripts %}

View File

@@ -0,0 +1,58 @@
{% extends "layouts/default.html" %}
{% block title %} Access Control {% endblock title %}
{% block stylesheets %}
<link rel="stylesheet" href="/static/assets/css/suggestags.css">
<link rel="stylesheet" href="/static/assets/css/bootstrap-select.min.css">
<link rel="stylesheet" href="/static/assets/css/select2.css">
{% endblock stylesheets %}
{% block content %}
{{ form.hidden_tag() }}
<div class="page-inner">
<div class="row">
<div class="col-md-12">
<a class="mb-2 ml-1 text-dark" href="/manage/access-control?cid={{ session['current_case'].case_id }}"><i class="fa-solid fa-arrow-left"></i> Back</a>
<div class="card mt-2">
<div class="card-header">
<div class="row">
<div class="col col-heading">
<div class="card-title">Users audit</div>
</div>
</div>
</div>
<div class="card-body" id="">
<div class="row">
<div class="col-12">
<div class="input-group mb-3">
<select id="users_audit_select" name="users_audit_select" class="form-control ml-12"
tabindex="-1" style="width: 50%"></select>
<div class="input-group-append">
<button type="button" class="btn btn-outline-success" id="get_user_audit_btn"
onclick="get_user_audit_page();">Audit</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
</div>
</div>
</div>
</div>
<div id="user_audit_content">
</div>
</div>
</div>
</div>
{% endblock content %}
{% block javascripts %}
<script src="/static/assets/js/plugin/datatables/dataTables.select.min.js"></script>
<script src="/static/assets/js/iris/manage.audit.users.js"></script>
<script src="/static/assets/js/iris/datatablesUtils.js"></script>
{% endblock javascripts %}

View File

@@ -0,0 +1,292 @@
<div class="modal-xl modal-dialog" role="document">
<div class="modal-content" id="modal_access_control_content">
<div class="modal-header">
<h4 class="modal-title mt-2 mr-4">{% if not group.group_id %}Add Group {% else %} Edit {{ group.group_name }} {% endif %}</h4>
<div class="row text-center mr-4">
{% if group.group_id %}
<ul class="nav nav-pills nav-default mr-4" role="tablist">
<li class="nav-item">
<a class="nav-link active show" data-toggle="pill" href="#group_details_tab" role="tab" aria-controls="group_details_tab" aria-selected="false">Info</a>
</li>
<li class="nav-item submenu">
<a class="nav-link" data-toggle="pill" href="#group_members_tab" role="tab" aria-controls="group_members_tab" aria-selected="false">Members</a>
</li>
<li class="nav-item submenu">
<a class="nav-link" data-toggle="pill" href="#group_cac_tab" role="tab" aria-controls="group_cac_tab" aria-selected="false">Cases access</a>
</li>
</ul>
{% endif %}
</div>
<div class="row text-right">
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
</div>
<div class="modal-body">
<div role="tabpanel">
<div class="tab-content">
<div class="tab-pane active" id="group_details_tab">
<div class="container col-md-12" >
<form method="post" action="" id="form_new_group">
<div class="col-md-12 col-lg-12 col-sm-12">
{{ form.hidden_tag() }}
{% if not group.group_id %}
<p class="ml-3"><i class="fa-solid fa-circle-info mr-2"></i>Members can be added once the group is created.</p>
{% endif %}
<div class="form-group">
<label for="group_name" class="mr-4">Group name *
</label>
{{ form.group_name(class='form-control', autocomplete="off") }}
</div>
<div class="form-group mt-3">
<label for="group_description" class="placeholder">Description *</label>
{{ form.group_description(class='form-control', autocomplete="off") }}
</div>
<div class="form-group" data-select2-id="7">
<label>Permissions *</label>
<div class="select2-input ml-12" data-select2-id="6">
<select id="group_permissions" name="group_permissions" class="form-control select2-hidden-accessible ml-12" multiple="" data-select2-id="group_permissions" tabindex="-1" aria-hidden="true" style="width: 100%">
</select>
</div>
</div>
</div>
{% if group.group_id %}
<button type="button" class="btn btn-danger mt-5"
onclick="delete_group('{{ group.group_id }}');">Delete</button>
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
id="submit_new_group">Update</button>
{% else %}
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
id="submit_new_group">Save</button>
{% endif %}
</form>
</div>
</div>
<div class="tab-pane" id="group_members_tab">
<div class="container col-md-12" >
<div class="row d-flex">
<div class="col pull-right">
<button class="btn btn-dark btn-sm pull-right" onclick="refresh_group_members({{ group.group_id }});">
<span class="menu-title">Refresh</span>
</button>
<button class="btn btn-dark btn-sm pull-right mr-2" onclick="add_members_to_group({{ group.group_id }});">
<span class="menu-title">Manage</span>
</button>
</div>
</div>
<div class="row">
<table class="table display table-bordered table-striped table-hover responsive" width="100%" cellspacing="0" id="group_members_table" >
<thead>
<tr>
<th>User ID</th>
<th>User login</th>
<th>User display name</th>
</tr>
</thead>
<tfoot>
<tr>
<th>User ID</th>
<th>User login</th>
<th>User display name</th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="tab-pane" id="group_cac_tab">
<div class="container col-md-12" >
<div class="row d-flex mb-2">
<div class="col">
{% if group.group_auto_follow %}
<b><i class="fa fa-triangle-exclamation text-warning mr-2"></i>This group is set to automatically include all new cases.</b>
{% endif %}
</div>
<div class="col pull-right">
<button class="btn btn-dark btn-sm pull-right" onclick="refresh_group_cac({{ group.group_id }});">
<span class="menu-title">Refresh</span>
</button>
<button class="btn btn-dark btn-sm pull-right mr-2" onclick="manage_group_cac({{ group.group_id }});">
<span class="menu-title" id="manage_group_cac_button">Set case access</span>
</button>
</div>
</div>
<div class="row mt-2">
<table class="table display table-bordered table-striped table-hover responsive" width="100%" cellspacing="0" id="group_cac_table" >
<thead>
<tr>
<th>Case ID</th>
<th>Case Name</th>
<th>Access</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Case ID</th>
<th>Case Name</th>
<th>Access</th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
<script>
var data = [
{% for e in all_perms %}
{
value: {{ e.value }},
label: "{{ e.name }}"
}
{% if not loop.last %},{% endif %}
{% endfor %}
];
$('#group_permissions').multiselect({
buttonWidth: 400,
nonSelectedText: 'Select permissions',
includeSelectAllOption: true,
enableFiltering: true,
enableCaseInsensitiveFiltering: true,
filterPlaceholder: 'Search',
filterBehavior: 'both',
widthSynchronizationMode: 'ifPopupIsSmaller'
});
$('#group_permissions').multiselect('dataprovider', data );
{% if group.group_permissions_list %}
$('#group_permissions').multiselect('select', [
{% for perm in group.group_permissions_list %} {{ perm.value }}, {% endfor %}
]);
$('#org_members').multiselect('refresh')
{% endif %}
var modal_group_table = $("#group_members_table").DataTable({
dom: 'Blfrtip',
aaData: [],
aoColumns: [
{
"data": "id",
"render": function ( data, type, row ) {
return `<i class="fa-solid fa-trash-can mr-2 text-danger" style="cursor:pointer;" title="Remove from group" href="javascript:void(0)" onclick="remove_members_from_group('{{ group.group_id }}',${data})"></i>${data}`;
},
"className": "dt-center"
},
{
"data": "user",
"className": "dt-center",
"render": function (data, type, row) {
return sanitizeHTML(data);
}
},
{
"data": "name",
"className": "dt-center",
"render": function (data, type, row) {
return sanitizeHTML(data);
}
}
],
filter: true,
info: true,
ordering: true,
processing: true
});
{% if group.group_id %}
modal_group_table.rows.add({{ group.group_members|tojson }});
modal_group_table.columns.adjust().draw();
{% endif %}
var modal_group_cac_table = $("#group_cac_table").DataTable({
dom: 'Blfrtip',
aaData: [],
aoColumns: [
{
"data": "case_id",
"render": function ( data, type, row ) {
return `<i class="fa-solid fa-trash-can mr-2 text-danger" style="cursor:pointer;" title="Remove access to case" href="javascript:void(0)" onclick="remove_case_access_from_group('{{ group.group_id }}',${data})"></i>${data}`;
},
"className": "dt-center"
},
{
"data": "case_name",
"className": "dt-center",
"render": function (data, type, row) {
return `<a target="_blank" rel="noopener" href="/case?cid=${row.case_id}">${sanitizeHTML(data)}</a>`;
}
},
{
"data": "access_level_list",
"render": function ( data, type, row ) {
ret_data = "";
for (acc in data) {
ret_data += `<span class="badge ml-2 badge-light">${data[acc].name}</span>`
}
return ret_data;
},
"className": "dt-center"
}
],
filter: true,
info: true,
ordering: true,
processing: true,
select: true
});
var actionOptionsGroup = {
classes: [],
contextMenu: {
enabled: true,
isMulti: true,
xoffset: -10,
yoffset: -10,
headerRenderer: function (rows) {
if (rows.length > 1) {
return rows.length + ' items selected';
} else {
let row = rows[0];
return 'Quick action';
}
},
},
buttonList: {
enabled: false,
},
deselectAfterAction: true,
items: [],
};
{% if group.group_id %}
actionOptionsGroup.items.push({
type: 'option',
title: 'Remove access',
multi: true,
iconClass: 'fas fa-trash',
buttonClasses: ['btn', 'btn-outline-primary'],
action: function(rows){
remove_group_cases_from_group_table({{ group.group_id }}, rows);
}
});
modal_group_cac_table.contextualActions(actionOptionsGroup);
current_group_cases_access_list = {{ group.group_cases_access|tojson }};
modal_group_cac_table.rows.add(current_group_cases_access_list);
modal_group_cac_table.columns.adjust().draw();
{% endif %}
</script>

View File

@@ -0,0 +1,90 @@
<div class="modal-lg modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title mt-2 mr-4">Set case access</h4>
<div class="row text-right">
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
</div>
<div class="modal-body">
<div class="container col-md-12" >
<div class="row">
<div class="form-group" data-select2-id="7">
<label>Set cases access of group <i>{{ group.group_name }}</i> *</label>
<div class="row">
<div class="col-12">
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" id="enable_auto_follow_cases" name="enable_auto_follow_cases" {% if group.group_auto_follow %}checked{% endif %}>
<span class="form-check-sign">Apply to currents and futures cases</span>
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<select id="group_case_access_select" name="org_case_access_select" class="form-control select2-hidden-accessible ml-12" multiple="multiple"
tabindex="-1" aria-hidden="true" style="width: 100%">
</select>
<select id="group_case_ac_select" name="org_case_ac_select" class="form-control select2-hidden-accessible ml-12"
tabindex="-1" aria-hidden="true" style="width: 100%">
</select>
</div>
</div>
</div>
</div>
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
id="grant_case_access_to_group">Set access</button>
</div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
<script>
var data = [];
$('#group_case_access_select').multiselect({
buttonWidth: 400,
nonSelectedText: 'Select case',
emptyText: 'No case available to add',
includeSelectAllOption: true,
enableFiltering: true,
enableCaseInsensitiveFiltering: true,
filterPlaceholder: 'Search',
filterBehavior: 'both',
widthSynchronizationMode: 'ifPopupIsSmaller'
});
$('#group_case_access_select').multiselect('dataprovider', [{% for ocs in outer_cases %}
{ label: "{{ ocs.case_name }}", value: {{ ocs.case_id }} }, {% endfor %}]);
$('#group_case_access_select').multiselect('refresh')
$('#group_case_ac_select').multiselect({
nonSelectedText: 'Select access level',
includeSelectAllOption: true,
enableFiltering: true,
enableCaseInsensitiveFiltering: true,
filterPlaceholder: 'Search',
filterBehavior: 'both',
widthSynchronizationMode: 'ifPopupIsSmaller'
});
$('#group_case_ac_select').multiselect('dataprovider', [{% for acc in access_levels %}
{ label: "{{ acc.name }}", value: {{ acc.value }} }, {% endfor %}]);
$('#group_case_ac_select').multiselect('refresh');
$('#enable_auto_follow_cases').on('change', function() {
if (this.checked) {
$('#group_case_access_select').multiselect('disable');
} else {
$('#group_case_access_select').multiselect('enable');
}
});
if ($('#enable_auto_follow_cases').is(':checked')) {
$('#group_case_access_select').multiselect('disable');
}
</script>

View File

@@ -0,0 +1,62 @@
<div class="modal-lg modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title mt-2 mr-4">Manage members of group {{ group.group_name }}</h4>
<div class="row text-right">
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
</div>
<div class="modal-body">
<div class="container col-md-12" >
<div class="row">
<div class="form-group" data-select2-id="7">
<label>Members *</label>
<div class="select2-input ml-12" data-select2-id="6">
<select id="group_members" name="group_members" class="form-control select2-hidden-accessible ml-12" multiple=""
data-select2-id="group_members" tabindex="-1" aria-hidden="true" style="width: 100%">
</select>
</div>
</div>
</div>
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
id="save_group_members">Save</button>
</div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
<script>
var data = [];
if (current_users_list.length === 0) {
refresh_users();
}
for (user in current_users_list) {
data.push({
label: `${current_users_list[user].user_login} (${current_users_list[user].user_name} - ${current_users_list[user].user_email})`,
value: current_users_list[user].user_id
});
}
$('#group_members').multiselect({
buttonWidth: 400,
nonSelectedText: 'Select members',
includeSelectAllOption: true,
enableFiltering: true,
enableCaseInsensitiveFiltering: true,
filterPlaceholder: 'Search',
filterBehavior: 'both',
widthSynchronizationMode: 'ifPopupIsSmaller'
});
$('#group_members').multiselect('dataprovider', data );
{% if group.group_members %}
$('#group_members').multiselect('select', [
{% for member in group.group_members %} {{ member.id }}, {% endfor %}
]);
$('#group_members').multiselect('refresh')
{% endif %}
</script>

View File

@@ -0,0 +1,343 @@
<div class="modal-xl modal-dialog" role="document">
<div class="modal-content" id="modal_access_control_content">
<div class="modal-header">
<div class="row w-100 d-flex justify-content-center">
<h4 class="modal-title ml-4 mt-3">{% if not user.user_id %}Add User{% else %} Edit user {% endif %}</h4>
<div class="col">
{% if user.user_id %}
<ul class="nav nav-pills nav-default justify-content-center" role="tablist">
<li class="nav-item">
<a class="nav-link active show" data-toggle="pill" href="#user_details_tab" role="tab" aria-controls="user_details_tab" aria-selected="false">Info</a>
</li>
<li class="nav-item submenu">
<a class="nav-link" data-toggle="pill" href="#user_permissions_tab" role="tab" aria-controls="user_permissions_tab" aria-selected="false">Permissions</a>
</li>
<li class="nav-item submenu">
<a class="nav-link" data-toggle="pill" href="#user_groups_tab" role="tab" aria-controls="user_groups_tab" aria-selected="false">Groups</a>
</li>
<li class="nav-item submenu">
<a class="nav-link" data-toggle="pill" href="#user_cac_tab" role="tab" aria-controls="user_cac_tab" aria-selected="false">Cases access</a>
</li>
</ul>
{% endif %}
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
</div>
</div>
<div class="modal-body">
<div role="tabpanel">
<div class="tab-content">
<div class="tab-pane active" id="user_details_tab">
<div class="container col-md-12">
<form method="post" action="" id="form_new_user">
<div class="col-md-12 col-lg-12 col-sm-12">
{{ form.hidden_tag() }}
{% if not user.user_id %}
<p class="ml-3"><i class="fa-solid fa-circle-info mr-2"></i>Permissions and groups memberships can be set once the user is created.</p>
{% endif %}
{% if user.user_is_service_account %}
<p class="ml-3 text-warning-high"><i class="fa-solid fa-circle-info mr-2"></i>This is a service account. It cannot login interactively nor have a password.</p>
{% endif %}
<div class="form-group">
<label for="user_name" class="mr-4">Full name
</label>
{{ form.user_name(class='form-control', autocomplete="off") }}
</div>
<div class="form-group mt-3">
<label for="user_login" class="placeholder">Login</label>
{{ form.user_login(class='form-control', autocomplete="off") }}
</div>
<div class="form-group mt-3">
<label for="user_email" class="placeholder">Email</label>
{{ form.user_email(class='form-control', autocomplete="off") }}
</div>
{% if not user.user_is_service_account %}
<div class="form-group mt-3" id="formGroupUserPassword">
<label for="user_pwd" class="placeholder">Password (optional for service accounts)</label>
<ul>
<li><small>Must contain at least {{ server_settings.password_policy_min_length }} chars</small></li>
{% if server_settings.password_policy_upper_case %}
<li class="text-sm"><small>Must contain at least an upper case</small></li>
{% endif %}
{% if server_settings.password_policy_lower_case %}
<li class="text-sm"><small>Must contain at least a lower case</small></li>
{% endif %}
{% if server_settings.password_policy_digit %}
<li class="text-sm"><small>Must contain at least a digit</small></li>
{% endif %}
{% if server_settings.password_policy_special_chars %}
<li class="text-sm"><small>Must contain at least one of : {{ server_settings.password_policy_special_chars }}</small></li>
{% endif %}
</ul>
<div class="input-group mb-3">
{{ form.user_password(class='form-control', autocomplete="off") }}
<div class="input-group-append">
<span class="input-group-text">
<div class="user_show_password" id="toggle_user_password"><i class="fa-solid fa-eye"></i></div>
</span>
</div>
</div>
</div>
{% endif %}
{% if not user.user_id %}
<div class="form-group mt-3">
<div class="form-check">
<label class="form-check-label mt-3" >
{{ form.user_is_service_account(class="form-check-input", type="checkbox") }}
<span class="form-check-sign" id="formCheckIsServiceAccount"> Use as service account
<i class="ml-1 mt-1 fa-regular fa-circle-question" title="If checked, the user won't appear in the attribution suggestions and won't be able to connect on the UI" style="cursor:pointer;"></i>
</span>
</label>
</div>
</div>
{% endif %}
{% if user.user_id %}
<div class="form-group mt-3">
<label for="user_id" class="placeholder">User ID</label>
<input autocomplete="off" class="form-control" type="text" value="{{ user.user_id }}" disabled>
</div>
<div class="form-group mt-3">
<label for="user_uuid" class="placeholder">User UUID</label>
<input autocomplete="off" class="form-control" type="text" value="{{ user.user_uuid }}" disabled>
</div>
<div class="form-group mt-3">
<label for="user_uuid" class="placeholder">User API Key</label>
<input autocomplete="off" class="form-control" type="text" value="{{ user.user_api_key }}" disabled>
</div>
{% endif %}
</div>
{% if user.user_id %}
<button type="button" class="btn btn-danger mt-5"
onclick="delete_user('{{ user.user_id }}');">Delete</button>
{% if user.user_active %}
<button type="button" class="btn btn-outline-danger mt-5"
onclick="deactivate_user('{{ user.user_id }}');">Deactivate</button>
{% else %}
<button type="button" class="btn btn-outline-success mt-5"
onclick="activate_user('{{ user.user_id }}');">Activate</button>
{% endif %}
<button type="button" class="btn btn-outline-success ml-1 mt-5 float-right"
id="submit_new_user">Update</button>
<button type="button" class="btn btn-dark mt-5 float-right"
onclick="refresh_user_ac('{{ user.user_id }}');" id="users_refresh_ac_btn">Refresh access</button>
{% else %}
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
id="submit_new_user">Save</button>
{% endif %}
</form>
</div>
</div>
<div class="tab-pane" id="user_permissions_tab">
<div class="container col-md-12">
<p class="mb-4"><i class="fa-solid fa-circle-info mr-2"></i>Permissions are inherited from the groups the user belongs to. The table shows the effective permissions the user has on the platform.</p>
<table class="table table-striped" id="user_permissions_table">
<thead>
<tr>
<th>Permission</th>
<th>Value</th>
<th>Inherited from groups</th>
</tr>
</thead>
<tbody>
{% for perm in user.user_permissions %}
<tr>
<td>{{ user.user_permissions[perm].name }}</td>
<td>0x{{ perm | int(perm,16) }}</td>
<td>{% for group in user.user_permissions[perm].inherited_from %}<span class="badge ml-2 badge-light">{{ group }}</span>{% endfor %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="tab-pane" id="user_groups_tab">
<div class="row">
<div class="container col-md-12">
<span class="ml-2 mt-3">Groups the user is member of.</span>
<button class="btn btn-dark btn-sm pull-right mr-2" onclick="manage_user_groups({{ user.user_id }});">
<span class="menu-title">Manage</span>
</button>
</div>
</div>
<div class="row mt-4">
<div class="container col-md-12">
<table class="table table-striped" id="user_groups_table">
<thead>
<tr>
<th>Group name</th>
<th>Group ID</th>
<th>Group UUID</th>
</tr>
</thead>
<tbody>
{% for group in user.user_groups %}
<tr>
<td>{{ group.group_name }}</td>
<td>{{ group.group_id }}</td>
<td>{{ group.group_uuid }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="tab-pane" id="user_cac_tab">
<div class="container col-md-12" >
<div class="row d-flex mb-4">
<div class="col-8">
<span>Cases accesses are usually inherited from groups memberships. These are not displayed here. This tab allows to add granular case access if necessary.</span>
</div>
<div class="col-4 pull-right">
<button class="btn btn-dark btn-sm pull-right" onclick="refresh_user_cac({{ user.user_id }});">
<span class="menu-title">Refresh</span>
</button>
<button class="btn btn-dark btn-sm pull-right mr-2" onclick="manage_user_cac({{ user.user_id }});">
<span class="menu-title" id="manage_user_cac_button">Set case access</span>
</button>
</div>
</div>
<div class="row mt-4">
<table class="table display table-striped table-hover responsive" width="100%" cellspacing="0" id="user_cac_table" >
<thead>
<tr>
<th>Case ID</th>
<th>Case Name</th>
<th>Access</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Case ID</th>
<th>Case Name</th>
<th>Access</th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
$('#toggle_user_password').on('click', function (e) {
const type = $('#user_password').attr('type') === 'password' ? 'text' : 'password';
$('#user_password').attr('type', type);
$('#toggle_user_password > i').attr('class', type === 'password' ? 'fa-solid fa-eye' : 'fa-solid fa-eye-slash');
});
$('#user_permissions_table').dataTable({
"order": [[ 1, "asc" ]]});
$('#user_groups_table').dataTable({
"order": [[ 1, "asc" ]],
"columns": [{
"title": "Group name",
"render": function ( data, type, row, meta ) {
if (type === 'display' ) {
return `<i class="fa-solid fa-trash-can mr-2 text-danger" style="cursor:pointer;" title="Remove from group" href="javascript:void(0)" onclick="remove_member_from_group_wrap('${row[1]}','{{ user.user_id }}')"></i>${sanitizeHTML(data)}`;
}
return data;
}},
{"title": "Group ID"},
{"title": "Group UUID"}
]
});
var modal_user_cac_table = $("#user_cac_table").DataTable({
dom: 'Blfrtip',
aaData: [],
aoColumns: [
{
"data": "case_id",
"render": function ( data, type, row ) {
return `<i class="fa-solid fa-trash-can mr-2 text-danger" style="cursor:pointer;" title="Remove access to case" href="javascript:void(0)" onclick="remove_case_access_from_user('{{ user.user_id }}',${data})"></i>${data}`;
},
"className": "dt-center"
},
{
"data": "case_name",
"className": "dt-center",
"render": function (data, type, row) {
return `<a target="_blank" rel="noopener" href="/case?cid=${row.case_id}">${sanitizeHTML(data)}</a>`;
}
},
{
"data": "access_level_list",
"render": function ( data, type, row ) {
ret_data = "";
for (acc in data) {
ret_data += `<span class="badge ml-2 badge-light">${data[acc].name}</span>`
}
return ret_data;
},
"className": "dt-center"
}
],
filter: true,
info: true,
ordering: true,
processing: true,
select: true
});
var actionOptionsUser = {
classes: [],
contextMenu: {
enabled: true,
isMulti: true,
xoffset: -10,
yoffset: -10,
headerRenderer: function (rows) {
if (rows.length > 1) {
return rows.length + ' items selected';
} else {
let row = rows[0];
return 'Quick action';
}
},
},
buttonList: {
enabled: false,
},
deselectAfterAction: true,
items: [],
};
actionOptionsUser.items.push({
type: 'option',
title: 'Remove access',
multi: true,
iconClass: 'fas fa-trash',
buttonClasses: ['btn', 'btn-outline-primary'],
action: function(rows){
remove_cases_access_from_user_table('{{ user.user_id }}', rows);
}
});
modal_user_cac_table.contextualActions(actionOptionsUser);
{% if user.user_id %}
current_user_cases_access_list = {{ user.user_cases_access|tojson }};
modal_user_cac_table.rows.add(current_user_cases_access_list);
modal_user_cac_table.columns.adjust().draw();
{% endif %}
</script>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->

View File

@@ -0,0 +1,66 @@
<div class="modal-lg modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title mt-2 mr-4">Set case access</h4>
<div class="row text-right">
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
</div>
<div class="modal-body">
<div class="container col-md-12" >
<div class="row">
<div class="form-group" data-select2-id="7">
<label>Set cases access of user <i>{{ user.user_name }}</i> *</label>
<div class="col-12">
<select id="user_case_access_select" name="org_case_access_select" class="form-control ml-12" multiple="multiple"
tabindex="-1" aria-hidden="true" style="width: 100%">
</select>
<select id="user_case_ac_select" name="org_case_ac_select" class="form-control ml-12"
tabindex="-1" aria-hidden="true" style="width: 100%">
</select>
</div>
</div>
</div>
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
id="grant_case_access_to_user">Set access</button>
</div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
<script>
var data = [];
$('#user_case_access_select').multiselect({
buttonWidth: 400,
nonSelectedText: 'Select case',
emptyText: 'No case available to add',
includeSelectAllOption: true,
enableFiltering: true,
enableCaseInsensitiveFiltering: true,
filterPlaceholder: 'Search',
filterBehavior: 'both',
widthSynchronizationMode: 'ifPopupIsSmaller'
});
$('#user_case_access_select').multiselect('dataprovider', [{% for ocs in outer_cases %}
{ label: "{{ ocs.case_name }}", value: {{ ocs.case_id }} }, {% endfor %}]);
$('#user_case_access_select').multiselect('refresh')
$('#user_case_ac_select').multiselect({
nonSelectedText: 'Select access level',
includeSelectAllOption: true,
enableFiltering: true,
enableCaseInsensitiveFiltering: true,
filterPlaceholder: 'Search',
filterBehavior: 'both',
widthSynchronizationMode: 'ifPopupIsSmaller'
});
$('#user_case_ac_select').multiselect('dataprovider', [{% for acc in access_levels %}
{ label: "{{ acc.name }}", value: {{ acc.value }} }, {% endfor %}]);
$('#user_case_ac_select').multiselect('refresh');
</script>

View File

@@ -0,0 +1,68 @@
<div class="modal-lg modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title mt-2 mr-4">Set case access via groups</h4>
<div class="row text-right">
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true"><i class="fa fa-times"></i></span></button>
</div>
</div>
<div class="modal-body">
<div class="container col-md-12" >
<div class="row">
<div class="form-group" data-select2-id="7">
<label>Set groups case access</label>
<div class="row">
<div class="col-12">
<select id="group_case_access_select" name="group_case_access_select" class="form-control select2-hidden-accessible ml-12" multiple="multiple"
tabindex="-1" aria-hidden="true" style="width: 100%">
</select>
<select id="group_case_ac_select" name="group_case_ac_select" class="form-control select2-hidden-accessible ml-12"
tabindex="-1" aria-hidden="true" style="width: 100%">
</select>
</div>
</div>
</div>
</div>
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
onclick="set_case_access_via_group('{{ caseid }}')">Set access</button>
</div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
<script>
var data = [];
$('#group_case_access_select').multiselect({
buttonWidth: 400,
nonSelectedText: 'Select group(s)',
emptyText: 'No groups available to set',
includeSelectAllOption: true,
enableFiltering: true,
enableCaseInsensitiveFiltering: true,
filterPlaceholder: 'Search',
filterBehavior: 'both',
widthSynchronizationMode: 'ifPopupIsSmaller'
});
$('#group_case_access_select').multiselect('dataprovider', [{% for ocs in groups %}
{ label: "{{ ocs.group_name }}", value: {{ ocs.group_id }} }, {% endfor %}]);
$('#group_case_access_select').multiselect('refresh')
$('#group_case_ac_select').multiselect({
nonSelectedText: 'Select access level',
includeSelectAllOption: true,
enableFiltering: true,
enableCaseInsensitiveFiltering: true,
filterPlaceholder: 'Search',
filterBehavior: 'both',
widthSynchronizationMode: 'ifPopupIsSmaller'
});
$('#group_case_ac_select').multiselect('dataprovider', [{% for acc in access_levels %}
{ label: "{{ acc.name }}", value: {{ acc.value }} }, {% endfor %}]);
$('#group_case_ac_select').multiselect('refresh');
</script>

View File

@@ -0,0 +1,65 @@
<div class="card">
<div class="card-header">
<div class="row">
<div class="col col-heading">
<div class="card-title">Case access</div>
</div>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-12">
<p>Click on a user to unveil access control path</p>
<table class="table table-striped table-bordered" id="case_audit_access_table">
<thead>
<tr>
<th>User ID</th>
<th>User Name</th>
<th>User UUID</th>
<th>Access</th>
</tr>
</thead>
<tbody>
{% for user_id in access_audit %}
<tr>
<td>
{% if access_audit[user_id].user_effective_access_list|length == 0 %}
<i class="mr-2 fa-solid text-danger fa-circle-xmark" title="No access"></i>
{% elif access_audit[user_id].user_effective_access_list|length == 1 %}
<i class="mr-2 fa-solid text-warning fa-circle-minus" title="Partial access"></i>
{% else %}
<i class="mr-2 fa-solid fa-circle-check text-success" title="All access"></i>
{% endif %}
{{ user_id }}
</td>
<td>{{ access_audit[user_id].user_info.user_name }}</td>
<td>{{ access_audit[user_id].user_info.user_uuid }}</td>
<td>{% if access_audit[user_id].user_effective_access_list|length == 0 %}
<span class="badge badge-pill badge-light" title="Click to show trace" href="#collapse_cac_{{user_id}}" style="cursor:pointer;" data-toggle="collapse" role="button" aria-expanded="false" aria-controls="collapse_cac_{{user_id}}">No access</span>
{% else %}
{% for uac in access_audit[user_id].user_effective_access_list %}
<span class="badge badge-pill badge-light" title="Click to show trace" href="#collapse_cac_{{user_id}}" style="cursor:pointer;" data-toggle="collapse" role="button" aria-expanded="false" aria-controls="collapse_cac_{{user_id}}">{{ uac }}</span>
{% endfor %}
<i class="fa-solid fa-eye ml-2" title="Click to show trace" href="#collapse_cac_{{user_id}}" style="cursor:pointer;" data-toggle="collapse" role="button" aria-expanded="false" aria-controls="collapse_cac_{{user_id}}"></i>
{% endif %}
<ol class="activity-feed collapse" id="collapse_cac_{{user_id}}">
{% if access_audit[user_id].user_effective_access_list|length == 0 %}
The user is neither in a group nor an organisation who has access to this case
{% endif %}
{% for uac in access_audit[user_id].access_trace %}
<li class="feed-item {% if uac.state == 'Effective' %} feed-item-success {% else %} feed-item-danger {% endif %}" title="{{ uac.state }}">
<span class="text"><span class="badge badge-pill badge-light">{{ uac.name }}</span> ({{ uac.state }})</span><br />
<span class="text">Inherited from {{ uac.inherited_from.object_type }} <i class="fa-solid fa-right-long"></i> {{ uac.inherited_from.object_name }} (ID {{ uac.inherited_from.object_id }} :: UUID {{ uac.inherited_from.object_uuid }})</span>
</li>
{% endfor %}
</ol>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More