This commit is contained in:
0
iris-web/source/app/blueprints/__init__.py
Normal file
0
iris-web/source/app/blueprints/__init__.py
Normal 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)
|
||||
@@ -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 %}
|
||||
0
iris-web/source/app/blueprints/alerts/__init__.py
Normal file
0
iris-web/source/app/blueprints/alerts/__init__.py
Normal file
1002
iris-web/source/app/blueprints/alerts/alerts_routes.py
Normal file
1002
iris-web/source/app/blueprints/alerts/alerts_routes.py
Normal file
File diff suppressed because it is too large
Load Diff
310
iris-web/source/app/blueprints/alerts/templates/alerts.html
Normal file
310
iris-web/source/app/blueprints/alerts/templates/alerts.html
Normal 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">×</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">×</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 %}
|
||||
|
||||
@@ -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">×</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>
|
||||
@@ -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">×</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>
|
||||
@@ -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">×</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>
|
||||
0
iris-web/source/app/blueprints/api/__init__.py
Normal file
0
iris-web/source/app/blueprints/api/__init__.py
Normal file
50
iris-web/source/app/blueprints/api/api_routes.py
Normal file
50
iris-web/source/app/blueprints/api/api_routes.py
Normal 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)
|
||||
25
iris-web/source/app/blueprints/case/__init__.py
Normal file
25
iris-web/source/app/blueprints/case/__init__.py
Normal 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 ------------------------------------------------
|
||||
503
iris-web/source/app/blueprints/case/case_assets_routes.py
Normal file
503
iris-web/source/app/blueprints/case/case_assets_routes.py
Normal 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)
|
||||
59
iris-web/source/app/blueprints/case/case_comments.py
Normal file
59
iris-web/source/app/blueprints/case/case_comments.py
Normal 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)
|
||||
152
iris-web/source/app/blueprints/case/case_graphs_routes.py
Normal file
152
iris-web/source/app/blueprints/case/case_graphs_routes.py
Normal 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)
|
||||
453
iris-web/source/app/blueprints/case/case_ioc_routes.py
Normal file
453
iris-web/source/app/blueprints/case/case_ioc_routes.py
Normal 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)
|
||||
511
iris-web/source/app/blueprints/case/case_notes_routes.py
Normal file
511
iris-web/source/app/blueprints/case/case_notes_routes.py
Normal 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)
|
||||
306
iris-web/source/app/blueprints/case/case_rfiles_routes.py
Normal file
306
iris-web/source/app/blueprints/case/case_rfiles_routes.py
Normal 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)
|
||||
449
iris-web/source/app/blueprints/case/case_routes.py
Normal file
449
iris-web/source/app/blueprints/case/case_routes.py
Normal 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))
|
||||
368
iris-web/source/app/blueprints/case/case_tasks_routes.py
Normal file
368
iris-web/source/app/blueprints/case/case_tasks_routes.py
Normal 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)
|
||||
|
||||
1155
iris-web/source/app/blueprints/case/case_timeline_routes.py
Normal file
1155
iris-web/source/app/blueprints/case/case_timeline_routes.py
Normal file
File diff suppressed because it is too large
Load Diff
49
iris-web/source/app/blueprints/case/templates/case-nav.html
Normal file
49
iris-web/source/app/blueprints/case/templates/case-nav.html
Normal 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>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
337
iris-web/source/app/blueprints/case/templates/case.html
Normal file
337
iris-web/source/app/blueprints/case/templates/case.html
Normal 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">×</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">×</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">×</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">×</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 %}
|
||||
148
iris-web/source/app/blueprints/case/templates/case_assets.html
Normal file
148
iris-web/source/app/blueprints/case/templates/case_assets.html
Normal 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">×</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 "|")</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 %}
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
149
iris-web/source/app/blueprints/case/templates/case_ioc.html
Normal file
149
iris-web/source/app/blueprints/case/templates/case_ioc.html
Normal 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">×</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
|
||||
<Value>,<Type>,<Description>,<TLP>,Tags separated with "|"</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 %}
|
||||
@@ -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>
|
||||
134
iris-web/source/app/blueprints/case/templates/case_notes.html
Normal file
134
iris-web/source/app/blueprints/case/templates/case_notes.html
Normal 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 %}
|
||||
105
iris-web/source/app/blueprints/case/templates/case_rfile.html
Normal file
105
iris-web/source/app/blueprints/case/templates/case_rfile.html
Normal 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 %}
|
||||
109
iris-web/source/app/blueprints/case/templates/case_tasks.html
Normal file
109
iris-web/source/app/blueprints/case/templates/case_tasks.html
Normal 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 %}
|
||||
178
iris-web/source/app/blueprints/case/templates/case_timeline.html
Normal file
178
iris-web/source/app/blueprints/case/templates/case_timeline.html
Normal 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">×</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 "|"),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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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">×</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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 you’re 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 %}
|
||||
25
iris-web/source/app/blueprints/context/__init__.py
Normal file
25
iris-web/source/app/blueprints/context/__init__.py
Normal 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 ------------------------------------------------
|
||||
145
iris-web/source/app/blueprints/context/context.py
Normal file
145
iris-web/source/app/blueprints/context/context.py
Normal 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
|
||||
381
iris-web/source/app/blueprints/dashboard/dashboard_routes.py
Normal file
381
iris-web/source/app/blueprints/dashboard/dashboard_routes.py
Normal 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")
|
||||
397
iris-web/source/app/blueprints/dashboard/templates/index.html
Normal file
397
iris-web/source/app/blueprints/dashboard/templates/index.html
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
455
iris-web/source/app/blueprints/datastore/datastore_routes.py
Normal file
455
iris-web/source/app/blueprints/datastore/datastore_routes.py
Normal 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)
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
59
iris-web/source/app/blueprints/demo_landing/demo_landing.py
Normal file
59
iris-web/source/app/blueprints/demo_landing/demo_landing.py
Normal 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
|
||||
)
|
||||
@@ -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&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>
|
||||
319
iris-web/source/app/blueprints/dim_tasks/dim_tasks.py
Normal file
319
iris-web/source/app/blueprints/dim_tasks/dim_tasks.py
Normal 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)
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
0
iris-web/source/app/blueprints/filters/__init__.py
Normal file
0
iris-web/source/app/blueprints/filters/__init__.py
Normal file
177
iris-web/source/app/blueprints/filters/filters_routes.py
Normal file
177
iris-web/source/app/blueprints/filters/filters_routes.py
Normal 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))
|
||||
0
iris-web/source/app/blueprints/login/__init__.py
Normal file
0
iris-web/source/app/blueprints/login/__init__.py
Normal file
163
iris-web/source/app/blueprints/login/login_routes.py
Normal file
163
iris-web/source/app/blueprints/login/login_routes.py
Normal 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)
|
||||
73
iris-web/source/app/blueprints/login/templates/login.html
Normal file
73
iris-web/source/app/blueprints/login/templates/login.html
Normal 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 %}
|
||||
0
iris-web/source/app/blueprints/manage/__init__.py
Normal file
0
iris-web/source/app/blueprints/manage/__init__.py
Normal 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)
|
||||
|
||||
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
@@ -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))
|
||||
231
iris-web/source/app/blueprints/manage/manage_case_state.py
Normal file
231
iris-web/source/app/blueprints/manage/manage_case_state.py
Normal 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")
|
||||
@@ -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")
|
||||
568
iris-web/source/app/blueprints/manage/manage_cases_routes.py
Normal file
568
iris-web/source/app/blueprints/manage/manage_cases_routes.py
Normal 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)
|
||||
404
iris-web/source/app/blueprints/manage/manage_customers_routes.py
Normal file
404
iris-web/source/app/blueprints/manage/manage_customers_routes.py
Normal 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")
|
||||
@@ -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))
|
||||
|
||||
383
iris-web/source/app/blueprints/manage/manage_groups.py
Normal file
383
iris-web/source/app/blueprints/manage/manage_groups.py
Normal 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)
|
||||
199
iris-web/source/app/blueprints/manage/manage_ioc_types_routes.py
Normal file
199
iris-web/source/app/blueprints/manage/manage_ioc_types_routes.py
Normal 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))
|
||||
321
iris-web/source/app/blueprints/manage/manage_modules_routes.py
Normal file
321
iris-web/source/app/blueprints/manage/manage_modules_routes.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
213
iris-web/source/app/blueprints/manage/manage_templates_routes.py
Normal file
213
iris-web/source/app/blueprints/manage/manage_templates_routes.py
Normal 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)
|
||||
51
iris-web/source/app/blueprints/manage/manage_tlps_routes.py
Normal file
51
iris-web/source/app/blueprints/manage/manage_tlps_routes.py
Normal 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)
|
||||
|
||||
|
||||
489
iris-web/source/app/blueprints/manage/manage_users.py
Normal file
489
iris-web/source/app/blueprints/manage/manage_users.py
Normal 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)
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">×</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 -->
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
Reference in New Issue
Block a user