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

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

View File

@ -0,0 +1,136 @@
#!/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 collections
import json
import logging as logger
import os
import urllib.parse
from flask import Flask
from flask import session
from flask_bcrypt import Bcrypt
from flask_caching import Cache
from flask_login import LoginManager
from flask_marshmallow import Marshmallow
from flask_socketio import SocketIO, Namespace
from flask_sqlalchemy import SQLAlchemy
from functools import partial
from sqlalchemy_imageattach.stores.fs import HttpExposedFileSystemStore
from werkzeug.middleware.proxy_fix import ProxyFix
from app.flask_dropzone import Dropzone
from app.iris_engine.tasker.celery import make_celery
class ReverseProxied(object):
def __init__(self, flask_app):
self._app = flask_app
def __call__(self, environ, start_response):
scheme = environ.get('HTTP_X_FORWARDED_PROTO', None)
if scheme is not None:
environ['wsgi.url_scheme'] = scheme
return self._app(environ, start_response)
class AlertsNamespace(Namespace):
pass
APP_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TEMPLATE_PATH = os.path.join(APP_PATH, 'templates/')
# Grabs the folder where the script runs.
basedir = os.path.abspath(os.path.dirname(__file__))
LOG_FORMAT = '%(asctime)s :: %(levelname)s :: %(module)s :: %(funcName)s :: %(message)s'
LOG_TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
logger.basicConfig(level=logger.INFO, format=LOG_FORMAT, datefmt=LOG_TIME_FORMAT)
app = Flask(__name__)
def ac_current_user_has_permission(*permissions):
"""
Return True if current user has permission
"""
for permission in permissions:
if session['permissions'] & permission.value == permission.value:
return True
return False
def ac_current_user_has_manage_perms():
if session['permissions'] != 1 and session['permissions'] & 0x1FFFFF0 != 0:
return True
return False
app.jinja_env.filters['unquote'] = lambda u: urllib.parse.unquote(u)
app.jinja_env.filters['tojsonsafe'] = lambda u: json.dumps(u, indent=4, ensure_ascii=False)
app.jinja_env.filters['tojsonindent'] = lambda u: json.dumps(u, indent=4)
app.jinja_env.filters['escape_dots'] = lambda u: u.replace('.', '[.]')
app.jinja_env.globals.update(user_has_perm=ac_current_user_has_permission)
app.jinja_env.globals.update(user_has_manage_perms=ac_current_user_has_manage_perms)
app.config.from_object('app.configuration.Config')
cache = Cache(app)
SQLALCHEMY_ENGINE_OPTIONS = {
"json_deserializer": partial(json.loads, object_pairs_hook=collections.OrderedDict)
}
db = SQLAlchemy(app, engine_options=SQLALCHEMY_ENGINE_OPTIONS) # flask-sqlalchemy
bc = Bcrypt(app) # flask-bcrypt
lm = LoginManager() # flask-loginmanager
lm.init_app(app) # init the login manager
ma = Marshmallow(app) # Init marshmallow
dropzone = Dropzone(app)
celery = make_celery(app)
store = HttpExposedFileSystemStore(
path='images',
prefix='/static/assets/images/'
)
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
app.wsgi_app = store.wsgi_middleware(app.wsgi_app)
socket_io = SocketIO(app, cors_allowed_origins="*")
alerts_namespace = AlertsNamespace('/alerts')
socket_io.on_namespace(alerts_namespace)
@app.teardown_appcontext
def shutdown_session(exception=None):
db.session.remove()
from app import views

View File

@ -0,0 +1,103 @@
[alembic]
# path to migration scripts
script_location = app/alembic
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator"
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. Valid values are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # default: use os.pathsep
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
#sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,app,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_app]
level = INFO
handlers =
qualname = app
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(asctime)s :: %(levelname)s :: %(module)s :: %(funcName)s :: %(message)s
datefmt = %Y-%m-%d %H:%M:%S

View File

@ -0,0 +1 @@
Generic single-database configuration.

View File

@ -0,0 +1,27 @@
from alembic import op
from sqlalchemy import engine_from_config
from sqlalchemy.engine import reflection
def _table_has_column(table, column):
config = op.get_context().config
engine = engine_from_config(
config.get_section(config.config_ini_section), prefix='sqlalchemy.')
insp = reflection.Inspector.from_engine(engine)
has_column = False
for col in insp.get_columns(table):
if column != col['name']:
continue
has_column = True
return has_column
def _has_table(table_name):
config = op.get_context().config
engine = engine_from_config(
config.get_section(config.config_ini_section), prefix="sqlalchemy."
)
inspector = reflection.Inspector.from_engine(engine)
tables = inspector.get_table_names()
return table_name in tables

View File

@ -0,0 +1,82 @@
from alembic import context
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
import os
os.environ["ALEMBIC"] = "1"
from app.configuration import SQLALCHEMY_BASE_ADMIN_URI, PG_DB_
config.set_main_option('sqlalchemy.url', SQLALCHEMY_BASE_ADMIN_URI + PG_DB_)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = None
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
#with context.begin_transaction(): -- Fixes stuck transaction. Need more info on that
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,31 @@
"""Add prevent post-init to register case objects again during boot
Revision ID: 00b43bc4e8ac
Revises: 2604f6962838
Create Date: 2023-05-05 18:43:07.236041
"""
from alembic import op
import sqlalchemy as sa
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = '00b43bc4e8ac'
down_revision = '2604f6962838'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('server_settings', 'prevent_post_objects_repush'):
op.add_column('server_settings',
sa.Column('prevent_post_objects_repush', sa.Boolean(), default=False)
)
pass
def downgrade():
if _table_has_column('server_settings', 'prevent_post_objects_repush'):
op.drop_column('server_settings', 'prevent_post_objects_repush')
pass

View File

@ -0,0 +1,63 @@
"""Add tags to assets
Revision ID: 0db700644a4f
Revises: 6a3b3b627d45
Create Date: 2022-01-06 13:47:12.648707
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
from app.alembic.alembic_utils import _table_has_column
revision = '0db700644a4f'
down_revision = '6a3b3b627d45'
branch_labels = None
depends_on = None
def upgrade():
# Now issue changes on existing tables and migrate Asset tags
# Add column asset_tags to CaseAssets if not existing
if not _table_has_column('case_assets', 'asset_tags'):
op.add_column('case_assets',
sa.Column('asset_tags', sa.Text)
)
if _table_has_column('case_assets', 'asset_tags'):
# Set schema and make migration of data
t_case_assets = sa.Table(
'case_assets',
sa.MetaData(),
sa.Column('asset_id', sa.Integer, primary_key=True),
sa.Column('asset_name', sa.Text),
sa.Column('asset_description', sa.Text),
sa.Column('asset_domain', sa.Text),
sa.Column('asset_ip', sa.Text),
sa.Column('asset_info', sa.Text),
sa.Column('asset_compromised', sa.Boolean),
sa.Column('asset_type_id', sa.ForeignKey('asset_type.asset_id')),
sa.Column('asset_tags', sa.Text),
sa.Column('case_id', sa.ForeignKey('cases.case_id')),
sa.Column('date_added', sa.DateTime),
sa.Column('date_update', sa.DateTime),
sa.Column('user_id', sa.ForeignKey('user.id')),
sa.Column('analysis_status_id', sa.ForeignKey('analysis_status.id'))
)
# Migrate existing Assets
conn = op.get_bind()
res = conn.execute("SELECT asset_id from case_assets WHERE asset_tags IS NULL;")
results = res.fetchall()
if results:
for res in results:
conn.execute(t_case_assets.update().where(t_case_assets.c.asset_id == res[0]).values(
asset_tags=''
))
def downgrade():
pass

View File

@ -0,0 +1,45 @@
"""Add module types
Revision ID: 10a7616f3cc7
Revises: 874ba5e5da44
Create Date: 2022-02-04 07:46:32.382640
"""
import sqlalchemy as sa
from alembic import op
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = '10a7616f3cc7'
down_revision = '874ba5e5da44'
branch_labels = None
depends_on = None
def upgrade():
# Issue changes on existing user activities table and migrate existing rows
# Add column is_from_api to user_activities if not existing and set existing ones to false
if not _table_has_column('iris_module', 'module_type'):
op.add_column('iris_module',
sa.Column('module_type', sa.Text)
)
t_ua = sa.Table(
'iris_module',
sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('module_type', sa.Text)
)
conn = op.get_bind()
conn.execute(t_ua.update().values(
module_type='pipeline'
))
pass
def downgrade():
pass

View File

@ -0,0 +1,32 @@
"""Add deletion confirmation option
Revision ID: 11e066542a88
Revises: 20447ecb2245
Create Date: 2022-09-25 08:51:13.383431
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
from sqlalchemy import Boolean
from app.alembic.alembic_utils import _table_has_column
revision = '11e066542a88'
down_revision = '20447ecb2245'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('user', 'has_deletion_confirmation'):
op.add_column('user',
sa.Column('has_deletion_confirmation', Boolean(), nullable=False, server_default='false')
)
pass
def downgrade():
pass

View File

@ -0,0 +1,51 @@
"""Add customer extended fields
Revision ID: 1df4adfa3160
Revises: a3eb60654ec4
Create Date: 2022-11-11 19:23:30.355618
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
from app.alembic.alembic_utils import _table_has_column
revision = '1df4adfa3160'
down_revision = 'a3eb60654ec4'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('client', 'description'):
op.add_column('client',
sa.Column('description', sa.Text())
)
if not _table_has_column('client', 'sla'):
op.add_column('client',
sa.Column('sla', sa.Text())
)
if not _table_has_column('client', 'creation_date'):
op.add_column('client',
sa.Column('creation_date', sa.DateTime())
)
if not _table_has_column('client', 'last_update_date'):
op.add_column('client',
sa.Column('last_update_date', sa.DateTime())
)
if not _table_has_column('client', 'created_by'):
op.add_column('client',
sa.Column('created_by', sa.BigInteger(), sa.ForeignKey('user.id'), nullable=True)
)
pass
def downgrade():
pass

View File

@ -0,0 +1,183 @@
"""Objects UUID field
Revision ID: 20447ecb2245
Revises: ad4e0cd17597
Create Date: 2022-09-23 21:07:20.007874
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
from sqlalchemy import text
from sqlalchemy.dialects.postgresql import UUID
from app.alembic.alembic_utils import _table_has_column
revision = '20447ecb2245'
down_revision = 'ad4e0cd17597'
branch_labels = None
depends_on = None
def upgrade():
# ---- Cases ----
op.alter_column('cases', 'case_id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
if not _table_has_column('cases', 'case_uuid'):
op.add_column('cases',
sa.Column('case_uuid', UUID(as_uuid=True), server_default=text("gen_random_uuid()"),
nullable=False)
)
# ---- Events ----
op.alter_column('cases_events', 'event_id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
if not _table_has_column('cases_events', 'event_uuid'):
op.add_column('cases_events',
sa.Column('event_uuid', UUID(as_uuid=True), server_default=text("gen_random_uuid()"),
nullable=False)
)
# ---- Clients ----
op.alter_column('client', 'client_id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
if not _table_has_column('client', 'client_uuid'):
op.add_column('client',
sa.Column('client_uuid', UUID(as_uuid=True), server_default=text("gen_random_uuid()"),
nullable=False)
)
# ---- Case assets ----
op.alter_column('case_assets', 'asset_id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
if not _table_has_column('case_assets', 'asset_uuid'):
op.add_column('case_assets',
sa.Column('asset_uuid', UUID(as_uuid=True), server_default=text("gen_random_uuid()"),
nullable=False)
)
# ---- Case objects states ----
op.alter_column('object_state', 'object_id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
# ---- Case event IOC ----
op.alter_column('case_events_ioc', 'id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
# ---- Case event assets ----
op.alter_column('case_events_assets', 'id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
# ---- IOC ----
op.alter_column('ioc', 'ioc_id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
if not _table_has_column('ioc', 'ioc_uuid'):
op.add_column('ioc',
sa.Column('ioc_uuid', UUID(as_uuid=True), server_default=text("gen_random_uuid()"),
nullable=False)
)
# ---- Notes ----
op.alter_column('notes', 'note_id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
if not _table_has_column('notes', 'note_uuid'):
op.add_column('notes',
sa.Column('note_uuid', UUID(as_uuid=True), server_default=text("gen_random_uuid()"),
nullable=False)
)
# ---- Notes group ----
op.alter_column('notes_group', 'group_id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
if not _table_has_column('notes_group', 'group_uuid'):
op.add_column('notes_group',
sa.Column('group_uuid', UUID(as_uuid=True), server_default=text("gen_random_uuid()"),
nullable=False)
)
# ---- Notes group link ----
op.alter_column('notes_group_link', 'link_id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
# ---- case received files ----
op.alter_column('case_received_file', 'id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
if not _table_has_column('case_received_file', 'file_uuid'):
op.add_column('case_received_file',
sa.Column('file_uuid', UUID(as_uuid=True), server_default=text("gen_random_uuid()"),
nullable=False)
)
# ---- case tasks ----
op.alter_column('case_tasks', 'id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
if not _table_has_column('case_tasks', 'task_uuid'):
op.add_column('case_tasks',
sa.Column('task_uuid', UUID(as_uuid=True), server_default=text("gen_random_uuid()"),
nullable=False)
)
# ---- global tasks ----
op.alter_column('global_tasks', 'id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
if not _table_has_column('global_tasks', 'task_uuid'):
op.add_column('global_tasks',
sa.Column('task_uuid', UUID(as_uuid=True), server_default=text("gen_random_uuid()"),
nullable=False)
)
# ---- user activity ----
op.alter_column('user_activity', 'id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
# ---- Iris Hooks ----
op.alter_column('iris_module_hooks', 'id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
pass
def downgrade():
pass

View File

@ -0,0 +1,54 @@
"""Add case state
Revision ID: 2604f6962838
Revises: db93d5c4c0aa
Create Date: 2023-05-05 11:16:19.997383
"""
from alembic import op
import sqlalchemy as sa
from app.models.cases import CaseState
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = '2604f6962838'
down_revision = 'db93d5c4c0aa'
branch_labels = None
depends_on = None
def upgrade():
# Add the state_id column to the cases table
if not _table_has_column('cases', 'state_id'):
state_id = 1
state = CaseState.query.filter_by(state_id=state_id).first()
if state is None:
state = CaseState()
state.id=state_id
state.state_name='Unspecified'
state.state_description='Unspecified'
state.protected=True
op.bulk_insert(CaseState.__table__, [state.__dict__])
op.add_column(
'cases',
sa.Column('state_id', sa.Integer, sa.ForeignKey('case_state.state_id'), nullable=True,
server_default=sa.text("1"))
)
# Set the default value for the state_id column
op.execute("UPDATE cases SET state_id = 1")
# Create a foreign key constraint between cases.state_id and case_state.state_id
op.create_foreign_key(
None, 'cases', 'case_state', ['state_id'], ['state_id']
)
def downgrade():
pass

View File

@ -0,0 +1,38 @@
"""Adding IOC and assets enrichments
Revision ID: 2a4a8330b908
Revises: f727badcc4e1
Create Date: 2023-04-26 08:42:19.397146
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = '2a4a8330b908'
down_revision = 'f727badcc4e1'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('case_assets', 'asset_enrichment'):
# Add asset_enrichment column to case_assets
op.add_column('case_assets', sa.Column('asset_enrichment', JSONB, nullable=True))
if not _table_has_column('ioc', 'ioc_enrichment'):
# Add ioc_enrichment column to ioc
op.add_column('ioc', sa.Column('ioc_enrichment', JSONB, nullable=True))
def downgrade():
if not _table_has_column('case_assets', 'asset_enrichment'):
# Remove asset_enrichment column from case_assets
op.drop_column('case_assets', 'asset_enrichment')
if _table_has_column('ioc', 'ioc_enrichment'):
# Remove ioc_enrichment column from ioc
op.drop_column('ioc', 'ioc_enrichment')

View File

@ -0,0 +1,43 @@
"""Add objects attributes
Revision ID: 2df770a4989c
Revises: 10a7616f3cc7
Create Date: 2022-02-11 20:13:14.365469
"""
import sqlalchemy as sa
from alembic import op
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = '2df770a4989c'
down_revision = '10a7616f3cc7'
branch_labels = None
depends_on = None
def upgrade():
tables = ['ioc', 'case_assets', 'case_received_file', 'case_tasks', 'notes', 'cases_events', 'cases', 'client']
for table in tables:
if not _table_has_column(table, 'custom_attributes'):
op.add_column(table,
sa.Column('custom_attributes', sa.JSON)
)
t_ua = sa.Table(
table,
sa.MetaData(),
sa.Column('custom_attributes', sa.JSON)
)
conn = op.get_bind()
conn.execute(t_ua.update().values(
custom_attributes={}
))
pass
def downgrade():
pass

View File

@ -0,0 +1,31 @@
"""Add event flag
Revision ID: 3204e9116233
Revises: 11e066542a88
Create Date: 2022-10-02 13:44:36.996070
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
from app.alembic.alembic_utils import _table_has_column
revision = '3204e9116233'
down_revision = '11e066542a88'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('cases_events', 'event_is_flagged'):
op.add_column('cases_events',
sa.Column('event_is_flagged', sa.Boolean, default=False)
)
pass
def downgrade():
pass

View File

@ -0,0 +1,28 @@
"""Rename opened to open
Revision ID: 3a4d4f15bd69
Revises: 65168cb6cc90
Create Date: 2023-10-05 11:36:45.246779
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3a4d4f15bd69'
down_revision = '65168cb6cc90'
branch_labels = None
depends_on = None
def upgrade():
op.execute(
"UPDATE case_state SET state_name='Open' WHERE state_name='Opened'"
)
def downgrade():
op.execute(
"UPDATE case_state SET state_name='Opened' WHERE state_name='Open'"
)

View File

@ -0,0 +1,51 @@
"""Add compromise status to assets
Revision ID: 4ecdfcb34f7c
Revises: a929ef458490
Create Date: 2022-11-26 17:06:33.061363
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
from app.alembic.alembic_utils import _table_has_column
from app.models import CompromiseStatus
revision = '4ecdfcb34f7c'
down_revision = 'a929ef458490'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('case_assets', 'asset_compromise_status_id'):
op.add_column('case_assets',
sa.Column('asset_compromise_status_id',
sa.Integer(),
nullable=True))
# Set schema and make migration of data
t_assets = sa.Table(
'case_assets',
sa.MetaData(),
sa.Column('asset_id', sa.BigInteger, primary_key=True),
sa.Column('asset_compromise_status_id', sa.Integer, nullable=True),
sa.Column('asset_compromised', sa.Boolean, nullable=True)
)
conn = op.get_bind()
conn.execute(t_assets.update().values(
asset_compromise_status_id=CompromiseStatus.compromised.value
).where(t_assets.c.asset_compromised == True))
conn.execute(t_assets.update().values(
asset_compromise_status_id=CompromiseStatus.not_compromised.value
).where(t_assets.c.asset_compromised == False))
op.drop_column('case_assets', 'asset_compromised')
pass
def downgrade():
pass

View File

@ -0,0 +1,75 @@
"""Add uniqueness to Tags table
Revision ID: 50f28953a485
Revises: c959c298ca00
Create Date: 2023-04-06 16:17:40.043545
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy import text
# revision identifiers, used by Alembic.
revision = '50f28953a485'
down_revision = 'c959c298ca00'
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
# Update the CaseTags table to point to the first tag with the same title
conn.execute(text("""
WITH duplicates AS (
SELECT
MIN(id) as min_id,
tag_title
FROM
tags
GROUP BY
tag_title
HAVING
COUNT(*) > 1
),
duplicate_tags AS (
SELECT
id,
tag_title
FROM
tags
WHERE
tag_title IN (SELECT tag_title FROM duplicates)
)
UPDATE
case_tags
SET
tag_id = duplicates.min_id
FROM
duplicates,
duplicate_tags
WHERE
case_tags.tag_id = duplicate_tags.id
AND duplicate_tags.tag_title = duplicates.tag_title
AND duplicate_tags.id <> duplicates.min_id;
"""))
# Remove duplicates in the tags table
conn.execute(text("""
DELETE FROM tags
WHERE id IN (
SELECT id FROM (
SELECT id, ROW_NUMBER()
OVER (PARTITION BY tag_title ORDER BY id) AS rnum
FROM tags) t
WHERE t.rnum > 1);
"""))
# Add the unique constraint to the tag_title column
op.create_unique_constraint(None, 'tags', ['tag_title'])
pass
def downgrade():
pass

View File

@ -0,0 +1,39 @@
"""Reviewer in case
Revision ID: 65168cb6cc90
Revises: e33dd011fb87
Create Date: 2023-07-09 09:01:39.243870
"""
from alembic import op
import sqlalchemy as sa
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = '65168cb6cc90'
down_revision = 'e33dd011fb87'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('cases', 'reviewer_id'):
op.add_column('cases',
sa.Column('reviewer_id', sa.Integer(), nullable=True)
)
op.create_foreign_key('fkey_cases_reviewer_id', 'cases', 'user', ['reviewer_id'], ['id'])
if not _table_has_column('cases', 'review_status_id'):
op.add_column('cases',
sa.Column('review_status_id', sa.Integer(), nullable=True)
)
op.create_foreign_key('fkey_cases_review_status_id', 'cases', 'review_status', ['review_status_id'], ['id'])
pass
def downgrade():
pass

View File

@ -0,0 +1,79 @@
"""Add IOC type
Revision ID: 6a3b3b627d45
Revises:
Create Date: 2022-01-01 23:40:35.283005
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
from app.alembic.alembic_utils import _table_has_column
revision = '6a3b3b627d45'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# IOC types is created by post init if not existing
# Now issue changes on existing tables and migrate IOC types
# Add column ioc_type_id to IOC if not existing
if not _table_has_column('ioc', 'ioc_type_id'):
op.add_column('ioc',
sa.Column('ioc_type_id', sa.Integer, sa.ForeignKey('ioc_type.type_id'))
)
# Add the foreign key of ioc_type to ioc
op.create_foreign_key(
constraint_name='ioc_ioc_type_id',
source_table="ioc",
referent_table="ioc_type",
local_cols=["ioc_type_id"],
remote_cols=["type_id"])
if _table_has_column('ioc', 'ioc_type'):
# Set schema and make migration of data
t_ioc = sa.Table(
'ioc',
sa.MetaData(),
sa.Column('ioc_id', sa.Integer, primary_key=True),
sa.Column('ioc_value', sa.Text),
sa.Column('ioc_type', sa.Unicode(length=50)),
sa.Column('ioc_type_id', sa.ForeignKey('ioc_type.type_id')),
sa.Column('ioc_tags', sa.Text),
sa.Column('user_id', sa.ForeignKey('user.id')),
sa.Column('ioc_misp', sa.Text),
sa.Column('ioc_tlp_id', sa.ForeignKey('tlp.tlp_id'))
)
to_update = [('Domain', 'domain'), ('IP', 'ip-any'), ('Hash', 'other'), ('File', 'filename'),
('Path', 'file-path'), ('Account', 'account'), ("Other", 'other')]
# Migrate existing IOCs
for src_up, dst_up in to_update:
conn = op.get_bind()
res = conn.execute(f"select ioc_id from ioc where ioc_type = '{src_up}';")
results = res.fetchall()
res = conn.execute(f"select type_id from ioc_type where type_name = '{dst_up}';")
e_info = res.fetchall()
if e_info:
domain_id = e_info[0][0]
for res in results:
conn.execute(t_ioc.update().where(t_ioc.c.ioc_id == res[0]).values(
ioc_type_id=domain_id
))
op.drop_column(
table_name='ioc',
column_name='ioc_type'
)
pass
def downgrade():
pass

View File

@ -0,0 +1,58 @@
"""Add server settings updates info
Revision ID: 79a9a54e8f9d
Revises: ff917e2ab02e
Create Date: 2022-05-05 18:39:19.027828
"""
import sqlalchemy as sa
from alembic import op
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = '79a9a54e8f9d'
down_revision = 'ff917e2ab02e'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('server_settings', 'has_updates_available'):
op.add_column('server_settings',
sa.Column('has_updates_available', sa.Boolean)
)
t_ua = sa.Table(
'server_settings',
sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('has_updates_available', sa.Boolean)
)
conn = op.get_bind()
conn.execute(t_ua.update().values(
has_updates_available=False
))
if not _table_has_column('server_settings', 'enable_updates_check'):
op.add_column('server_settings',
sa.Column('enable_updates_check', sa.Boolean)
)
t_ua = sa.Table(
'server_settings',
sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('enable_updates_check', sa.Boolean)
)
conn = op.get_bind()
conn.execute(t_ua.update().values(
enable_updates_check=True
))
pass
def downgrade():
pass

View File

@ -0,0 +1,28 @@
"""Migrate user int to big int
Revision ID: 7cc588444b79
Revises: 92ecbf0f6d10
Create Date: 2022-06-14 08:28:59.027411
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7cc588444b79'
down_revision = '92ecbf0f6d10'
branch_labels = None
depends_on = None
def upgrade():
op.alter_column('user', 'id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
pass
def downgrade():
pass

View File

@ -0,0 +1,44 @@
"""Add task log api field
Revision ID: 874ba5e5da44
Revises: c773a35c280f
Create Date: 2022-02-03 16:22:37.506019
"""
import sqlalchemy as sa
from alembic import op
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = '874ba5e5da44'
down_revision = 'c773a35c280f'
branch_labels = None
depends_on = None
def upgrade():
# Issue changes on existing user activities table and migrate existing rows
# Add column is_from_api to user_activities if not existing and set existing ones to false
if not _table_has_column('user_activity', 'is_from_api'):
op.add_column('user_activity',
sa.Column('is_from_api', sa.Boolean)
)
t_ua = sa.Table(
'user_activity',
sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('is_from_api', sa.Boolean)
)
conn = op.get_bind()
conn.execute(t_ua.update().values(
is_from_api=False
))
pass
def downgrade():
pass

View File

@ -0,0 +1,46 @@
"""Modifying case tasks to remove assignee id for instead, adding a table named task_assignee
Revision ID: 875edc4adb40
Revises: fcc375ed37d1
Create Date: 2022-07-17 14:57:22.809977
"""
from alembic import op
from app.alembic.alembic_utils import _has_table
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = '875edc4adb40'
down_revision = 'fcc375ed37d1'
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
# Get all users with their roles
if _has_table("case_tasks"):
if _table_has_column("case_tasks", "task_assignee_id"):
res = conn.execute(f"select id, task_assignee_id from case_tasks")
results_tasks = res.fetchall()
for task in results_tasks:
task_id = task[0]
user_id = task[1]
if not user_id:
user_id = 1
# Migrate assignees to task_assignee
conn.execute(f"insert into task_assignee (user_id, task_id) values ({user_id}, {task_id}) "
f"on conflict do nothing;")
op.drop_column(
table_name='case_tasks',
column_name='task_assignee_id'
)
def downgrade():
pass

View File

@ -0,0 +1,42 @@
"""Add user external ID
Revision ID: 92ecbf0f6d10
Revises: cd519d2d24df
Create Date: 2022-06-13 08:59:04.860887
"""
import sqlalchemy as sa
from alembic import op
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = '92ecbf0f6d10'
down_revision = 'cd519d2d24df'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('user', 'external_id'):
op.add_column('user',
sa.Column('external_id', sa.Text)
)
t_ua = sa.Table(
'user',
sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('external_id', sa.Text)
)
conn = op.get_bind()
conn.execute(t_ua.update().values(
external_id=None
))
pass
def downgrade():
pass

View File

@ -0,0 +1,34 @@
"""Add cases status
Revision ID: a3eb60654ec4
Revises: 3204e9116233
Create Date: 2022-11-10 07:52:22.502834
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy import Integer
from sqlalchemy import text
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = 'a3eb60654ec4'
down_revision = '3204e9116233'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('cases', 'status_id'):
op.add_column('cases',
sa.Column('status_id', Integer, server_default=text("0"),
nullable=False)
)
pass
def downgrade():
pass

View File

@ -0,0 +1,42 @@
"""Add activity no display field
Revision ID: a929ef458490
Revises: 1df4adfa3160
Create Date: 2022-11-21 15:26:49.088050
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
from app.alembic.alembic_utils import _table_has_column
revision = 'a929ef458490'
down_revision = '1df4adfa3160'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('user_activity', 'display_in_ui'):
op.add_column('user_activity',
sa.Column('display_in_ui', sa.Boolean, default=True)
)
t_ua = sa.Table(
'user_activity',
sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('display_in_ui', sa.Boolean)
)
conn = op.get_bind()
conn.execute(t_ua.update().values(
display_in_ui=True
))
pass
def downgrade():
pass

View File

@ -0,0 +1,96 @@
"""Add IocType validation
Revision ID: ad4e0cd17597
Revises: cd519d2d24df
Create Date: 2022-08-04 15:37:44.484997
"""
import sqlalchemy as sa
from alembic import op
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = 'ad4e0cd17597'
down_revision = '875edc4adb40'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('ioc_type', 'type_validation_regex'):
op.add_column('ioc_type',
sa.Column('type_validation_regex', sa.Text)
)
if not _table_has_column('ioc_type', 'type_validation_expect'):
op.add_column('ioc_type',
sa.Column('type_validation_expect', sa.Text)
)
# Migrate known existing rows if any
migration_map = {
"authentihash": "[a-f0-9]{64}",
"filename|authentihash": ".+\|[a-f0-9]{64}",
"filename|imphash": ".+\|[a-f0-9]{32}",
"filename|md5": ".+\|[a-f0-9]{32}",
"filename|pehash": ".+\|[a-f0-9]{40}",
"filename|sha1": ".+\|[a-f0-9]{40}",
"filename|sha224": ".+\|[a-f0-9]{56}",
"filename|sha256": ".+\|[a-f0-9]{64}",
"filename|sha3-224": ".+\|[a-f0-9]{56}",
"filename|sha3-256": ".+\|[a-f0-9]{64}",
"filename|sha3-384": ".+\|[a-f0-9]{96}",
"filename|sha3-512": ".+\|[a-f0-9]{128}",
"filename|sha384": ".+\|[a-f0-9]{96}",
"filename|sha512": ".+\|[a-f0-9]{128}",
"filename|sha512/224": ".+\|[a-f0-9]{56}",
"filename|sha512/256": ".+\|[a-f0-9]{64}",
"filename|tlsh": ".+\|t?[a-f0-9]{35,}",
"git-commit-id": "[a-f0-9]{40}",
"hassh-md5": "[a-f0-9]{32}",
"hasshserver-md5": "[a-f0-9]{32}",
"imphash": "[a-f0-9]{32}",
"ja3-fingerprint-md5": "[a-f0-9]{32}",
"jarm-fingerprint": "[a-f0-9]{62}",
"md5": "[a-f0-9]{32}",
"pehash": "[a-f0-9]{40}",
"sha1": "[a-f0-9]{40}",
"sha224": "[a-f0-9]{56}",
"sha256": "[a-f0-9]{64}",
"sha3-224": "[a-f0-9]{56}",
"sha3-256": "[a-f0-9]{64}",
"sha3-384": "[a-f0-9]{96}",
"sha3-512": "[a-f0-9]{128}",
"sha384": "[a-f0-9]{96}",
"sha512": "[a-f0-9]{128}",
"sha512/224": "[a-f0-9]{56}",
"sha512/256": "[a-f0-9]{64}",
"telfhash": "[a-f0-9]{70}",
"tlsh": "^t?[a-f0-9]{35,}",
"x509-fingerprint-md5": "[a-f0-9]{32}",
"x509-fingerprint-sha1": "[a-f0-9]{40}",
"x509-fingerprint-sha256": "[a-f0-9]{64}"
}
t_tasks = sa.Table(
'ioc_type',
sa.MetaData(),
sa.Column('type_id', sa.Integer, primary_key=True),
sa.Column('type_name', sa.Text),
sa.Column('type_validation_regex', sa.Text),
sa.Column('type_validation_expect', sa.Text),
)
conn = op.get_bind()
for type_name in migration_map:
conn.execute(t_tasks.update().where(t_tasks.c.type_name == type_name).values(
type_validation_regex=migration_map[type_name]
))
def downgrade():
op.drop_column('ioc_type', 'type_validation_regex')
op.drop_column('ioc_type', 'type_validation_expect')

View File

@ -0,0 +1,42 @@
"""Add dark mode
Revision ID: b664ca1203a4
Revises: 2df770a4989c
Create Date: 2022-03-06 18:00:46.251407
"""
import sqlalchemy as sa
from alembic import op
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = 'b664ca1203a4'
down_revision = '2df770a4989c'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('user', 'in_dark_mode'):
op.add_column('user',
sa.Column('in_dark_mode', sa.Boolean)
)
t_ua = sa.Table(
'user',
sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('in_dark_mode', sa.Boolean)
)
conn = op.get_bind()
conn.execute(t_ua.update().values(
in_dark_mode=False
))
pass
def downgrade():
pass

View File

@ -0,0 +1,118 @@
"""Update tasks status
Revision ID: c773a35c280f
Revises: 0db700644a4f
Create Date: 2022-01-18 07:51:43.714021
"""
import sqlalchemy as sa
from alembic import op
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = 'c773a35c280f'
down_revision = '0db700644a4f'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('case_tasks', 'task_status_id'):
op.add_column('case_tasks',
sa.Column('task_status_id', sa.Integer, sa.ForeignKey('task_status.id'))
)
# Add the foreign key of ioc_type to ioc
op.create_foreign_key(
constraint_name='task_task_status_id',
source_table="case_tasks",
referent_table="task_status",
local_cols=["task_status_id"],
remote_cols=["id"])
if _table_has_column('case_tasks', 'task_status'):
# Set schema and make migration of data
t_tasks = sa.Table(
'case_tasks',
sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('task_title', sa.Text),
sa.Column('task_status', sa.Text),
sa.Column('task_status_id', sa.ForeignKey('task_status.id')),
)
to_update = ['To do', 'In progress', 'On hold', 'Done', 'Canceled']
# Migrate existing IOCs
for update in to_update:
conn = op.get_bind()
res = conn.execute(f"select id from case_tasks where task_status = '{update}';")
results = res.fetchall()
res = conn.execute(f"select id from task_status where status_name = '{update}';")
e_info = res.fetchall()
if e_info:
status_id = e_info[0][0]
for res in results:
conn.execute(t_tasks.update().where(t_tasks.c.id == res[0]).values(
task_status_id=status_id
))
op.drop_column(
table_name='case_tasks',
column_name='task_status'
)
if not _table_has_column('global_tasks', 'task_status_id'):
op.add_column('global_tasks',
sa.Column('task_status_id', sa.Integer, sa.ForeignKey('task_status.id'))
)
# Add the foreign key of ioc_type to ioc
op.create_foreign_key(
constraint_name='global_task_status_id',
source_table="global_tasks",
referent_table="task_status",
local_cols=["task_status_id"],
remote_cols=["id"])
if _table_has_column('global_tasks', 'task_status'):
# Set schema and make migration of data
tg_tasks = sa.Table(
'global_tasks',
sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('task_title', sa.Text),
sa.Column('task_status', sa.Text),
sa.Column('task_status_id', sa.ForeignKey('task_status.id')),
)
to_update = ['To do', 'In progress', 'On hold', 'Done', 'Canceled']
# Migrate existing IOCs
for update in to_update:
conn = op.get_bind()
res = conn.execute(f"select id from global_tasks where task_status = '{update}';")
results = res.fetchall()
res = conn.execute(f"select id from task_status where status_name = '{update}';")
e_info = res.fetchall()
if e_info:
status_id = e_info[0][0]
for res in results:
conn.execute(tg_tasks.update().where(tg_tasks.c.id == res[0]).values(
task_status_id=status_id
))
op.drop_column(
table_name='global_tasks',
column_name='task_status'
)
pass
def downgrade():
pass

View File

@ -0,0 +1,27 @@
"""Evidence file_size int to bigint
Revision ID: c832bd69f827
Revises: b664ca1203a4
Create Date: 2022-04-11 21:49:30.739817
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = 'c832bd69f827'
down_revision = 'b664ca1203a4'
branch_labels = None
depends_on = None
def upgrade():
op.alter_column('case_received_file', 'file_size',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
pass
def downgrade():
pass

View File

@ -0,0 +1,127 @@
"""Add classification, history, closing note and initial date
Revision ID: c959c298ca00
Revises: 4ecdfcb34f7c
Create Date: 2023-03-03 23:49:16.360494
"""
import logging
from alembic import op
import sqlalchemy as sa
from sqlalchemy import text
from app.alembic.alembic_utils import _table_has_column, _has_table
# revision identifiers, used by Alembic.
revision = 'c959c298ca00'
down_revision = '4ecdfcb34f7c'
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
cases_table = sa.Table(
'cases',
sa.MetaData(),
sa.Column('case_id', sa.Integer, primary_key=True),
sa.Column('open_date', sa.DateTime, nullable=False),
sa.Column('initial_date', sa.DateTime, nullable=False),
sa.Column('user_id', sa.Integer, sa.ForeignKey('user.id'), nullable=False),
sa.Column('owner_id', sa.Integer, sa.ForeignKey('user.id'), nullable=False),
sa.Column('classification_id', sa.Integer, sa.ForeignKey('case_classification.id'), nullable=False)
)
res = conn.execute(f"select case_id, open_date, user_id from \"cases\";")
results = res.fetchall()
ras = conn.execute(f"select id from \"user\" ORDER BY id ASC LIMIT 1;")
user = ras.fetchone()
if not _table_has_column('cases', 'modification_history'):
op.add_column('cases',
sa.Column('modification_history', sa.JSON)
)
if not _table_has_column('cases', 'initial_date'):
op.add_column('cases',
sa.Column('initial_date', sa.DateTime, nullable=False, server_default=sa.text("now()"))
)
for case in results:
conn.execute(cases_table.update().where(cases_table.c.case_id == case[0]).values(
initial_date=case[1]
))
if not _table_has_column('cases', 'closing_note'):
op.add_column('cases',
sa.Column('closing_note', sa.Text)
)
if not _has_table('case_classification'):
op.create_table('case_classification',
sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('name', sa.Text),
sa.Column('name_expanded', sa.Text),
sa.Column('creation_date', sa.DateTime, server_default=text("now()"), nullable=True),
sa.Column('created_by', sa.ForeignKey('user.id'), nullable=True)
)
op.create_foreign_key('fk_case_classification_user_id', 'case_classification', 'user', ['created_by'], ['id'])
if not _table_has_column('cases', 'classification_id'):
classification_table = sa.Table(
'case_classification',
sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('name', sa.Text),
sa.Column('name_expanded', sa.Text),
sa.Column('creation_date', sa.DateTime, server_default=text("now()"), nullable=True),
sa.Column('created_by', sa.ForeignKey('user.id'), nullable=True),
keep_existing=True
)
other_classification = sa.select([classification_table.c.id]).where(
classification_table.c.name == 'other:other')
if conn.execute(other_classification).fetchone() is None:
# Create other:other for migration - the rest of the data will be handled by post init
op.execute(f"insert into case_classification (name, name_expanded, description, created_by_id) "
f"values ('other:other', 'Other: Other', 'All incidents that do not fit in one of the given "
f"categories should be put into this class. If the number of incidents in this category "
f"increases, it is an indicator that the classification scheme must be revised.', {user[0]});")
other_classification = sa.select([classification_table.c.id]).where(
classification_table.c.name == 'other:other')
other_classification_id = conn.execute(other_classification).fetchone()[0]
else:
other_classification_id = conn.execute(other_classification).fetchone()[0]
op.add_column('cases',
sa.Column('classification_id', sa.Integer, sa.ForeignKey('case_classification.id'),
server_default=text(str(other_classification_id))),
)
cid_list = [c[0] for c in results]
op.execute(cases_table.update().where(cases_table.c.case_id.in_(cid_list)).values(
classification_id=other_classification_id
))
if not _table_has_column('cases', 'owner_id'):
op.add_column('cases',
sa.Column('owner_id', sa.Integer, sa.ForeignKey('user.id'),
server_default=text("1")),
)
for case in results:
conn.execute(cases_table.update().where(cases_table.c.case_id == case[0]).values(
owner_id=case[2]
))
def downgrade():
pass

View File

@ -0,0 +1,30 @@
"""Add created by in events
Revision ID: ca93d4b54571
Revises: 79a9a54e8f9d
Create Date: 2022-05-08 14:58:38.839651
"""
import sqlalchemy as sa
from alembic import op
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = 'ca93d4b54571'
down_revision = '79a9a54e8f9d'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('cases_events', 'modification_history'):
op.add_column('cases_events',
sa.Column('modification_history', sa.JSON)
)
pass
def downgrade():
pass

View File

@ -0,0 +1,60 @@
"""Password policy edition
Revision ID: cd519d2d24df
Revises: ca93d4b54571
Create Date: 2022-05-25 18:09:08.741619
"""
import sqlalchemy as sa
from alembic import op
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = 'cd519d2d24df'
down_revision = 'ca93d4b54571'
branch_labels = None
depends_on = None
def upgrade():
columns = {
"password_policy_min_length": sa.Integer,
"password_policy_upper_case": sa.Boolean,
"password_policy_lower_case": sa.Boolean,
"password_policy_digit": sa.Boolean,
"password_policy_special_chars": sa.Text,
}
for col in columns:
if not _table_has_column('server_settings', col):
op.add_column('server_settings',
sa.Column(col, columns[col])
)
t_ua = sa.Table(
'server_settings',
sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('password_policy_min_length', sa.Integer),
sa.Column('password_policy_upper_case', sa.Boolean),
sa.Column('password_policy_lower_case', sa.Boolean),
sa.Column('password_policy_digit', sa.Boolean),
sa.Column('password_policy_special_chars', sa.Text)
)
conn = op.get_bind()
conn.execute(t_ua.update().values(
password_policy_min_length=12,
password_policy_upper_case=True,
password_policy_lower_case=True,
password_policy_digit=True,
password_policy_special_chars=''
))
pass
def downgrade():
pass

View File

@ -0,0 +1,39 @@
"""Add service account and minibar in users
Revision ID: db93d5c4c0aa
Revises: 2a4a8330b908
Create Date: 2023-04-26 14:14:47.990230
"""
from alembic import op
import sqlalchemy as sa
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = 'db93d5c4c0aa'
down_revision = '2a4a8330b908'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('user', 'is_service_account'):
op.add_column('user',
sa.Column('is_service_account', sa.Boolean, default=False))
if not _table_has_column('user', 'has_mini_sidebar'):
op.add_column('user',
sa.Column('has_mini_sidebar', sa.Boolean, default=False))
pass
def downgrade():
if _table_has_column('user', 'is_service_account'):
op.drop_column('user', 'is_service_account')
if _table_has_column('user', 'has_mini_sidebar'):
op.drop_column('user', 'has_mini_sidebar')
pass

View File

@ -0,0 +1,30 @@
"""resolution status in alerts
Revision ID: e33dd011fb87
Revises: 00b43bc4e8ac
Create Date: 2023-07-03 13:28:08.882759
"""
from alembic import op
import sqlalchemy as sa
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = 'e33dd011fb87'
down_revision = '00b43bc4e8ac'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('alerts', 'alert_resolution_status_id'):
op.add_column('alerts', sa.Column('alert_resolution_status_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'alerts', 'alert_resolution_status',
['alert_resolution_status_id'], ['resolution_status_id'])
pass
def downgrade():
pass

View File

@ -0,0 +1,35 @@
"""Add alert in comments
Revision ID: f727badcc4e1
Revises: 50f28953a485
Create Date: 2023-04-12 09:28:58.993723
"""
from alembic import op
import sqlalchemy as sa
from app.alembic.alembic_utils import _table_has_column
# revision identifiers, used by Alembic.
revision = 'f727badcc4e1'
down_revision = '50f28953a485'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column("comments", "comment_alert_id"):
op.add_column('comments',
sa.Column('comment_alert_id',
sa.BigInteger(), nullable=True)
)
op.create_foreign_key(None,
'comments', 'alerts',
['comment_alert_id'], ['alert_id'])
def downgrade():
op.drop_constraint(None, 'comments', type_='foreignkey')
op.drop_column('comments', 'comment_alert_id')
pass

View File

@ -0,0 +1,232 @@
"""Access control migration
Revision ID: fcc375ed37d1
Revises: 7cc588444b79
Create Date: 2022-06-14 17:01:29.205520
"""
import uuid
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects.postgresql import UUID
from app.alembic.alembic_utils import _has_table
# revision identifiers, used by Alembic.
from app.alembic.alembic_utils import _table_has_column
from app.iris_engine.access_control.utils import ac_get_mask_analyst
from app.iris_engine.access_control.utils import ac_get_mask_case_access_level_full
from app.iris_engine.access_control.utils import ac_get_mask_full_permissions
revision = 'fcc375ed37d1'
down_revision = '7cc588444b79'
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
# Add UUID to users
if not _table_has_column('user', 'uuid'):
op.add_column('user',
sa.Column('uuid', UUID(as_uuid=True), default=uuid.uuid4, nullable=False,
server_default=sa.text('gen_random_uuid()'))
)
# Add UUID to existing users
t_users = sa.Table(
'user',
sa.MetaData(),
sa.Column('id', sa.BigInteger(), primary_key=True),
sa.Column('uuid', UUID(as_uuid=True), default=uuid.uuid4, nullable=False)
)
res = conn.execute(f"select id from \"user\";")
results = res.fetchall()
for user in results:
conn.execute(t_users.update().where(t_users.c.id == user[0]).values(
uuid=uuid.uuid4()
))
# Add all the new access control tables if they don't exist
if not _has_table('user_case_access'):
op.create_table('user_case_access',
sa.Column('id', sa.BigInteger(), primary_key=True, nullable=False),
sa.Column('user_id', sa.BigInteger(), sa.ForeignKey('user.id'), nullable=False),
sa.Column('case_id', sa.BigInteger(), sa.ForeignKey('cases.case_id'), nullable=False),
sa.Column('access_level', sa.BigInteger()),
keep_existing=True
)
op.create_foreign_key('fk_user_case_access_user_id', 'user_case_access', 'user', ['user_id'], ['id'])
op.create_foreign_key('fk_user_case_access_case_id', 'user_case_access', 'cases', ['case_id'], ['case_id'])
op.create_unique_constraint('uq_user_case_access_user_id_case_id', 'user_case_access', ['user_id', 'case_id'])
if not _has_table('user_case_effective_access'):
op.create_table('user_case_effective_access',
sa.Column('id', sa.BigInteger(), primary_key=True, nullable=False),
sa.Column('user_id', sa.BigInteger(), sa.ForeignKey('user.id'), nullable=False),
sa.Column('case_id', sa.BigInteger(), sa.ForeignKey('cases.case_id'), nullable=False),
sa.Column('access_level', sa.BigInteger()),
keep_existing=True
)
op.create_foreign_key('fk_user_case_effective_access_user_id', 'user_case_effective_access',
'user', ['user_id'], ['id'])
op.create_foreign_key('fk_user_case_effective_access_case_id', 'user_case_effective_access',
'cases', ['case_id'], ['case_id'])
op.create_unique_constraint('uq_user_case_effective_access_user_id_case_id',
'user_case_access', ['user_id', 'case_id'])
if not _has_table('group_case_access'):
op.create_table('group_case_access',
sa.Column('id', sa.BigInteger(), primary_key=True, nullable=False),
sa.Column('group_id', sa.BigInteger(), sa.ForeignKey('groups.group_id'), nullable=False),
sa.Column('case_id', sa.BigInteger(), sa.ForeignKey('cases.case_id'), nullable=False),
sa.Column('access_level', sa.BigInteger(), nullable=False),
keep_existing=True
)
op.create_foreign_key('group_case_access_group_id_fkey', 'group_case_access', 'groups',
['group_id'], ['group_id'])
op.create_foreign_key('group_case_access_case_id_fkey', 'group_case_access', 'cases',
['case_id'], ['case_id'])
op.create_unique_constraint('group_case_access_unique', 'group_case_access', ['group_id', 'case_id'])
if not _has_table('groups'):
op.create_table('groups',
sa.Column('group_id', sa.BigInteger(), primary_key=True, nullable=False),
sa.Column('group_uuid', UUID(as_uuid=True), default=uuid.uuid4, nullable=False,
server_default=sa.text('gen_random_uuid()'), unique=True),
sa.Column('group_name', sa.Text(), nullable=False),
sa.Column('group_description', sa.Text(), nullable=False),
sa.Column('group_permissions', sa.BigInteger(), nullable=False),
sa.Column('group_auto_follow', sa.Boolean(), nullable=False, default=False),
sa.Column('group_auto_follow_access_level', sa.BigInteger(), nullable=True),
keep_existing=True
)
op.create_unique_constraint('groups_group_name_unique', 'groups', ['group_name'])
if not _has_table('organisations'):
op.create_table('organisations',
sa.Column('org_id', sa.BigInteger(), primary_key=True, nullable=False),
sa.Column('org_uuid', UUID(as_uuid=True), default=uuid.uuid4(), nullable=False,
server_default=sa.text('gen_random_uuid()'), unique=True),
sa.Column('org_name', sa.Text(), nullable=False),
sa.Column('org_description', sa.Text(), nullable=False),
sa.Column('org_url', sa.Text(), nullable=False),
sa.Column('org_email', sa.Text(), nullable=False),
sa.Column('org_logo', sa.Text(), nullable=False),
sa.Column('org_type', sa.Text(), nullable=False),
sa.Column('org_sector', sa.Text(), nullable=False),
sa.Column('org_nationality', sa.Text(), nullable=False),
keep_existing=True
)
op.create_unique_constraint('organisation_name_unique', 'organisations', ['org_name'])
if not _has_table('organisation_case_access'):
op.create_table('organisation_case_access',
sa.Column('id', sa.BigInteger(), primary_key=True, nullable=False),
sa.Column('org_id', sa.BigInteger(), sa.ForeignKey('organisations.org_id'), nullable=False),
sa.Column('case_id', sa.BigInteger(), sa.ForeignKey('cases.case_id'), nullable=False),
sa.Column('access_level', sa.BigInteger(), nullable=False),
keep_existing=True
)
op.create_foreign_key('organisation_case_access_org_id_fkey', 'organisation_case_access',
'organisations', ['org_id'], ['org_id'])
op.create_foreign_key('organisation_case_access_case_id_fkey', 'organisation_case_access', 'cases',
['case_id'], ['case_id'])
op.create_unique_constraint('organisation_case_access_unique', 'organisation_case_access',
['org_id', 'case_id'])
if not _has_table('user_organisation'):
op.create_table('user_organisation',
sa.Column('id', sa.BigInteger(), primary_key=True, nullable=False),
sa.Column('user_id', sa.BigInteger(), sa.ForeignKey('user.id'), nullable=False),
sa.Column('org_id', sa.BigInteger(), sa.ForeignKey('organisations.org_id'), nullable=False),
sa.Column('is_primary_org', sa.Boolean(), nullable=False),
keep_existing=True
)
op.create_foreign_key('user_organisation_user_id_fkey', 'user_organisation', 'user', ['user_id'], ['id'])
op.create_foreign_key('user_organisation_org_id_fkey', 'user_organisation', 'organisations',
['org_id'], ['org_id'])
op.create_unique_constraint('user_organisation_unique', 'user_organisation', ['user_id', 'org_id'])
if not _has_table('user_group'):
op.create_table('user_group',
sa.Column('id', sa.BigInteger(), primary_key=True, nullable=False),
sa.Column('user_id', sa.BigInteger(), sa.ForeignKey('user.id'), nullable=False),
sa.Column('group_id', sa.BigInteger(), sa.ForeignKey('groups.group_id'), nullable=False),
keep_existing=True
)
op.create_foreign_key('user_group_user_id_fkey', 'user_group', 'user', ['user_id'], ['id'])
op.create_foreign_key('user_group_group_id_fkey', 'user_group', 'groups', ['group_id'], ['group_id'])
op.create_unique_constraint('user_group_unique', 'user_group', ['user_id', 'group_id'])
# Create the groups if they don't exist
res = conn.execute(f"select group_id from groups where group_name = 'Administrators';")
if res.rowcount == 0:
conn.execute(f"insert into groups (group_name, group_description, group_permissions, group_uuid, "
f"group_auto_follow, group_auto_follow_access_level) "
f"values ('Administrators', 'Administrators', '{ac_get_mask_full_permissions()}', '{uuid.uuid4()}',"
f" true, 4);")
res = conn.execute(f"select group_id from groups where group_name = 'Administrators';")
admin_group_id = res.fetchone()[0]
res = conn.execute(f"select group_id from groups where group_name = 'Analysts';")
if res.rowcount == 0:
conn.execute(f"insert into groups (group_name, group_description, group_permissions, group_uuid, "
f"group_auto_follow, group_auto_follow_access_level) "
f"values ('Analysts', 'Standard Analysts', '{ac_get_mask_analyst()}', '{uuid.uuid4()}', true, 4);")
res = conn.execute(f"select group_id from groups where group_name = 'Analysts';")
analyst_group_id = res.fetchone()[0]
# Create the organisations if they don't exist
res = conn.execute(f"select org_id from organisations where org_name = 'Default Org';")
if res.rowcount == 0:
conn.execute(f"insert into organisations (org_name, org_description, org_url, org_email, org_logo, "
f"org_type, org_sector, org_nationality, org_uuid) values ('Default Org', 'Default Organisation', "
f"'', '', "
f"'','', '', '', '{uuid.uuid4()}');")
res = conn.execute(f"select org_id from organisations where org_name = 'Default Org';")
default_org_id = res.fetchone()[0]
# Give the organisation access to all the cases
res = conn.execute(f"select case_id from cases;")
result_cases = [case[0] for case in res.fetchall()]
access_level = ac_get_mask_case_access_level_full()
# Migrate the users to the new access control system
conn = op.get_bind()
# Get all users with their roles
if _has_table("user_roles"):
res = conn.execute(f"select distinct roles.name, \"user\".id from user_roles INNER JOIN \"roles\" ON "
f"\"roles\".id = user_roles.role_id INNER JOIN \"user\" ON \"user\".id = user_roles.user_id;")
results_users = res.fetchall()
for user_id in results_users:
role_name = user_id[0]
user_id = user_id[1]
# Migrate user to groups
if role_name == 'administrator':
conn.execute(f"insert into user_group (user_id, group_id) values ({user_id}, {admin_group_id}) "
f"on conflict do nothing;")
elif role_name == 'investigator':
conn.execute(f"insert into user_group (user_id, group_id) values ({user_id}, {analyst_group_id}) "
f"on conflict do nothing;")
# Add user to default organisation
conn.execute(f"insert into user_organisation (user_id, org_id, is_primary_org) values ({user_id}, "
f"{default_org_id}, true) on conflict do nothing;")
# Add default cases effective permissions
for case_id in result_cases:
conn.execute(f"insert into user_case_effective_access (case_id, user_id, access_level) values "
f"({case_id}, {user_id}, {access_level}) on conflict do nothing;")
op.drop_table('user_roles')
pass
def downgrade():
pass

View File

@ -0,0 +1,85 @@
"""changed the assets_type table for custom icons
Revision ID: ff917e2ab02e
Revises: c832bd69f827
Create Date: 2022-04-21 22:14:55.815983
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
from app.alembic.alembic_utils import _table_has_column
revision = 'ff917e2ab02e'
down_revision = 'c832bd69f827'
branch_labels = None
depends_on = None
def upgrade():
if not _table_has_column('assets_type', 'asset_icon_not_compromised'):
op.add_column('assets_type',
sa.Column('asset_icon_not_compromised', sa.String(255))
)
if not _table_has_column('assets_type', 'asset_icon_compromised'):
op.add_column('assets_type',
sa.Column('asset_icon_compromised', sa.String(255))
)
t_assets_type = sa.Table(
'assets_type',
sa.MetaData(),
sa.Column('asset_id', sa.Integer, primary_key=True),
sa.Column('asset_name', sa.String(155)),
sa.Column('asset_icon_not_compromised', sa.String(255)),
sa.Column('asset_icon_compromised', sa.String(255))
)
# Migrate existing Asset_types
conn = op.get_bind()
res = conn.execute("SELECT asset_id, asset_name FROM public.assets_type;")
results = res.fetchall()
if results:
for res in results:
icon_not_compromised, icon_compromised = _get_icons(res[1])
conn.execute(t_assets_type.update().where(t_assets_type.c.asset_id == res[0]).values(
asset_icon_not_compromised=icon_not_compromised,
asset_icon_compromised=icon_compromised
))
def downgrade():
pass
def _get_icons(asset_name):
assets = {
"Account": ("Generic Account", "user.png", "ioc_user.png"),
"Firewall": ("Firewall", "firewall.png", "ioc_firewall.png"),
"Linux - Server": ("Linux server", "server.png", "ioc_server.png"),
"Linux - Computer": ("Linux computer", "desktop.png", "ioc_desktop.png"),
"Linux Account": ("Linux Account", "user.png", "ioc_user.png"),
"Mac - Computer": ("Mac computer", "desktop.png", "ioc_desktop.png"),
"Phone - Android": ("Android Phone", "phone.png", "ioc_phone.png"),
"Phone - IOS": ("Apple Phone", "phone.png", "ioc_phone.png"),
"Windows - Computer": ("Standard Windows Computer", "windows_desktop.png", "ioc_windows_desktop.png"),
"Windows - Server": ("Standard Windows Server", "windows_server.png", "ioc_windows_server.png"),
"Windows - DC": ("Domain Controller", "windows_server.png", "ioc_windows_server.png"),
"Router": ("Router", "router.png", "ioc_router.png"),
"Switch": ("Switch", "switch.png", "ioc_switch.png"),
"VPN": ("VPN", "vpn.png", "ioc_vpn.png"),
"WAF": ("WAF", "firewall.png", "ioc_firewall.png"),
"Windows Account - Local": ("Windows Account - Local", "user.png", "ioc_user.png"),
"Windows Account - Local - Admin": ("Windows Account - Local - Admin", "user.png", "ioc_user.png"),
"Windows Account - AD": ("Windows Account - AD", "user.png", "ioc_user.png"),
"Windows Account - AD - Admin": ("Windows Account - AD - Admin", "user.png", "ioc_user.png"),
"Windows Account - AD - krbtgt": ("Windows Account - AD - krbtgt", "user.png", "ioc_user.png"),
"Windows Account - AD - Service": ("Windows Account - AD - krbtgt", "user.png", "ioc_user.png")
}
if assets.get(asset_name):
return assets.get(asset_name)[1], assets.get(asset_name)[2]
else:
return "question-mark.png","ioc_question-mark.png"

View File

@ -0,0 +1,82 @@
#!/usr/bin/env python3
#
#
# IRIS Source Code
# Copyright (C) 2021 - Airbus CyberSecurity (SAS)
# ir@cyberactionlab.net
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# IMPORTS ------------------------------------------------
import os
from flask import Blueprint
from flask import redirect
from flask import render_template
from flask import url_for
from flask_wtf import FlaskForm
import app
from app.datamgmt.activities.activities_db import get_all_users_activities
from app.datamgmt.activities.activities_db import get_users_activities
from app.models.authorization import Permissions
from app.util import ac_api_requires
from app.util import ac_requires
from app.util import response_success
activities_blueprint = Blueprint(
'activities',
__name__,
template_folder='templates'
)
basedir = os.path.abspath(os.path.dirname(app.__file__))
# CONTENT ------------------------------------------------
@activities_blueprint.route('/activities', methods=['GET'])
@ac_requires(Permissions.activities_read, Permissions.all_activities_read)
def activities_index(caseid: int, url_redir):
if url_redir:
return redirect(url_for('activities.activities_index', cid=caseid, redirect=True))
form = FlaskForm()
return render_template('activities.html', form=form)
@activities_blueprint.route('/activities/list', methods=['GET'])
@ac_api_requires(Permissions.activities_read, Permissions.all_activities_read)
def list_activities(caseid):
# Get User activities from database
user_activities = get_users_activities()
data = [row._asdict() for row in user_activities]
data = sorted(data, key=lambda i: i['activity_date'], reverse=True)
return response_success("", data=data)
@activities_blueprint.route('/activities/list-all', methods=['GET'])
@ac_api_requires(Permissions.all_activities_read)
def list_all_activities(caseid):
# Get User activities from database
user_activities = get_all_users_activities()
data = [row._asdict() for row in user_activities]
data = sorted(data, key=lambda i: i['activity_date'], reverse=True)
return response_success("", data=data)

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,50 @@
#!/usr/bin/env python3
#
# IRIS Source Code
# Copyright (C) 2021 - Airbus CyberSecurity (SAS)
# ir@cyberactionlab.net
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from flask import Blueprint
from app import app
from app.util import ac_api_requires
from app.util import response_success
api_blueprint = Blueprint(
'api',
__name__,
template_folder='templates'
)
# CONTENT ------------------------------------------------
@api_blueprint.route('/api/ping', methods=['GET'])
@ac_api_requires()
def api_ping(caseid):
return response_success("pong")
@api_blueprint.route('/api/versions', methods=['GET'])
@ac_api_requires()
def api_version(caseid):
versions = {
"iris_current": app.config.get('IRIS_VERSION'),
"api_min": app.config.get('API_MIN_VERSION'),
"api_current": app.config.get('API_MAX_VERSION')
}
return response_success(data=versions)

View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
#
# IRIS Source Code
# Copyright (C) 2021 - Airbus CyberSecurity (SAS)
# ir@cyberactionlab.net
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# IMPORTS ------------------------------------------------
# VARS ---------------------------------------------------
# CONTENT ------------------------------------------------

View File

@ -0,0 +1,503 @@
#!/usr/bin/env python3
#
# IRIS Source Code
# Copyright (C) 2021 - Airbus CyberSecurity (SAS) - DFIR-IRIS Team
# ir@cyberactionlab.net - contact@dfir-iris.org
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import csv
# IMPORTS ------------------------------------------------
from datetime import datetime
import marshmallow
from flask import Blueprint
from flask import redirect
from flask import render_template
from flask import request
from flask import url_for
from flask_login import current_user
from app import db
from app.blueprints.case.case_comments import case_comment_update
from app.datamgmt.case.case_assets_db import add_comment_to_asset
from app.datamgmt.case.case_assets_db import create_asset
from app.datamgmt.case.case_assets_db import delete_asset
from app.datamgmt.case.case_assets_db import delete_asset_comment
from app.datamgmt.case.case_assets_db import get_analysis_status_list
from app.datamgmt.case.case_assets_db import get_asset
from app.datamgmt.case.case_assets_db import get_asset_type_id
from app.datamgmt.case.case_assets_db import get_assets
from app.datamgmt.case.case_assets_db import get_assets_ioc_links
from app.datamgmt.case.case_assets_db import get_assets_types
from app.datamgmt.case.case_assets_db import get_case_asset_comment
from app.datamgmt.case.case_assets_db import get_case_asset_comments
from app.datamgmt.case.case_assets_db import get_case_assets_comments_count
from app.datamgmt.case.case_assets_db import get_compromise_status_list
from app.datamgmt.case.case_assets_db import get_linked_iocs_finfo_from_asset
from app.datamgmt.case.case_assets_db import get_linked_iocs_id_from_asset
from app.datamgmt.case.case_assets_db import get_similar_assets
from app.datamgmt.case.case_assets_db import set_ioc_links
from app.datamgmt.case.case_db import get_case
from app.datamgmt.case.case_db import get_case_client_id
from app.datamgmt.case.case_iocs_db import get_iocs
from app.datamgmt.manage.manage_attribute_db import get_default_custom_attributes
from app.datamgmt.manage.manage_users_db import get_user_cases_fast
from app.datamgmt.states import get_assets_state
from app.datamgmt.states import update_assets_state
from app.forms import AssetBasicForm
from app.forms import ModalAddCaseAssetForm
from app.iris_engine.module_handler.module_handler import call_modules_hook
from app.iris_engine.utils.tracker import track_activity
from app.models import AnalysisStatus
from app.models.authorization import CaseAccessLevel
from app.schema.marshables import CaseAssetsSchema
from app.schema.marshables import CommentSchema
from app.util import ac_api_case_requires
from app.util import ac_case_requires
from app.util import response_error
from app.util import response_success
case_assets_blueprint = Blueprint('case_assets',
__name__,
template_folder='templates')
@case_assets_blueprint.route('/case/assets', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_assets(caseid, url_redir):
"""
Returns the page of case assets, with the list of available assets types.
:return: The HTML page of case assets
"""
if url_redir:
return redirect(url_for('case_assets.case_assets', cid=caseid, redirect=True))
form = ModalAddCaseAssetForm()
# Get asset types from database
form.asset_id.choices = get_assets_types()
# Retrieve the assets linked to the investigation
case = get_case(caseid)
return render_template("case_assets.html", case=case, form=form)
@case_assets_blueprint.route('/case/assets/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_list_assets(caseid):
"""
Returns the list of assets from the case.
:return: A JSON object containing the assets of the case, enhanced with assets seen on other cases.
"""
# Get all assets objects from the case and the customer id
assets = get_assets(caseid)
customer_id = get_case_client_id(caseid)
ret = {}
ret['assets'] = []
ioc_links_req = get_assets_ioc_links(caseid)
cache_ioc_link = {}
for ioc in ioc_links_req:
if ioc.asset_id not in cache_ioc_link:
cache_ioc_link[ioc.asset_id] = [ioc._asdict()]
else:
cache_ioc_link[ioc.asset_id].append(ioc._asdict())
cases_access = get_user_cases_fast(current_user.id)
for asset in assets:
asset = asset._asdict()
if len(assets) < 300:
# Find similar assets from other cases with the same customer
asset['link'] = list(get_similar_assets(
asset['asset_name'], asset['asset_type_id'], caseid, customer_id, cases_access))
else:
asset['link'] = []
asset['ioc_links'] = cache_ioc_link.get(asset['asset_id'])
ret['assets'].append(asset)
ret['state'] = get_assets_state(caseid=caseid)
return response_success("", data=ret)
@case_assets_blueprint.route('/case/assets/state', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_assets_state(caseid):
os = get_assets_state(caseid=caseid)
if os:
return response_success(data=os)
else:
return response_error('No assets state for this case.')
@case_assets_blueprint.route('/case/assets/add/modal', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def add_asset_modal(caseid):
form = AssetBasicForm()
form.asset_type_id.choices = get_assets_types()
form.analysis_status_id.choices = get_analysis_status_list()
form.asset_compromise_status_id.choices = get_compromise_status_list()
# Get IoCs from the case
ioc = get_iocs(caseid)
attributes = get_default_custom_attributes('asset')
return render_template("modal_add_case_multi_asset.html", form=form, asset=None, ioc=ioc, attributes=attributes)
@case_assets_blueprint.route('/case/assets/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def add_asset(caseid):
try:
# validate before saving
add_asset_schema = CaseAssetsSchema()
request_data = call_modules_hook('on_preload_asset_create', data=request.get_json(), caseid=caseid)
asset = add_asset_schema.load(request_data)
asset = create_asset(asset=asset,
caseid=caseid,
user_id=current_user.id
)
if request_data.get('ioc_links'):
errors, logs = set_ioc_links(request_data.get('ioc_links'), asset.asset_id)
if errors:
return response_error(f'Encountered errors while linking IOC. Asset has still been updated.')
asset = call_modules_hook('on_postload_asset_create', data=asset, caseid=caseid)
if asset:
track_activity(f"added asset \"{asset.asset_name}\"", caseid=caseid)
return response_success("Asset added", data=add_asset_schema.dump(asset))
return response_error("Unable to create asset for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_assets_blueprint.route('/case/assets/upload', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_upload_ioc(caseid):
try:
# validate before saving
add_asset_schema = CaseAssetsSchema()
jsdata = request.get_json()
# get IOC list from request
csv_lines = jsdata["CSVData"].splitlines() # unavoidable since the file is passed as a string
headers = "asset_name,asset_type_name,asset_description,asset_ip,asset_domain,asset_tags"
if csv_lines[0].lower() != headers:
csv_lines.insert(0, headers)
# convert list of strings into CSV
csv_data = csv.DictReader(csv_lines, delimiter=',')
ret = []
errors = []
analysis_status = AnalysisStatus.query.filter(AnalysisStatus.name == 'Unspecified').first()
analysis_status_id = analysis_status.id
index = 0
for row in csv_data:
missing_field = False
for e in headers.split(','):
if row.get(e) is None:
errors.append(f"{e} is missing for row {index}")
missing_field = True
continue
if missing_field:
continue
# Asset name must not be empty
if not row.get("asset_name"):
errors.append(f"Empty asset name for row {index}")
track_activity(f"Attempted to upload an empty asset name")
index += 1
continue
if row.get("asset_tags"):
row["asset_tags"] = row.get("asset_tags").replace("|", ",") # Reformat Tags
if not row.get('asset_type_name'):
errors.append(f"Empty asset type for row {index}")
track_activity(f"Attempted to upload an empty asset type")
index += 1
continue
type_id = get_asset_type_id(row['asset_type_name'].lower())
if not type_id:
errors.append(f"{row.get('asset_name')} (invalid asset type: {row.get('asset_type_name')}) for row {index}")
track_activity(f"Attempted to upload unrecognized asset type \"{row.get('asset_type_name')}\"")
index += 1
continue
row['asset_type_id'] = type_id.asset_id
row.pop('asset_type_name', None)
row['analysis_status_id'] = analysis_status_id
request_data = call_modules_hook('on_preload_asset_create', data=row, caseid=caseid)
asset_sc = add_asset_schema.load(request_data)
asset_sc.custom_attributes = get_default_custom_attributes('asset')
asset = create_asset(asset=asset_sc,
caseid=caseid,
user_id=current_user.id
)
asset = call_modules_hook('on_postload_asset_create', data=asset, caseid=caseid)
if not asset:
errors.append(f"Unable to add asset for internal reason")
index += 1
continue
ret.append(request_data)
track_activity(f"added asset {asset.asset_name}", caseid=caseid)
index += 1
if len(errors) == 0:
msg = "Successfully imported data."
else:
msg = "Data is imported but we got errors with the following rows:\n- " + "\n- ".join(errors)
return response_success(msg=msg, data=ret)
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_assets_blueprint.route('/case/assets/<int:cur_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def asset_view(cur_id, caseid):
# Get IoCs already linked to the asset
asset_iocs = get_linked_iocs_finfo_from_asset(cur_id)
ioc_prefill = [row._asdict() for row in asset_iocs]
asset = get_asset(cur_id, caseid)
if not asset:
return response_error("Invalid asset ID for this case")
asset_schema = CaseAssetsSchema()
data = asset_schema.dump(asset)
data['linked_ioc'] = ioc_prefill
return response_success(data=data)
@case_assets_blueprint.route('/case/assets/<int:cur_id>/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def asset_view_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_assets.case_assets', cid=caseid, redirect=True))
# Get IoCs from the case
case_iocs = get_iocs(caseid)
# Get IoCs already linked to the asset
asset_iocs = get_linked_iocs_id_from_asset(cur_id)
ioc_prefill = [row for row in asset_iocs]
# Build the form
form = AssetBasicForm()
asset = get_asset(cur_id, caseid)
form.asset_name.render_kw = {'value': asset.asset_name}
form.asset_description.data = asset.asset_description
form.asset_info.data = asset.asset_info
form.asset_ip.render_kw = {'value': asset.asset_ip}
form.asset_domain.render_kw = {'value': asset.asset_domain}
form.asset_compromise_status_id.choices = get_compromise_status_list()
form.asset_type_id.choices = get_assets_types()
form.analysis_status_id.choices = get_analysis_status_list()
form.asset_tags.render_kw = {'value': asset.asset_tags}
comments_map = get_case_assets_comments_count([cur_id])
return render_template("modal_add_case_asset.html", form=form, asset=asset, map={}, ioc=case_iocs,
ioc_prefill=ioc_prefill, attributes=asset.custom_attributes, comments_map=comments_map)
@case_assets_blueprint.route('/case/assets/update/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def asset_update(cur_id, caseid):
try:
asset = get_asset(cur_id, caseid)
if not asset:
return response_error("Invalid asset ID for this case")
# validate before saving
add_asset_schema = CaseAssetsSchema()
request_data = call_modules_hook('on_preload_asset_update', data=request.get_json(), caseid=caseid)
request_data['asset_id'] = cur_id
asset_schema = add_asset_schema.load(request_data, instance=asset)
update_assets_state(caseid=caseid)
db.session.commit()
if hasattr(asset_schema, 'ioc_links'):
errors, logs = set_ioc_links(asset_schema.ioc_links, asset.asset_id)
if errors:
return response_error(f'Encountered errors while linking IOC. Asset has still been updated.')
asset_schema = call_modules_hook('on_postload_asset_update', data=asset_schema, caseid=caseid)
if asset_schema:
track_activity(f"updated asset \"{asset_schema.asset_name}\"", caseid=caseid)
return response_success("Updated asset {}".format(asset_schema.asset_name),
add_asset_schema.dump(asset_schema))
return response_error("Unable to update asset for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_assets_blueprint.route('/case/assets/delete/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def asset_delete(cur_id, caseid):
call_modules_hook('on_preload_asset_delete', data=cur_id, caseid=caseid)
asset = get_asset(cur_id, caseid)
if not asset:
return response_error("Invalid asset ID for this case")
# Deletes an asset and the potential links with the IoCs from the database
delete_asset(cur_id, caseid)
call_modules_hook('on_postload_asset_delete', data=cur_id, caseid=caseid)
track_activity(f"removed asset ID {asset.asset_name}", caseid=caseid)
return response_success("Deleted")
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_asset_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_task.case_task', cid=caseid, redirect=True))
asset = get_asset(cur_id, caseid=caseid)
if not asset:
return response_error('Invalid asset ID')
return render_template("modal_conversation.html", element_id=cur_id, element_type='assets',
title=asset.asset_name)
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_asset_list(cur_id, caseid):
asset_comments = get_case_asset_comments(cur_id)
if asset_comments is None:
return response_error('Invalid asset ID')
# CommentSchema(many=True).dump(task_comments)
# res = [com._asdict() for com in task_comments]
return response_success(data=CommentSchema(many=True).dump(asset_comments))
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_asset_add(cur_id, caseid):
try:
asset = get_asset(cur_id, caseid=caseid)
if not asset:
return response_error('Invalid asset ID')
comment_schema = CommentSchema()
comment = comment_schema.load(request.get_json())
comment.comment_case_id = caseid
comment.comment_user_id = current_user.id
comment.comment_date = datetime.now()
comment.comment_update_date = datetime.now()
db.session.add(comment)
db.session.commit()
add_comment_to_asset(asset.asset_id, comment.comment_id)
db.session.commit()
hook_data = {
"comment": comment_schema.dump(comment),
"asset": CaseAssetsSchema().dump(asset)
}
call_modules_hook('on_postload_asset_commented', data=hook_data, caseid=caseid)
track_activity(f"asset \"{asset.asset_name}\" commented", caseid=caseid)
return response_success("Asset commented", data=comment_schema.dump(comment))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.normalized_messages(), status=400)
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_asset_get(cur_id, com_id, caseid):
comment = get_case_asset_comment(cur_id, com_id)
if not comment:
return response_error("Invalid comment ID")
return response_success(data=comment._asdict())
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_asset_edit(cur_id, com_id, caseid):
return case_comment_update(com_id, 'assets', caseid)
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_asset_delete(cur_id, com_id, caseid):
success, msg = delete_asset_comment(cur_id, com_id, caseid)
if not success:
return response_error(msg)
call_modules_hook('on_postload_asset_comment_delete', data=com_id, caseid=caseid)
track_activity(f"comment {com_id} on asset {cur_id} deleted", caseid=caseid)
return response_success(msg)

View File

@ -0,0 +1,59 @@
#!/usr/bin/env python3
#
# IRIS Source Code
# DFIR-IRIS Team
# contact@dfir-iris.org
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import marshmallow
from datetime import datetime
from flask import request
from app import db
from app.datamgmt.case.case_comments import get_case_comment
from app.iris_engine.module_handler.module_handler import call_modules_hook
from app.iris_engine.utils.tracker import track_activity
from app.schema.marshables import CommentSchema
from app.util import response_error
from app.util import response_success
def case_comment_update(comment_id, object_type, caseid):
comment = get_case_comment(comment_id, caseid=caseid)
if not comment:
return response_error("Invalid comment ID")
try:
rq_t = request.get_json()
comment_text = rq_t.get('comment_text')
comment.comment_text = comment_text
comment.comment_update_date = datetime.utcnow()
comment_schema = CommentSchema()
db.session.commit()
hook = object_type
if hook.endswith('s'):
hook = hook[:-1]
call_modules_hook(f'on_postload_{hook}_comment_update', data=comment_schema.dump(comment), caseid=caseid)
track_activity(f"comment {comment.comment_id} on {object_type} edited", caseid=caseid)
return response_success("Comment edited", data=comment_schema.dump(comment))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.normalized_messages(), status=400)

View File

@ -0,0 +1,152 @@
#!/usr/bin/env python3
#
# IRIS Source Code
# Copyright (C) 2021 - Airbus CyberSecurity (SAS) - DFIR-IRIS Team
# ir@cyberactionlab.net - contact@dfir-iris.org
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import itertools
from datetime import datetime
from flask import Blueprint
from flask import redirect
from flask import render_template
from flask import url_for
from flask_login import current_user
from flask_wtf import FlaskForm
from app.datamgmt.case.case_db import get_case
from app.datamgmt.case.case_events_db import get_case_events_assets_graph
from app.datamgmt.case.case_events_db import get_case_events_ioc_graph
from app.models.authorization import CaseAccessLevel
from app.util import ac_api_case_requires
from app.util import ac_case_requires
from app.util import response_success
case_graph_blueprint = Blueprint('case_graph',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@case_graph_blueprint.route('/case/graph', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_graph(caseid, url_redir):
if url_redir:
return redirect(url_for('case_graph.case_graph', cid=caseid, redirect=True))
case = get_case(caseid)
form = FlaskForm()
return render_template("case_graph.html", case=case, form=form)
@case_graph_blueprint.route('/case/graph/getdata', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_graph_get_data(caseid):
events = get_case_events_assets_graph(caseid)
events.extend(get_case_events_ioc_graph(caseid))
nodes = []
edges = []
dates = {
"human": [],
"machine": []
}
tmp = {}
for event in events:
if hasattr(event, 'asset_compromise_status_id'):
if event.asset_compromise_status_id == 1:
img = event.asset_icon_compromised
else:
img = event.asset_icon_not_compromised
if event.asset_ip:
title = "{} -{}".format(event.asset_ip, event.asset_description)
else:
title = "{}".format(event.asset_description)
label = event.asset_name
idx = f'a{event.asset_id}'
node_type = 'asset'
else:
img = 'virus-covid-solid.png'
label = event.ioc_value
title = event.ioc_description
idx = f'b{event.ioc_id}'
node_type = 'ioc'
try:
date = "{}-{}-{}".format(event.event_date.day, event.event_date.month, event.event_date.year)
except:
date = '15-05-2021'
if date not in dates:
dates['human'].append(date)
dates['machine'].append(datetime.timestamp(event.event_date))
new_node = {
'id': idx,
'label': label,
'image': '/static/assets/img/graph/' + img,
'shape': 'image',
'title': title,
'value': 1
}
if current_user.in_dark_mode:
new_node['font'] = "12px verdana white"
if not any(node['id'] == idx for node in nodes):
nodes.append(new_node)
ak = {
'node_id': idx,
'node_title': "{} - {}".format(event.event_date, event.event_title),
'node_name': label,
'node_type': node_type
}
if tmp.get(event.event_id):
tmp[event.event_id]['list'].append(ak)
else:
tmp[event.event_id] = {
'master_node': [],
'list': [ak]
}
for event_id in tmp:
for subset in itertools.combinations(tmp[event_id]['list'], 2):
if subset[0]['node_type'] == 'ioc' and subset[1]['node_type'] == 'ioc' and len(tmp[event_id]['list']) != 2:
continue
edge = {
'from': subset[0]['node_id'],
'to': subset[1]['node_id'],
'title': subset[0]['node_title'],
'dashes': subset[0]['node_type'] == 'ioc' or subset[1]['node_type'] == 'ioc'
}
edges.append(edge)
resp = {
'nodes': nodes,
'edges': edges,
'dates': dates
}
return response_success("", data=resp)

View File

@ -0,0 +1,453 @@
#!/usr/bin/env python3
#
# IRIS Source Code
# Copyright (C) 2021 - Airbus CyberSecurity (SAS) - DFIR-IRIS Team
# ir@cyberactionlab.net - contact@dfir-iris.org
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# IMPORTS ------------------------------------------------
from datetime import datetime
import csv
import logging as log
import marshmallow
from flask import Blueprint
from flask import redirect
from flask import render_template
from flask import request
from flask import url_for
from flask_login import current_user
from app import db
from app.blueprints.case.case_comments import case_comment_update
from app.datamgmt.case.case_assets_db import get_assets_types
from app.datamgmt.case.case_db import get_case
from app.datamgmt.case.case_iocs_db import add_comment_to_ioc
from app.datamgmt.case.case_iocs_db import add_ioc
from app.datamgmt.case.case_iocs_db import add_ioc_link
from app.datamgmt.case.case_iocs_db import check_ioc_type_id
from app.datamgmt.case.case_iocs_db import delete_ioc
from app.datamgmt.case.case_iocs_db import delete_ioc_comment
from app.datamgmt.case.case_iocs_db import get_case_ioc_comment
from app.datamgmt.case.case_iocs_db import get_case_ioc_comments
from app.datamgmt.case.case_iocs_db import get_case_iocs_comments_count
from app.datamgmt.case.case_iocs_db import get_detailed_iocs
from app.datamgmt.case.case_iocs_db import get_ioc
from app.datamgmt.case.case_iocs_db import get_ioc_links
from app.datamgmt.case.case_iocs_db import get_ioc_type_id
from app.datamgmt.case.case_iocs_db import get_ioc_types_list
from app.datamgmt.case.case_iocs_db import get_tlps
from app.datamgmt.case.case_iocs_db import get_tlps_dict
from app.datamgmt.manage.manage_attribute_db import get_default_custom_attributes
from app.datamgmt.states import get_ioc_state
from app.datamgmt.states import update_ioc_state
from app.forms import ModalAddCaseAssetForm
from app.forms import ModalAddCaseIOCForm
from app.iris_engine.module_handler.module_handler import call_modules_hook
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import CaseAccessLevel
from app.models.models import Ioc
from app.schema.marshables import CommentSchema
from app.schema.marshables import IocSchema
from app.util import ac_api_case_requires
from app.util import ac_case_requires
from app.util import response_error
from app.util import response_success
case_ioc_blueprint = Blueprint(
'case_ioc',
__name__,
template_folder='templates'
)
# CONTENT ------------------------------------------------
@case_ioc_blueprint.route('/case/ioc', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_ioc(caseid, url_redir):
if url_redir:
return redirect(url_for('case_ioc.case_ioc', cid=caseid, redirect=True))
form = ModalAddCaseAssetForm()
form.asset_id.choices = get_assets_types()
# Retrieve the assets linked to the investigation
case = get_case(caseid)
return render_template("case_ioc.html", case=case, form=form)
@case_ioc_blueprint.route('/case/ioc/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_list_ioc(caseid):
iocs = get_detailed_iocs(caseid)
ret = {}
ret['ioc'] = []
for ioc in iocs:
out = ioc._asdict()
# Get links of the IoCs seen in other cases
ial = get_ioc_links(ioc.ioc_id, caseid)
out['link'] = [row._asdict() for row in ial]
# Legacy, must be changed next version
out['misp_link'] = None
ret['ioc'].append(out)
ret['state'] = get_ioc_state(caseid=caseid)
return response_success("", data=ret)
@case_ioc_blueprint.route('/case/ioc/state', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_ioc_state(caseid):
os = get_ioc_state(caseid=caseid)
if os:
return response_success(data=os)
else:
return response_error('No IOC state for this case.')
@case_ioc_blueprint.route('/case/ioc/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_add_ioc(caseid):
try:
# validate before saving
add_ioc_schema = IocSchema()
request_data = call_modules_hook('on_preload_ioc_create', data=request.get_json(), caseid=caseid)
ioc = add_ioc_schema.load(request_data)
if not check_ioc_type_id(type_id=ioc.ioc_type_id):
return response_error("Not a valid IOC type")
ioc, existed = add_ioc(ioc=ioc,
user_id=current_user.id,
caseid=caseid
)
link_existed = add_ioc_link(ioc.ioc_id, caseid)
if link_existed:
return response_success("IOC already exists and linked to this case", data=add_ioc_schema.dump(ioc))
if not link_existed:
ioc = call_modules_hook('on_postload_ioc_create', data=ioc, caseid=caseid)
if ioc:
track_activity("added ioc \"{}\"".format(ioc.ioc_value), caseid=caseid)
msg = "IOC already existed in DB. Updated with info on DB." if existed else "IOC added"
return response_success(msg=msg, data=add_ioc_schema.dump(ioc))
return response_error("Unable to create IOC for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_ioc_blueprint.route('/case/ioc/upload', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_upload_ioc(caseid):
try:
# validate before saving
add_ioc_schema = IocSchema()
jsdata = request.get_json()
# get IOC list from request
headers = "ioc_value,ioc_type,ioc_description,ioc_tags,ioc_tlp"
csv_lines = jsdata["CSVData"].splitlines() # unavoidable since the file is passed as a string
if csv_lines[0].lower() != headers:
csv_lines.insert(0, headers)
# convert list of strings into CSV
csv_data = csv.DictReader(csv_lines, quotechar='"', delimiter=',')
# build a Dict of possible TLP
tlp_dict = get_tlps_dict()
ret = []
errors = []
index = 0
for row in csv_data:
for e in headers.split(','):
if row.get(e) is None:
errors.append(f"{e} is missing for row {index}")
index += 1
continue
# IOC value must not be empty
if not row.get("ioc_value"):
errors.append(f"Empty IOC value for row {index}")
track_activity(f"Attempted to upload an empty IOC value")
index += 1
continue
row["ioc_tags"] = row["ioc_tags"].replace("|", ",") # Reformat Tags
# Convert TLP into TLP id
if row["ioc_tlp"] in tlp_dict:
row["ioc_tlp_id"] = tlp_dict[row["ioc_tlp"]]
else:
row["ioc_tlp_id"] = ""
row.pop("ioc_tlp", None)
type_id = get_ioc_type_id(row['ioc_type'].lower())
if not type_id:
errors.append(f"{row['ioc_value']} (invalid ioc type: {row['ioc_type']}) for row {index}")
log.error(f'Unrecognised IOC type {row["ioc_type"]}')
index += 1
continue
row['ioc_type_id'] = type_id.type_id
row.pop('ioc_type', None)
request_data = call_modules_hook('on_preload_ioc_create', data=row, caseid=caseid)
ioc = add_ioc_schema.load(request_data)
ioc.custom_attributes = get_default_custom_attributes('ioc')
ioc, existed = add_ioc(ioc=ioc,
user_id=current_user.id,
caseid=caseid
)
link_existed = add_ioc_link(ioc.ioc_id, caseid)
if link_existed:
errors.append(f"{ioc.ioc_value} (already exists and linked to this case)")
log.error(f"IOC {ioc.ioc_value} already exists and linked to this case")
index += 1
continue
if ioc:
ioc = call_modules_hook('on_postload_ioc_create', data=ioc, caseid=caseid)
ret.append(request_data)
track_activity(f"added ioc \"{ioc.ioc_value}\"", caseid=caseid)
else:
errors.append(f"{ioc.ioc_value} (internal reasons)")
log.error(f"Unable to create IOC {ioc.ioc_value} for internal reasons")
index += 1
if len(errors) == 0:
msg = "Successfully imported data."
else:
msg = "Data is imported but we got errors with the following rows:\n- " + "\n- ".join(errors)
return response_success(msg=msg, data=ret)
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_ioc_blueprint.route('/case/ioc/add/modal', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_add_ioc_modal(caseid):
form = ModalAddCaseIOCForm()
form.ioc_type_id.choices = [(row['type_id'], row['type_name']) for row in get_ioc_types_list()]
form.ioc_tlp_id.choices = get_tlps()
attributes = get_default_custom_attributes('ioc')
return render_template("modal_add_case_ioc.html", form=form, ioc=Ioc(), attributes=attributes)
@case_ioc_blueprint.route('/case/ioc/delete/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_delete_ioc(cur_id, caseid):
call_modules_hook('on_preload_ioc_delete', data=cur_id, caseid=caseid)
ioc = get_ioc(cur_id, caseid)
if not ioc:
return response_error('Not a valid IOC for this case')
if not delete_ioc(ioc, caseid):
track_activity(f"unlinked IOC ID {ioc.ioc_value}", caseid=caseid)
return response_success(f"IOC {cur_id} unlinked")
call_modules_hook('on_postload_ioc_delete', data=cur_id, caseid=caseid)
track_activity(f"deleted IOC \"{ioc.ioc_value}\"", caseid=caseid)
return response_success(f"IOC {cur_id} deleted")
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_view_ioc_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_assets.case_assets', cid=caseid, redirect=True))
form = ModalAddCaseIOCForm()
ioc = get_ioc(cur_id, caseid)
if not ioc:
return response_error("Invalid IOC ID for this case")
form.ioc_type_id.choices = [(row['type_id'], row['type_name']) for row in get_ioc_types_list()]
form.ioc_tlp_id.choices = get_tlps()
# Render the IOC
form.ioc_tags.render_kw = {'value': ioc.ioc_tags}
form.ioc_description.data = ioc.ioc_description
form.ioc_value.data = ioc.ioc_value
comments_map = get_case_iocs_comments_count([cur_id])
return render_template("modal_add_case_ioc.html", form=form, ioc=ioc, attributes=ioc.custom_attributes,
comments_map=comments_map)
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_view_ioc(cur_id, caseid):
ioc_schema = IocSchema()
ioc = get_ioc(cur_id, caseid)
if not ioc:
return response_error("Invalid IOC ID for this case")
return response_success(data=ioc_schema.dump(ioc))
@case_ioc_blueprint.route('/case/ioc/update/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_update_ioc(cur_id, caseid):
try:
ioc = get_ioc(cur_id, caseid)
if not ioc:
return response_error("Invalid IOC ID for this case")
request_data = call_modules_hook('on_preload_ioc_update', data=request.get_json(), caseid=caseid)
# validate before saving
ioc_schema = IocSchema()
request_data['ioc_id'] = cur_id
ioc_sc = ioc_schema.load(request_data, instance=ioc)
ioc_sc.user_id = current_user.id
if not check_ioc_type_id(type_id=ioc_sc.ioc_type_id):
return response_error("Not a valid IOC type")
update_ioc_state(caseid=caseid)
db.session.commit()
ioc_sc = call_modules_hook('on_postload_ioc_update', data=ioc_sc, caseid=caseid)
if ioc_sc:
track_activity(f"updated ioc \"{ioc_sc.ioc_value}\"", caseid=caseid)
return response_success(f"Updated ioc \"{ioc_sc.ioc_value}\"", data=ioc_schema.dump(ioc))
return response_error("Unable to update ioc for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_ioc_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_ioc.case_ioc', cid=caseid, redirect=True))
ioc = get_ioc(cur_id, caseid=caseid)
if not ioc:
return response_error('Invalid ioc ID')
return render_template("modal_conversation.html", element_id=cur_id, element_type='ioc',
title=ioc.ioc_value)
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_ioc_list(cur_id, caseid):
ioc_comments = get_case_ioc_comments(cur_id)
if ioc_comments is None:
return response_error('Invalid ioc ID')
return response_success(data=CommentSchema(many=True).dump(ioc_comments))
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_ioc_add(cur_id, caseid):
try:
ioc = get_ioc(cur_id, caseid=caseid)
if not ioc:
return response_error('Invalid ioc ID')
comment_schema = CommentSchema()
comment = comment_schema.load(request.get_json())
comment.comment_case_id = caseid
comment.comment_user_id = current_user.id
comment.comment_date = datetime.now()
comment.comment_update_date = datetime.now()
db.session.add(comment)
db.session.commit()
add_comment_to_ioc(ioc.ioc_id, comment.comment_id)
db.session.commit()
hook_data = {
"comment": comment_schema.dump(comment),
"ioc": IocSchema().dump(ioc)
}
call_modules_hook('on_postload_ioc_commented', data=hook_data, caseid=caseid)
track_activity(f"ioc \"{ioc.ioc_value}\" commented", caseid=caseid)
return response_success("Event commented", data=comment_schema.dump(comment))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.normalized_messages(), status=400)
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_ioc_get(cur_id, com_id, caseid):
comment = get_case_ioc_comment(cur_id, com_id)
if not comment:
return response_error("Invalid comment ID")
return response_success(data=comment._asdict())
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_ioc_edit(cur_id, com_id, caseid):
return case_comment_update(com_id, 'ioc', caseid)
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_ioc_delete(cur_id, com_id, caseid):
success, msg = delete_ioc_comment(cur_id, com_id)
if not success:
return response_error(msg)
call_modules_hook('on_postload_ioc_comment_delete', data=com_id, caseid=caseid)
track_activity(f"comment {com_id} on ioc {cur_id} deleted", caseid=caseid)
return response_success(msg)

View File

@ -0,0 +1,511 @@
#!/usr/bin/env python3
#
# IRIS Source Code
# Copyright (C) 2021 - Airbus CyberSecurity (SAS) - DFIR-IRIS Team
# ir@cyberactionlab.net - contact@dfir-iris.org
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import marshmallow
# IMPORTS ------------------------------------------------
from datetime import datetime
from flask import Blueprint
from flask import redirect
from flask import render_template
from flask import request
from flask import url_for
from flask_login import current_user
from flask_socketio import emit, join_room, leave_room
from flask_wtf import FlaskForm
from app import db, socket_io
from app.blueprints.case.case_comments import case_comment_update
from app.datamgmt.case.case_db import case_get_desc_crc
from app.datamgmt.case.case_db import get_case
from app.datamgmt.case.case_notes_db import add_comment_to_note
from app.datamgmt.case.case_notes_db import add_note
from app.datamgmt.case.case_notes_db import add_note_group
from app.datamgmt.case.case_notes_db import delete_note
from app.datamgmt.case.case_notes_db import delete_note_comment
from app.datamgmt.case.case_notes_db import delete_note_group
from app.datamgmt.case.case_notes_db import find_pattern_in_notes
from app.datamgmt.case.case_notes_db import get_case_note_comment
from app.datamgmt.case.case_notes_db import get_case_note_comments
from app.datamgmt.case.case_notes_db import get_case_notes_comments_count
from app.datamgmt.case.case_notes_db import get_group_details
from app.datamgmt.case.case_notes_db import get_groups_short
from app.datamgmt.case.case_notes_db import get_note
from app.datamgmt.case.case_notes_db import get_notes_from_group
from app.datamgmt.case.case_notes_db import update_note
from app.datamgmt.case.case_notes_db import update_note_group
from app.datamgmt.states import get_notes_state
from app.forms import CaseNoteForm
from app.iris_engine.module_handler.module_handler import call_modules_hook
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import CaseAccessLevel
from app.schema.marshables import CaseAddNoteSchema
from app.schema.marshables import CaseGroupNoteSchema
from app.schema.marshables import CaseNoteSchema
from app.schema.marshables import CommentSchema
from app.util import ac_api_case_requires, ac_socket_requires
from app.util import ac_case_requires
from app.util import response_error
from app.util import response_success
case_notes_blueprint = Blueprint('case_notes',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@case_notes_blueprint.route('/case/notes', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_notes(caseid, url_redir):
if url_redir:
return redirect(url_for('case_notes.case_notes', cid=caseid, redirect=True))
form = FlaskForm()
case = get_case(caseid)
if case:
crc32, desc = case_get_desc_crc(caseid)
else:
crc32 = None
desc = None
return render_template('case_notes.html', case=case, form=form, th_desc=desc, crc=crc32)
@case_notes_blueprint.route('/case/notes/<int:cur_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_note_detail(cur_id, caseid):
try:
note = get_note(cur_id, caseid=caseid)
if not note:
return response_error(msg="Invalid note ID")
note_schema = CaseNoteSchema()
return response_success(data=note_schema.dump(note))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_notes_blueprint.route('/case/notes/<int:cur_id>/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_note_detail_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_notes.case_notes', cid=caseid, redirect=True))
form = CaseNoteForm()
note = get_note(cur_id, caseid)
ca = None
if note:
form.content = note.note_content
form.title = note.note_title
form.note_title.render_kw = {"value": note.note_title}
setattr(form, 'note_id', note.note_id)
setattr(form, 'note_uuid', note.note_uuid)
ca = note.custom_attributes
comments_map = get_case_notes_comments_count([cur_id])
return render_template("modal_note_edit.html", note=form, id=cur_id, attributes=ca,
ncid=note.note_case_id, comments_map=comments_map)
return response_error(f'Unable to find note ID {cur_id} for case {caseid}')
@case_notes_blueprint.route('/case/notes/delete/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_note_delete(cur_id, caseid):
call_modules_hook('on_preload_note_delete', data=cur_id, caseid=caseid)
note = get_note(cur_id, caseid)
if not note:
return response_error("Invalid note ID for this case")
try:
delete_note(cur_id, caseid)
except Exception as e:
return response_error("Unable to remove note", data=e.__traceback__)
call_modules_hook('on_postload_note_delete', data=cur_id, caseid=caseid)
track_activity(f"deleted note \"{note.note_title}\"", caseid=caseid)
return response_success(f"Note deleted {cur_id}")
@case_notes_blueprint.route('/case/notes/update/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_note_save(cur_id, caseid):
try:
# validate before saving
addnote_schema = CaseAddNoteSchema()
request_data = call_modules_hook('on_preload_note_update', data=request.get_json(), caseid=caseid)
request_data['note_id'] = cur_id
addnote_schema.load(request_data, partial=['group_id'])
note = update_note(note_content=request_data.get('note_content'),
note_title=request_data.get('note_title'),
update_date=datetime.utcnow(),
user_id=current_user.id,
note_id=cur_id,
caseid=caseid
)
if not note:
return response_error("Invalid note ID for this case")
note = call_modules_hook('on_postload_note_update', data=note, caseid=caseid)
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
track_activity(f"updated note \"{note.note_title}\"", caseid=caseid)
return response_success(f"Note ID {cur_id} saved", data=addnote_schema.dump(note))
@case_notes_blueprint.route('/case/notes/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_note_add(caseid):
try:
# validate before saving
addnote_schema = CaseAddNoteSchema()
request_data = call_modules_hook('on_preload_note_create', data=request.get_json(), caseid=caseid)
addnote_schema.verify_group_id(request_data, caseid=caseid)
addnote_schema.load(request_data)
note = add_note(request_data.get('note_title'),
datetime.utcnow(),
current_user.id,
caseid,
request_data.get('group_id'),
note_content=request_data.get('note_content'))
note = call_modules_hook('on_postload_note_create', data=note, caseid=caseid)
if note:
casenote_schema = CaseNoteSchema()
track_activity(f"added note \"{note.note_title}\"", caseid=caseid)
return response_success('Note added', data=casenote_schema.dump(note))
return response_error("Unable to create note for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_notes_blueprint.route('/case/notes/groups/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_load_notes_groups(caseid):
if not get_case(caseid=caseid):
return response_error("Invalid case ID")
groups_short = get_groups_short(caseid)
sta = []
for group in groups_short:
notes = get_notes_from_group(caseid=caseid, group_id=group.group_id)
group_d = group._asdict()
group_d['notes'] = [note._asdict() for note in notes]
sta.append(group_d)
sta = sorted(sta, key=lambda i: i['group_id'])
ret = {
'groups': sta,
'state': get_notes_state(caseid=caseid)
}
return response_success("", data=ret)
@case_notes_blueprint.route('/case/notes/state', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_notes_state(caseid):
os = get_notes_state(caseid=caseid)
if os:
return response_success(data=os)
else:
return response_error('No notes state for this case.')
@case_notes_blueprint.route('/case/notes/search', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_search_notes(caseid):
if request.is_json:
search = request.json.get('search_term')
ns = []
if search:
search = "%{}%".format(search)
ns = find_pattern_in_notes(search, caseid)
ns = [row._asdict() for row in ns]
return response_success("", data=ns)
return response_error("Invalid request")
@case_notes_blueprint.route('/case/notes/groups/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_add_notes_groups(caseid):
title = ''
if request.is_json:
title = request.json.get('group_title') or ''
ng = add_note_group(group_title=title,
caseid=caseid,
userid=current_user.id,
creationdate=datetime.utcnow())
if ng.group_id:
group_schema = CaseGroupNoteSchema()
track_activity(f"added group note \"{ng.group_title}\"", caseid=caseid)
return response_success("Notes group added", data=group_schema.dump(ng))
else:
return response_error("Unable to add a new group")
return response_error("Invalid request")
@case_notes_blueprint.route('/case/notes/groups/delete/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_delete_notes_groups(cur_id, caseid):
if not delete_note_group(cur_id, caseid):
return response_error("Invalid group ID")
track_activity("deleted group note ID {}".format(cur_id), caseid=caseid)
return response_success("Group ID {} deleted".format(cur_id))
@case_notes_blueprint.route('/case/notes/groups/<int:cur_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_get_notes_group(cur_id, caseid):
group = get_group_details(cur_id, caseid)
if not group:
return response_error(f"Group ID {cur_id} not found")
return response_success("", data=group)
@case_notes_blueprint.route('/case/notes/groups/update/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_edit_notes_groups(cur_id, caseid):
js_data = request.get_json()
if not js_data:
return response_error("Invalid data")
group_title = js_data.get('group_title')
if not group_title:
return response_error("Missing field group_title")
ng = update_note_group(group_title, cur_id, caseid)
if ng:
# Note group has been properly found and updated in db
track_activity("updated group note \"{}\"".format(group_title), caseid=caseid)
group_schema = CaseGroupNoteSchema()
return response_success("Updated title of group ID {}".format(cur_id), data=group_schema.dump(ng))
return response_error("Group ID {} not found".format(cur_id))
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_note_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_note.case_note', cid=caseid, redirect=True))
note = get_note(cur_id, caseid=caseid)
if not note:
return response_error('Invalid note ID')
return render_template("modal_conversation.html", element_id=cur_id, element_type='notes',
title=note.note_title)
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_note_list(cur_id, caseid):
note_comments = get_case_note_comments(cur_id)
if note_comments is None:
return response_error('Invalid note ID')
return response_success(data=CommentSchema(many=True).dump(note_comments))
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_note_add(cur_id, caseid):
try:
note = get_note(cur_id, caseid=caseid)
if not note:
return response_error('Invalid note ID')
comment_schema = CommentSchema()
comment = comment_schema.load(request.get_json())
comment.comment_case_id = caseid
comment.comment_user_id = current_user.id
comment.comment_date = datetime.now()
comment.comment_update_date = datetime.now()
db.session.add(comment)
db.session.commit()
add_comment_to_note(note.note_id, comment.comment_id)
db.session.commit()
hook_data = {
"comment": comment_schema.dump(comment),
"note": CaseNoteSchema().dump(note)
}
call_modules_hook('on_postload_note_commented', data=hook_data, caseid=caseid)
track_activity("note \"{}\" commented".format(note.note_title), caseid=caseid)
return response_success("Event commented", data=comment_schema.dump(comment))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.normalized_messages(), status=400)
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_note_get(cur_id, com_id, caseid):
comment = get_case_note_comment(cur_id, com_id)
if not comment:
return response_error("Invalid comment ID")
return response_success(data=comment._asdict())
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_note_edit(cur_id, com_id, caseid):
return case_comment_update(com_id, 'notes', caseid)
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_note_delete(cur_id, com_id, caseid):
success, msg = delete_note_comment(cur_id, com_id)
if not success:
return response_error(msg)
call_modules_hook('on_postload_note_comment_delete', data=com_id, caseid=caseid)
track_activity(f"comment {com_id} on note {cur_id} deleted", caseid=caseid)
return response_success(msg)
@socket_io.on('change-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_change_note(data):
data['last_change'] = current_user.user
emit('change-note', data, to=data['channel'], skip_sid=request.sid, room=data['channel'])
@socket_io.on('save-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_save_note(data):
data['last_saved'] = current_user.user
emit('save-note', data, to=data['channel'], skip_sid=request.sid, room=data['channel'])
@socket_io.on('clear_buffer-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_clear_buffer_note(message):
emit('clear_buffer-note', message, room=message['channel'])
@socket_io.on('join-notes')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_join_note(data):
room = data['channel']
join_room(room=room)
emit('join-notes', {
'message': f"{current_user.user} just joined",
"user": current_user.user
}, room=room)
@socket_io.on('ping-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_ping_note(data):
emit('ping-note', {"user": current_user.name, "note_id": data['note_id']}, room=data['channel'])
@socket_io.on('pong-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_ping_note(data):
emit('pong-note', {"user": current_user.name, "note_id": data['note_id']}, room=data['channel'])
@socket_io.on('overview-map-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_overview_map_note(data):
emit('overview-map-note', {"user": current_user.user, "note_id": data['note_id']}, room=data['channel'])
@socket_io.on('join-notes-overview')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_join_overview(data):
room = data['channel']
join_room(room=room)
emit('join-notes-overview', {
'message': f"{current_user.user} just joined",
"user": current_user.user
}, room=room)
@socket_io.on('disconnect')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_disconnect(data):
emit('disconnect', current_user.user, broadcast=True)

View File

@ -0,0 +1,306 @@
#!/usr/bin/env python3
#
# IRIS Source Code
# Copyright (C) 2021 - Airbus CyberSecurity (SAS) - DFIR-IRIS Team
# ir@cyberactionlab.net - contact@dfir-iris.org
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# IMPORTS ------------------------------------------------
from datetime import datetime
import marshmallow
from flask import Blueprint
from flask import redirect
from flask import render_template
from flask import request
from flask import url_for
from flask_login import current_user
from flask_wtf import FlaskForm
from app import db
from app.blueprints.case.case_comments import case_comment_update
from app.datamgmt.case.case_db import get_case
from app.datamgmt.case.case_rfiles_db import add_comment_to_evidence
from app.datamgmt.case.case_rfiles_db import add_rfile
from app.datamgmt.case.case_rfiles_db import delete_evidence_comment
from app.datamgmt.case.case_rfiles_db import delete_rfile
from app.datamgmt.case.case_rfiles_db import get_case_evidence_comment
from app.datamgmt.case.case_rfiles_db import get_case_evidence_comments
from app.datamgmt.case.case_rfiles_db import get_case_evidence_comments_count
from app.datamgmt.case.case_rfiles_db import get_rfile
from app.datamgmt.case.case_rfiles_db import get_rfiles
from app.datamgmt.case.case_rfiles_db import update_rfile
from app.datamgmt.manage.manage_attribute_db import get_default_custom_attributes
from app.datamgmt.states import get_evidences_state
from app.iris_engine.module_handler.module_handler import call_modules_hook
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import CaseAccessLevel
from app.schema.marshables import CaseEvidenceSchema
from app.schema.marshables import CommentSchema
from app.util import ac_api_case_requires
from app.util import ac_case_requires
from app.util import response_error
from app.util import response_success
case_rfiles_blueprint = Blueprint(
'case_rfiles',
__name__,
template_folder='templates'
)
# CONTENT ------------------------------------------------
@case_rfiles_blueprint.route('/case/evidences', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_rfile(caseid, url_redir):
if url_redir:
return redirect(url_for('case_rfiles.case_rfile', cid=caseid, redirect=True))
form = FlaskForm()
case = get_case(caseid)
return render_template("case_rfile.html", case=case, form=form)
@case_rfiles_blueprint.route('/case/evidences/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_list_rfiles(caseid):
crf = get_rfiles(caseid)
ret = {
"evidences": [row._asdict() for row in crf],
"state": get_evidences_state(caseid=caseid)
}
return response_success("", data=ret)
@case_rfiles_blueprint.route('/case/evidences/state', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_rfiles_state(caseid):
os = get_evidences_state(caseid=caseid)
if os:
return response_success(data=os)
else:
return response_error('No evidences state for this case.')
@case_rfiles_blueprint.route('/case/evidences/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_add_rfile(caseid):
try:
# validate before saving
evidence_schema = CaseEvidenceSchema()
request_data = call_modules_hook('on_preload_evidence_create', data=request.get_json(), caseid=caseid)
evidence = evidence_schema.load(request_data)
crf = add_rfile(evidence=evidence,
user_id=current_user.id,
caseid=caseid
)
crf = call_modules_hook('on_postload_evidence_create', data=crf, caseid=caseid)
if crf:
track_activity(f"added evidence \"{crf.filename}\"", caseid=caseid)
return response_success("Evidence added", data=evidence_schema.dump(crf))
return response_error("Unable to create task for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_get_evidence(cur_id, caseid):
crf = get_rfile(cur_id, caseid)
if not crf:
return response_error("Invalid evidence ID for this case")
evidence_schema = CaseEvidenceSchema()
return response_success(data=evidence_schema.dump(crf))
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_edit_rfile_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_rfiles.case_rfile', cid=caseid, redirect=True))
crf = get_rfile(cur_id, caseid)
if not crf:
return response_error("Invalid evidence ID for this case")
comments_map = get_case_evidence_comments_count([cur_id])
return render_template("modal_add_case_rfile.html", rfile=crf, attributes=crf.custom_attributes,
comments_map=comments_map)
@case_rfiles_blueprint.route('/case/evidences/add/modal', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_add_rfile_modal(caseid):
return render_template("modal_add_case_rfile.html", rfile=None, attributes=get_default_custom_attributes('evidence'))
@case_rfiles_blueprint.route('/case/evidences/update/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_edit_rfile(cur_id, caseid):
try:
# validate before saving
evidence_schema = CaseEvidenceSchema()
request_data = call_modules_hook('on_preload_evidence_update', data=request.get_json(), caseid=caseid)
crf = get_rfile(cur_id, caseid)
if not crf:
return response_error("Invalid evidence ID for this case")
request_data['id'] = cur_id
evidence = evidence_schema.load(request_data, instance=crf)
evd = update_rfile(evidence=evidence,
user_id=current_user.id,
caseid=caseid
)
evd = call_modules_hook('on_postload_evidence_update', data=evd, caseid=caseid)
if evd:
track_activity(f"updated evidence \"{evd.filename}\"", caseid=caseid)
return response_success("Evidence {} updated".format(evd.filename), data=evidence_schema.dump(evd))
return response_error("Unable to update task for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_rfiles_blueprint.route('/case/evidences/delete/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_delete_rfile(cur_id, caseid):
call_modules_hook('on_preload_evidence_delete', data=cur_id, caseid=caseid)
crf = get_rfile(cur_id, caseid)
if not crf:
return response_error("Invalid evidence ID for this case")
delete_rfile(cur_id, caseid=caseid)
call_modules_hook('on_postload_evidence_delete', data=cur_id, caseid=caseid)
track_activity(f"deleted evidence \"{crf.filename}\" from registry", caseid)
return response_success("Evidence deleted")
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_evidence_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_task.case_task', cid=caseid, redirect=True))
evidence = get_rfile(cur_id, caseid=caseid)
if not evidence:
return response_error('Invalid evidence ID')
return render_template("modal_conversation.html", element_id=cur_id, element_type='evidences',
title=evidence.filename)
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_evidence_list(cur_id, caseid):
evidence_comments = get_case_evidence_comments(cur_id)
if evidence_comments is None:
return response_error('Invalid evidence ID')
return response_success(data=CommentSchema(many=True).dump(evidence_comments))
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_evidence_add(cur_id, caseid):
try:
evidence = get_rfile(cur_id, caseid=caseid)
if not evidence:
return response_error('Invalid evidence ID')
comment_schema = CommentSchema()
comment = comment_schema.load(request.get_json())
comment.comment_case_id = caseid
comment.comment_user_id = current_user.id
comment.comment_date = datetime.now()
comment.comment_update_date = datetime.now()
db.session.add(comment)
db.session.commit()
add_comment_to_evidence(evidence.id, comment.comment_id)
db.session.commit()
hook_data = {
"comment": comment_schema.dump(comment),
"evidence": CaseEvidenceSchema().dump(evidence)
}
call_modules_hook('on_postload_evidence_commented', data=hook_data, caseid=caseid)
track_activity(f"evidence \"{evidence.filename}\" commented", caseid=caseid)
return response_success("Event commented", data=comment_schema.dump(comment))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.normalized_messages(), status=400)
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_evidence_get(cur_id, com_id, caseid):
comment = get_case_evidence_comment(cur_id, com_id)
if not comment:
return response_error("Invalid comment ID")
return response_success(data=comment._asdict())
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_evidence_edit(cur_id, com_id, caseid):
return case_comment_update(com_id, 'tasks', caseid)
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_evidence_delete(cur_id, com_id, caseid):
success, msg = delete_evidence_comment(cur_id, com_id)
if not success:
return response_error(msg)
call_modules_hook('on_postload_evidence_comment_delete', data=com_id, caseid=caseid)
track_activity(f"comment {com_id} on evidence {cur_id} deleted", caseid=caseid)
return response_success(msg)

View File

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

View File

@ -0,0 +1,368 @@
#!/usr/bin/env python3
#
# IRIS Source Code
# Copyright (C) 2021 - Airbus CyberSecurity (SAS) - DFIR-IRIS Team
# ir@cyberactionlab.net - contact@dfir-iris.org
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# IMPORTS ------------------------------------------------
from datetime import datetime
import marshmallow
from flask import Blueprint
from flask import redirect
from flask import render_template
from flask import request
from flask import url_for
from flask_login import current_user
from flask_wtf import FlaskForm
from app import db
from app.blueprints.case.case_comments import case_comment_update
from app.datamgmt.case.case_db import get_case
from app.datamgmt.case.case_tasks_db import add_comment_to_task
from app.datamgmt.case.case_tasks_db import add_task
from app.datamgmt.case.case_tasks_db import delete_task
from app.datamgmt.case.case_tasks_db import delete_task_comment
from app.datamgmt.case.case_tasks_db import get_case_task_comment
from app.datamgmt.case.case_tasks_db import get_case_task_comments
from app.datamgmt.case.case_tasks_db import get_case_tasks_comments_count
from app.datamgmt.case.case_tasks_db import get_task
from app.datamgmt.case.case_tasks_db import get_task_with_assignees
from app.datamgmt.case.case_tasks_db import get_tasks_status
from app.datamgmt.case.case_tasks_db import get_tasks_with_assignees
from app.datamgmt.case.case_tasks_db import update_task_assignees
from app.datamgmt.case.case_tasks_db import update_task_status
from app.datamgmt.manage.manage_attribute_db import get_default_custom_attributes
from app.datamgmt.states import get_tasks_state
from app.datamgmt.states import update_tasks_state
from app.forms import CaseTaskForm
from app.iris_engine.module_handler.module_handler import call_modules_hook
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import CaseAccessLevel
from app.models.authorization import User
from app.models.models import CaseTasks
from app.schema.marshables import CaseTaskSchema
from app.schema.marshables import CommentSchema
from app.util import ac_api_case_requires
from app.util import ac_case_requires
from app.util import response_error
from app.util import response_success
case_tasks_blueprint = Blueprint('case_tasks',
__name__,
template_folder='templates')
# CONTENT ------------------------------------------------
@case_tasks_blueprint.route('/case/tasks', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_tasks(caseid, url_redir):
if url_redir:
return redirect(url_for('case_tasks.case_tasks', cid=caseid, redirect=True))
form = FlaskForm()
case = get_case(caseid)
return render_template("case_tasks.html", case=case, form=form)
@case_tasks_blueprint.route('/case/tasks/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_get_tasks(caseid):
ct = get_tasks_with_assignees(caseid)
if not ct:
output = []
else:
output = ct
ret = {
"tasks_status": get_tasks_status(),
"tasks": output,
"state": get_tasks_state(caseid=caseid)
}
return response_success("", data=ret)
@case_tasks_blueprint.route('/case/tasks/state', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_get_tasks_state(caseid):
os = get_tasks_state(caseid=caseid)
if os:
return response_success(data=os)
else:
return response_error('No tasks state for this case.')
@case_tasks_blueprint.route('/case/tasks/status/update/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_task_statusupdate(cur_id, caseid):
task = get_task(task_id=cur_id, caseid=caseid)
if not task:
return response_error("Invalid task ID for this case")
if request.is_json:
if update_task_status(request.json.get('task_status_id'), cur_id, caseid):
task_schema = CaseTaskSchema()
return response_success("Task status updated", data=task_schema.dump(task))
else:
return response_error("Invalid status")
else:
return response_error("Invalid request")
@case_tasks_blueprint.route('/case/tasks/add/modal', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_add_task_modal(caseid):
task = CaseTasks()
task.custom_attributes = get_default_custom_attributes('task')
form = CaseTaskForm()
form.task_status_id.choices = [(a.id, a.status_name) for a in get_tasks_status()]
form.task_assignees_id.choices = []
return render_template("modal_add_case_task.html", form=form, task=task, uid=current_user.id, user_name=None,
attributes=task.custom_attributes)
@case_tasks_blueprint.route('/case/tasks/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_add_task(caseid):
try:
# validate before saving
task_schema = CaseTaskSchema()
request_data = call_modules_hook('on_preload_task_create', data=request.get_json(), caseid=caseid)
if 'task_assignee_id' in request_data or 'task_assignees_id' not in request_data:
return response_error('task_assignee_id is not valid anymore since v1.5.0')
task_assignee_list = request_data['task_assignees_id']
del request_data['task_assignees_id']
task = task_schema.load(request_data)
ctask = add_task(task=task,
assignee_id_list=task_assignee_list,
user_id=current_user.id,
caseid=caseid
)
ctask = call_modules_hook('on_postload_task_create', data=ctask, caseid=caseid)
if ctask:
track_activity(f"added task \"{ctask.task_title}\"", caseid=caseid)
return response_success("Task '{}' added".format(ctask.task_title), data=task_schema.dump(ctask))
return response_error("Unable to create task for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_task_view(cur_id, caseid):
task = get_task_with_assignees(task_id=cur_id, case_id=caseid)
if not task:
return response_error("Invalid task ID for this case")
task_schema = CaseTaskSchema()
return response_success(data=task_schema.dump(task))
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_task_view_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_tasks.case_tasks', cid=caseid, redirect=True))
form = CaseTaskForm()
task = get_task_with_assignees(task_id=cur_id, case_id=caseid)
form.task_status_id.choices = [(a.id, a.status_name) for a in get_tasks_status()]
form.task_assignees_id.choices = []
if not task:
return response_error("Invalid task ID for this case")
form.task_title.render_kw = {'value': task.task_title}
form.task_description.data = task.task_description
user_name, = User.query.with_entities(User.name).filter(User.id == task.task_userid_update).first()
comments_map = get_case_tasks_comments_count([task.id])
return render_template("modal_add_case_task.html", form=form, task=task, user_name=user_name,
comments_map=comments_map, attributes=task.custom_attributes)
@case_tasks_blueprint.route('/case/tasks/update/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_edit_task(cur_id, caseid):
try:
task = get_task_with_assignees(task_id=cur_id, case_id=caseid)
if not task:
return response_error("Invalid task ID for this case")
request_data = call_modules_hook('on_preload_task_update', data=request.get_json(), caseid=caseid)
if 'task_assignee_id' in request_data or 'task_assignees_id' not in request_data:
return response_error('task_assignee_id is not valid anymore since v1.5.0')
# validate before saving
task_assignee_list = request_data['task_assignees_id']
del request_data['task_assignees_id']
task_schema = CaseTaskSchema()
request_data['id'] = cur_id
task = task_schema.load(request_data, instance=task)
task.task_userid_update = current_user.id
task.task_last_update = datetime.utcnow()
update_task_assignees(task, task_assignee_list, caseid)
update_tasks_state(caseid=caseid)
db.session.commit()
task = call_modules_hook('on_postload_task_update', data=task, caseid=caseid)
if task:
track_activity(f"updated task \"{task.task_title}\" (status {task.task_status_id})",
caseid=caseid)
return response_success("Task '{}' updated".format(task.task_title), data=task_schema.dump(task))
return response_error("Unable to update task for internal reasons")
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
@case_tasks_blueprint.route('/case/tasks/delete/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_delete_task(cur_id, caseid):
call_modules_hook('on_preload_task_delete', data=cur_id, caseid=caseid)
task = get_task_with_assignees(task_id=cur_id, case_id=caseid)
if not task:
return response_error("Invalid task ID for this case")
delete_task(task.id)
update_tasks_state(caseid=caseid)
call_modules_hook('on_postload_task_delete', data=cur_id, caseid=caseid)
track_activity(f"deleted task \"{task.task_title}\"")
return response_success("Task deleted")
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_task_modal(cur_id, caseid, url_redir):
if url_redir:
return redirect(url_for('case_task.case_task', cid=caseid, redirect=True))
task = get_task(cur_id, caseid=caseid)
if not task:
return response_error('Invalid task ID')
return render_template("modal_conversation.html", element_id=cur_id, element_type='tasks',
title=task.task_title)
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/list', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_task_list(cur_id, caseid):
task_comments = get_case_task_comments(cur_id)
if task_comments is None:
return response_error('Invalid task ID')
return response_success(data=CommentSchema(many=True).dump(task_comments))
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_task_add(cur_id, caseid):
try:
task = get_task(cur_id, caseid=caseid)
if not task:
return response_error('Invalid task ID')
comment_schema = CommentSchema()
comment = comment_schema.load(request.get_json())
comment.comment_case_id = caseid
comment.comment_user_id = current_user.id
comment.comment_date = datetime.now()
comment.comment_update_date = datetime.now()
db.session.add(comment)
db.session.commit()
add_comment_to_task(task.id, comment.comment_id)
db.session.commit()
hook_data = {
"comment": comment_schema.dump(comment),
"task": CaseTaskSchema().dump(task)
}
call_modules_hook('on_postload_task_commented', data=hook_data, caseid=caseid)
track_activity(f"task \"{task.task_title}\" commented", caseid=caseid)
return response_success("Task commented", data=comment_schema.dump(comment))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.normalized_messages(), status=400)
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def case_comment_task_get(cur_id, com_id, caseid):
comment = get_case_task_comment(cur_id, com_id)
if not comment:
return response_error("Invalid comment ID")
return response_success(data=comment._asdict())
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_task_edit(cur_id, com_id, caseid):
return case_comment_update(com_id, 'tasks', caseid)
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def case_comment_task_delete(cur_id, com_id, caseid):
success, msg = delete_task_comment(cur_id, com_id)
if not success:
return response_error(msg)
call_modules_hook('on_postload_task_comment_delete', data=com_id, caseid=caseid)
track_activity(f"comment {com_id} on task {cur_id} deleted", caseid=caseid)
return response_success(msg)

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
#
# IRIS Source Code
# Copyright (C) 2021 - Airbus CyberSecurity (SAS)
# ir@cyberactionlab.net
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# IMPORTS ------------------------------------------------
# VARS ---------------------------------------------------
# CONTENT ------------------------------------------------

View File

@ -0,0 +1,145 @@
#!/usr/bin/env python3
#
# IRIS Source Code
# Copyright (C) 2021 - Airbus CyberSecurity (SAS)
# ir@cyberactionlab.net
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from flask import Blueprint
# IMPORTS ------------------------------------------------
from flask import redirect
from flask import request
from flask_login import current_user
from app import app
from app import cache
from app import db
from app.datamgmt.context.context_db import ctx_get_user_cases
from app.datamgmt.context.context_db import ctx_search_user_cases
from app.models.authorization import Permissions
from app.models.cases import Cases
from app.models.models import Client
from app.util import ac_api_requires
from app.util import not_authenticated_redirection_url
from app.util import response_success
# CONTENT ------------------------------------------------
ctx_blueprint = Blueprint(
'context',
__name__,
template_folder='templates'
)
@ctx_blueprint.route('/context/set', methods=['POST'])
def set_ctx():
"""
Set the context elements of a user i.e the current case
:return: Page
"""
if not current_user.is_authenticated:
return redirect(not_authenticated_redirection_url())
ctx = request.form.get('ctx')
ctx_h = request.form.get('ctx_h')
current_user.ctx_case = ctx
current_user.ctx_human_case = ctx_h
db.session.commit()
update_user_case_ctx()
return response_success(msg="Saved")
@app.context_processor
def iris_version():
return dict(iris_version=app.config.get('IRIS_VERSION'),
organisation_name=app.config.get('ORGANISATION_NAME'),
std_permissions=Permissions,
demo_domain=app.config.get('DEMO_DOMAIN', None))
@app.context_processor
@cache.cached(timeout=3600, key_prefix='iris_has_updates')
def has_updates():
return dict(has_updates=False)
@ctx_blueprint.route('/context/get-cases/<int:max_results>', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def cases_context(max_results, caseid):
# Get all investigations not closed
datao = ctx_get_user_cases(current_user.id, max_results=max_results)
return response_success(data=datao)
@ctx_blueprint.route('/context/search-cases', methods=['GET'])
@ac_api_requires(no_cid_required=True)
def cases_context_search(caseid):
search = request.args.get('q')
if not search:
return response_success(data=[])
# Get all investigations not closed
datao = ctx_search_user_cases(search, current_user.id, max_results=100)
return response_success(data=datao)
def update_user_case_ctx():
"""
Retrieve a list of cases for the case selector
:return:
"""
# Get all investigations not closed
res = Cases.query.with_entities(
Cases.name,
Client.name,
Cases.case_id,
Cases.close_date) \
.join(Cases.client) \
.order_by(Cases.open_date) \
.all()
data = [row for row in res]
if current_user and current_user.ctx_case:
# If the current user have a current case,
# Look for it in the fresh list. If not
# exists then remove from the user context
is_found = False
for row in data:
if row[2] == current_user.ctx_case:
is_found = True
break
if not is_found:
# The case does not exist,
# Removes it from the context
current_user.ctx_case = None
current_user.ctx_human_case = "Not set"
db.session.commit()
# current_user.save()
app.jinja_env.globals.update({
'cases_context_selector': data
})
return data

View File

@ -0,0 +1,381 @@
#!/usr/bin/env python3
#
# IRIS Source Code
# Copyright (C) 2021 - Airbus CyberSecurity (SAS)
# ir@cyberactionlab.net
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import marshmallow
# IMPORTS ------------------------------------------------
from datetime import datetime
from datetime import timedelta
from flask import Blueprint
from flask import redirect
from flask import render_template
from flask import request
from flask import session
from flask import url_for
from flask_login import current_user
from flask_login import logout_user
from flask_wtf import FlaskForm
from sqlalchemy import distinct
from app import app
from app import db
from app.datamgmt.dashboard.dashboard_db import get_global_task, list_user_cases, list_user_reviews
from app.datamgmt.dashboard.dashboard_db import get_tasks_status
from app.datamgmt.dashboard.dashboard_db import list_global_tasks
from app.datamgmt.dashboard.dashboard_db import list_user_tasks
from app.forms import CaseGlobalTaskForm
from app.iris_engine.module_handler.module_handler import call_modules_hook
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import User
from app.models.cases import Cases
from app.models.models import CaseTasks
from app.models.models import GlobalTasks
from app.models.models import TaskStatus
from app.models.models import UserActivity
from app.schema.marshables import CaseTaskSchema, CaseSchema, CaseDetailsSchema
from app.schema.marshables import GlobalTasksSchema
from app.util import ac_api_requires
from app.util import ac_requires
from app.util import not_authenticated_redirection_url
from app.util import response_error
from app.util import response_success
# CONTENT ------------------------------------------------
dashboard_blueprint = Blueprint(
'index',
__name__,
template_folder='templates'
)
# Logout user
@dashboard_blueprint.route('/logout')
def logout():
"""
Logout function. Erase its session and redirect to index i.e login
:return: Page
"""
if session['current_case']:
current_user.ctx_case = session['current_case']['case_id']
current_user.ctx_human_case = session['current_case']['case_name']
db.session.commit()
track_activity("user '{}' has been logged-out".format(current_user.user), ctx_less=True, display_in_ui=False)
logout_user()
return redirect(not_authenticated_redirection_url(request_url='/'))
@dashboard_blueprint.route('/dashboard/case_charts', methods=['GET'])
@ac_api_requires()
def get_cases_charts(caseid):
"""
Get case charts
:return: JSON
"""
res = Cases.query.with_entities(
Cases.open_date
).filter(
Cases.open_date > (datetime.utcnow() - timedelta(days=365))
).order_by(
Cases.open_date
).all()
retr = [[], []]
rk = {}
for case in res:
month = "{}/{}/{}".format(case.open_date.day, case.open_date.month, case.open_date.year)
if month in rk:
rk[month] += 1
else:
rk[month] = 1
retr = [list(rk.keys()), list(rk.values())]
return response_success("", retr)
@dashboard_blueprint.route('/')
def root():
if app.config['DEMO_MODE_ENABLED'] == 'True':
return redirect(url_for('demo-landing.demo_landing'))
return redirect(url_for('index.index'))
@dashboard_blueprint.route('/dashboard')
@ac_requires()
def index(caseid, url_redir):
"""
Index page. Load the dashboard data, create the add customer form
:return: Page
"""
if url_redir:
return redirect(url_for('index.index', cid=caseid if caseid is not None else 1, redirect=True))
msg = None
# Retrieve the dashboard data from multiple sources.
# Quite fast as it is only counts.
user_open_case = Cases.query.filter(
Cases.owner_id == current_user.id,
Cases.close_date == None
).count()
data = {
"user_open_count": user_open_case,
"cases_open_count": Cases.query.filter(Cases.close_date == None).count(),
"cases_count": Cases.query.with_entities(distinct(Cases.case_id)).count(),
}
# Create the customer form to be able to quickly add a customer
form = FlaskForm()
return render_template('index.html', data=data, form=form, msg=msg)
@dashboard_blueprint.route('/global/tasks/list', methods=['GET'])
@ac_api_requires()
def get_gtasks(caseid):
tasks_list = list_global_tasks()
if tasks_list:
output = [c._asdict() for c in tasks_list]
else:
output = []
ret = {
"tasks_status": get_tasks_status(),
"tasks": output
}
return response_success("", data=ret)
@dashboard_blueprint.route('/user/cases/list', methods=['GET'])
@ac_api_requires()
def list_own_cases(caseid):
cases = list_user_cases(
request.args.get('show_closed', 'false', type=str).lower() == 'true'
)
return response_success("", data=CaseDetailsSchema(many=True).dump(cases))
@dashboard_blueprint.route('/global/tasks/<int:cur_id>', methods=['GET'])
@ac_api_requires()
def view_gtask(cur_id, caseid):
task = get_global_task(task_id=cur_id)
if not task:
return response_error(f'Global task ID {cur_id} not found')
return response_success("", data=task._asdict())
@dashboard_blueprint.route('/user/tasks/list', methods=['GET'])
@ac_api_requires()
def get_utasks(caseid):
ct = list_user_tasks()
if ct:
output = [c._asdict() for c in ct]
else:
output = []
ret = {
"tasks_status": get_tasks_status(),
"tasks": output
}
return response_success("", data=ret)
@dashboard_blueprint.route('/user/reviews/list', methods=['GET'])
@ac_api_requires()
def get_reviews(caseid):
ct = list_user_reviews()
if ct:
output = [c._asdict() for c in ct]
else:
output = []
return response_success("", data=output)
@dashboard_blueprint.route('/user/tasks/status/update', methods=['POST'])
@ac_api_requires()
def utask_statusupdate(caseid):
jsdata = request.get_json()
if not jsdata:
return response_error("Invalid request")
jsdata = request.get_json()
if not jsdata:
return response_error("Invalid request")
case_id = jsdata.get('case_id') if jsdata.get('case_id') else caseid
task_id = jsdata.get('task_id')
task = CaseTasks.query.filter(CaseTasks.id == task_id, CaseTasks.task_case_id == case_id).first()
if not task:
return response_error(f"Invalid case task ID {task_id} for case {case_id}")
status_id = jsdata.get('task_status_id')
status = TaskStatus.query.filter(TaskStatus.id == status_id).first()
if not status:
return response_error(f"Invalid task status ID {status_id}")
task.task_status_id = status_id
try:
db.session.commit()
except Exception as e:
return response_error(f"Unable to update task. Error {e}")
task_schema = CaseTaskSchema()
return response_success("Updated", data=task_schema.dump(task))
@dashboard_blueprint.route('/global/tasks/add/modal', methods=['GET'])
@ac_api_requires()
def add_gtask_modal(caseid):
task = GlobalTasks()
form = CaseGlobalTaskForm()
form.task_assignee_id.choices = [(user.id, user.name) for user in User.query.filter(User.active == True).order_by(User.name).all()]
form.task_status_id.choices = [(a.id, a.status_name) for a in get_tasks_status()]
return render_template("modal_add_global_task.html", form=form, task=task, uid=current_user.id, user_name=None)
@dashboard_blueprint.route('/global/tasks/add', methods=['POST'])
@ac_api_requires()
def add_gtask(caseid):
try:
gtask_schema = GlobalTasksSchema()
request_data = call_modules_hook('on_preload_global_task_create', data=request.get_json(), caseid=caseid)
gtask = gtask_schema.load(request_data)
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
gtask.task_userid_update = current_user.id
gtask.task_open_date = datetime.utcnow()
gtask.task_last_update = datetime.utcnow()
gtask.task_last_update = datetime.utcnow()
try:
db.session.add(gtask)
db.session.commit()
except Exception as e:
return response_error(msg="Data error", data=e.__str__(), status=400)
gtask = call_modules_hook('on_postload_global_task_create', data=gtask, caseid=caseid)
track_activity("created new global task \'{}\'".format(gtask.task_title), caseid=caseid)
return response_success('Task added', data=gtask_schema.dump(gtask))
@dashboard_blueprint.route('/global/tasks/update/<int:cur_id>/modal', methods=['GET'])
@ac_api_requires()
def edit_gtask_modal(cur_id, caseid):
form = CaseGlobalTaskForm()
task = GlobalTasks.query.filter(GlobalTasks.id == cur_id).first()
form.task_assignee_id.choices = [(user.id, user.name) for user in
User.query.filter(User.active == True).order_by(User.name).all()]
form.task_status_id.choices = [(a.id, a.status_name) for a in get_tasks_status()]
# Render the task
form.task_title.render_kw = {'value': task.task_title}
form.task_description.data = task.task_description
user_name, = User.query.with_entities(User.name).filter(User.id == task.task_userid_update).first()
return render_template("modal_add_global_task.html", form=form, task=task,
uid=task.task_assignee_id, user_name=user_name)
@dashboard_blueprint.route('/global/tasks/update/<int:cur_id>', methods=['POST'])
@ac_api_requires()
def edit_gtask(cur_id, caseid):
form = CaseGlobalTaskForm()
task = GlobalTasks.query.filter(GlobalTasks.id == cur_id).first()
form.task_assignee_id.choices = [(user.id, user.name) for user in User.query.filter(User.active == True).order_by(User.name).all()]
form.task_status_id.choices = [(a.id, a.status_name) for a in get_tasks_status()]
if not task:
return response_error(msg="Data error", data="Invalid task ID", status=400)
try:
gtask_schema = GlobalTasksSchema()
request_data = call_modules_hook('on_preload_global_task_update', data=request.get_json(),
caseid=caseid)
gtask = gtask_schema.load(request_data, instance=task)
gtask.task_userid_update = current_user.id
gtask.task_last_update = datetime.utcnow()
db.session.commit()
gtask = call_modules_hook('on_postload_global_task_update', data=gtask, caseid=caseid)
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages, status=400)
track_activity("updated global task {} (status {})".format(task.task_title, task.task_status_id), caseid=caseid)
return response_success('Task updated', data=gtask_schema.dump(gtask))
@dashboard_blueprint.route('/global/tasks/delete/<int:cur_id>', methods=['POST'])
@ac_api_requires()
def gtask_delete(cur_id, caseid):
call_modules_hook('on_preload_global_task_delete', data=cur_id, caseid=caseid)
if not cur_id:
return response_error("Missing parameter")
data = GlobalTasks.query.filter(GlobalTasks.id == cur_id).first()
if not data:
return response_error("Invalid global task ID")
GlobalTasks.query.filter(GlobalTasks.id == cur_id).delete()
db.session.commit()
call_modules_hook('on_postload_global_task_delete', data=request.get_json(), caseid=caseid)
track_activity("deleted global task ID {}".format(cur_id), caseid=caseid)
return response_success("Task deleted")

View File

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

View File

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

View File

@ -0,0 +1,455 @@
#!/usr/bin/env python3
#
#
# IRIS Source Code
# Copyright (C) 2022 - DFIR IRIS Team
# contact@dfir-iris.org
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import base64
import datetime
import json
import marshmallow.exceptions
import urllib.parse
from flask import Blueprint
from flask import render_template
from flask import request
from flask import send_file
from flask import url_for
from flask_login import current_user
from pathlib import Path
from werkzeug.utils import redirect
import app
from app import db
from app.datamgmt.datastore.datastore_db import datastore_add_child_node
from app.datamgmt.datastore.datastore_db import datastore_add_file_as_evidence
from app.datamgmt.datastore.datastore_db import datastore_add_file_as_ioc
from app.datamgmt.datastore.datastore_db import datastore_delete_file
from app.datamgmt.datastore.datastore_db import datastore_delete_node
from app.datamgmt.datastore.datastore_db import datastore_filter_tree
from app.datamgmt.datastore.datastore_db import datastore_get_file
from app.datamgmt.datastore.datastore_db import datastore_get_interactive_path_node
from app.datamgmt.datastore.datastore_db import datastore_get_local_file_path
from app.datamgmt.datastore.datastore_db import datastore_get_path_node
from app.datamgmt.datastore.datastore_db import datastore_get_standard_path
from app.datamgmt.datastore.datastore_db import datastore_rename_node
from app.datamgmt.datastore.datastore_db import ds_list_tree
from app.forms import ModalDSFileForm
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import CaseAccessLevel
from app.schema.marshables import DSFileSchema, DSPathSchema
from app.util import ac_api_case_requires
from app.util import ac_case_requires
from app.util import add_obj_history_entry
from app.util import response_error
from app.util import response_success
datastore_blueprint = Blueprint(
'datastore',
__name__,
template_folder='templates'
)
logger = app.logger
@datastore_blueprint.route('/datastore/list/tree', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def datastore_list_tree(caseid):
data = ds_list_tree(caseid)
return response_success("", data=data)
@datastore_blueprint.route('/datastore/list/filter', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def datastore_list_filter(caseid):
args = request.args.to_dict()
query_filter = args.get('q')
try:
filter_d = dict(json.loads(urllib.parse.unquote_plus(query_filter)))
except Exception as e:
logger.error('Error parsing filter: {}'.format(query_filter))
logger.error(e)
return response_error('Invalid query')
data, log = datastore_filter_tree(filter_d, caseid)
if data is None:
logger.error('Error parsing filter: {}'.format(query_filter))
logger.error(log)
return response_error('Invalid query')
return response_success("", data=data)
@datastore_blueprint.route('/datastore/file/add/<int:cur_id>/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.full_access)
def datastore_add_file_modal(cur_id: int, caseid: int, url_redir: bool):
if url_redir:
return redirect(url_for('index.index', cid=caseid, redirect=True))
dsp = datastore_get_path_node(cur_id, caseid)
if not dsp:
return response_error('Invalid path node for this case')
form = ModalDSFileForm()
return render_template("modal_ds_file.html", form=form, file=None, dsp=dsp)
@datastore_blueprint.route('/datastore/filter-help/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def datastore_filter_help_modal(caseid, url_redir):
if url_redir:
return redirect(url_for('index.index', cid=caseid, redirect=True))
return render_template("modal_help_filter_ds.html")
@datastore_blueprint.route('/datastore/file/update/<int:cur_id>/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def datastore_update_file_modal(cur_id: int, caseid: int, url_redir: bool):
if url_redir:
return redirect(url_for('index.index', cid=caseid, redirect=True))
file = datastore_get_file(cur_id, caseid)
if not file:
return response_error('Invalid file ID for this case')
dsp = datastore_get_path_node(file.file_parent_id, caseid)
form = ModalDSFileForm()
form.file_is_ioc.data = file.file_is_ioc
form.file_original_name.data = file.file_original_name
form.file_password.data = file.file_password
form.file_password.render_kw = {'disabled': 'disabled'}
form.file_description.data = file.file_description
form.file_is_evidence.data = file.file_is_evidence
return render_template("modal_ds_file.html", form=form, file=file, dsp=dsp)
@datastore_blueprint.route('/datastore/file/info/<int:cur_id>/modal', methods=['GET'])
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def datastore_info_file_modal(cur_id: int, caseid: int, url_redir: bool):
if url_redir:
return redirect(url_for('index.index', cid=caseid, redirect=True))
file = datastore_get_file(cur_id, caseid)
if not file:
return response_error('Invalid file ID for this case')
dsp = datastore_get_path_node(file.file_parent_id, caseid)
return render_template("modal_ds_file_info.html", file=file, dsp=dsp)
@datastore_blueprint.route('/datastore/file/info/<int:cur_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def datastore_info_file(cur_id: int, caseid: int):
file = datastore_get_file(cur_id, caseid)
if not file:
return response_error('Invalid file ID for this case')
file_schema = DSFileSchema()
file = file_schema.dump(file)
del file['file_local_name']
return response_success("", data=file)
@datastore_blueprint.route('/datastore/file/update/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_update_file(cur_id: int, caseid: int):
dsf = datastore_get_file(cur_id, caseid)
if not dsf:
return response_error('Invalid file ID for this case')
dsf_schema = DSFileSchema()
try:
dsf_sc = dsf_schema.load(request.form, instance=dsf, partial=True)
add_obj_history_entry(dsf_sc, 'updated')
dsf.file_is_ioc = request.form.get('file_is_ioc') is not None or request.form.get('file_is_ioc') is True
dsf.file_is_evidence = request.form.get('file_is_evidence') is not None or request.form.get('file_is_evidence') is True
db.session.commit()
if request.files.get('file_content'):
ds_location = datastore_get_standard_path(dsf_sc, caseid)
dsf_sc.file_local_name, dsf_sc.file_size, dsf_sc.file_sha256 = dsf_schema.ds_store_file(
request.files.get('file_content'),
ds_location,
dsf_sc.file_is_ioc,
dsf_sc.file_password)
if dsf_sc.file_is_ioc and not dsf_sc.file_password:
dsf_sc.file_password = 'infected'
db.session.commit()
msg_added_as = ''
if dsf.file_is_ioc:
datastore_add_file_as_ioc(dsf, caseid)
msg_added_as += 'and added in IOC'
if dsf.file_is_evidence:
datastore_add_file_as_evidence(dsf, caseid)
msg_added_as += ' and evidence' if len(msg_added_as) > 0 else 'and added in evidence'
track_activity(f'File \"{dsf.file_original_name}\" updated in DS', caseid=caseid)
return response_success('File updated in datastore', data=dsf_schema.dump(dsf_sc))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages)
@datastore_blueprint.route('/datastore/file/move/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_move_file(cur_id: int, caseid: int):
if not request.json:
return response_error("Invalid data")
dsf = datastore_get_file(cur_id, caseid)
if not dsf:
return response_error('Invalid file ID for this case')
dsp = datastore_get_path_node(request.json.get('destination-node'), caseid)
if not dsp:
return response_error('Invalid destination node ID for this case')
dsf.file_parent_id = dsp.path_id
db.session.commit()
track_activity(f'File \"{dsf.file_original_name}\" moved to \"{dsp.path_name}\" in DS', caseid=caseid)
return response_success(f"File successfully moved to {dsp.path_name}")
@datastore_blueprint.route('/datastore/folder/move/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_move_folder(cur_id: int, caseid: int):
if not request.json:
return response_error("Invalid data")
dsp = datastore_get_path_node(cur_id, caseid)
if not dsp:
return response_error('Invalid file ID for this case')
dsp_dst = datastore_get_path_node(request.json.get('destination-node'), caseid)
if not dsp_dst:
return response_error('Invalid destination node ID for this case')
if dsp.path_id == dsp_dst.path_id:
return response_error("If that's true, then I've made a mistake, and you should kill me now.")
dsp.path_parent_id = dsp_dst.path_id
db.session.commit()
dsf_folder_schema = DSPathSchema()
msg = f"Folder \"{dsp.path_name}\" successfully moved to \"{dsp_dst.path_name}\""
track_activity(msg, caseid=caseid)
return response_success(msg, data=dsf_folder_schema.dump(dsp))
@datastore_blueprint.route('/datastore/file/view/<int:cur_id>', methods=['GET'])
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
def datastore_view_file(cur_id: int, caseid: int):
has_error, dsf = datastore_get_local_file_path(cur_id, caseid)
if has_error:
return response_error('Unable to get requested file ID', data=dsf)
if dsf.file_is_ioc or dsf.file_password:
dsf.file_original_name += ".zip"
if not Path(dsf.file_local_name).is_file():
return response_error(f'File {dsf.file_local_name} does not exists on the server. '
f'Update or delete virtual entry')
resp = send_file(dsf.file_local_name, as_attachment=True,
download_name=dsf.file_original_name)
track_activity(f"File \"{dsf.file_original_name}\" downloaded", caseid=caseid, display_in_ui=False)
return resp
@datastore_blueprint.route('/datastore/file/add/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_add_file(cur_id: int, caseid: int):
dsp = datastore_get_path_node(cur_id, caseid)
if not dsp:
return response_error('Invalid path node for this case')
dsf_schema = DSFileSchema()
try:
dsf_sc = dsf_schema.load(request.form, partial=True)
dsf_sc.file_parent_id = dsp.path_id
dsf_sc.added_by_user_id = current_user.id
dsf_sc.file_date_added = datetime.datetime.now()
dsf_sc.file_local_name = 'tmp_xc'
dsf_sc.file_case_id = caseid
add_obj_history_entry(dsf_sc, 'created')
if dsf_sc.file_is_ioc and not dsf_sc.file_password:
dsf_sc.file_password = 'infected'
db.session.add(dsf_sc)
db.session.commit()
ds_location = datastore_get_standard_path(dsf_sc, caseid)
dsf_sc.file_local_name, dsf_sc.file_size, dsf_sc.file_sha256 = dsf_schema.ds_store_file(
request.files.get('file_content'),
ds_location,
dsf_sc.file_is_ioc,
dsf_sc.file_password)
db.session.commit()
msg_added_as = ''
if dsf_sc.file_is_ioc:
datastore_add_file_as_ioc(dsf_sc, caseid)
msg_added_as += 'and added in IOC'
if dsf_sc.file_is_evidence:
datastore_add_file_as_evidence(dsf_sc, caseid)
msg_added_as += ' and evidence' if len(msg_added_as) > 0 else 'and added in evidence'
track_activity(f"File \"{dsf_sc.file_original_name}\" added to DS", caseid=caseid)
return response_success(f'File saved in datastore {msg_added_as}', data=dsf_schema.dump(dsf_sc))
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages)
@datastore_blueprint.route('/datastore/file/add-interactive', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_add_interactive_file(caseid: int):
dsp = datastore_get_interactive_path_node(caseid)
if not dsp:
return response_error('Invalid path node for this case')
dsf_schema = DSFileSchema()
try:
js_data = request.json
try:
file_content = base64.b64decode(js_data.get('file_content'))
filename = js_data.get('file_original_name')
except Exception as e:
return response_error(msg=str(e))
dsf_sc, existed = dsf_schema.ds_store_file_b64(filename, file_content, dsp, caseid)
if not existed:
msg = "File saved in datastore"
else:
msg = "File already existing in datastore. Using it."
track_activity(f"File \"{dsf_sc.file_original_name}\" added to DS", caseid=caseid)
return response_success(msg, data={"file_url": f"/datastore/file/view/{dsf_sc.file_id}"})
except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages)
@datastore_blueprint.route('/datastore/folder/add', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_add_folder(caseid: int):
data = request.json
if not data:
return response_error('Invalid data')
parent_node = data.get('parent_node')
folder_name = data.get('folder_name')
if not parent_node or not folder_name:
return response_error('Invalid data')
has_error, logs, node = datastore_add_child_node(parent_node, folder_name, caseid)
dsf_folder_schema = DSPathSchema()
if not has_error:
track_activity(f"Folder \"{folder_name}\" added to DS", caseid=caseid)
return response_success(msg=logs, data=dsf_folder_schema.dump(node))
return response_error(msg=logs)
@datastore_blueprint.route('/datastore/folder/rename/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_rename_folder(cur_id: int, caseid: int):
data = request.json
if not data:
return response_error('Invalid data')
parent_node = data.get('parent_node')
folder_name = data.get('folder_name')
if not parent_node or not folder_name:
return response_error('Invalid data')
if int(parent_node) != cur_id:
return response_error('Invalid data')
has_error, logs, dsp_base = datastore_rename_node(parent_node, folder_name, caseid)
dsf_folder_schema = DSPathSchema()
if has_error:
return response_error(logs)
track_activity(f"Folder \"{parent_node}\" renamed to \"{folder_name}\" in DS", caseid=caseid)
return response_success(logs, data=dsf_folder_schema.dump(dsp_base))
@datastore_blueprint.route('/datastore/folder/delete/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_delete_folder_route(cur_id: int, caseid: int):
has_error, logs = datastore_delete_node(cur_id, caseid)
if has_error:
return response_error(logs)
track_activity(f"Folder \"{cur_id}\" deleted from DS", caseid=caseid)
return response_success(logs)
@datastore_blueprint.route('/datastore/file/delete/<int:cur_id>', methods=['POST'])
@ac_api_case_requires(CaseAccessLevel.full_access)
def datastore_delete_file_route(cur_id: int, caseid: int):
has_error, logs = datastore_delete_file(cur_id, caseid)
if has_error:
return response_error(logs)
track_activity(f"File \"{cur_id}\" deleted from DS", caseid=caseid)
return response_success(logs)

View File

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

View File

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

View File

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

View File

@ -0,0 +1,59 @@
#!/usr/bin/env python3
#
# IRIS Source Code
# contact@dfir-iris.org
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# IMPORTS ------------------------------------------------
from flask import Blueprint
from flask import render_template
from app import app
from app.iris_engine.demo_builder import gen_demo_admins
from app.iris_engine.demo_builder import gen_demo_users
demo_blueprint = Blueprint(
'demo-landing',
__name__,
template_folder='templates'
)
log = app.logger
if app.config.get('DEMO_MODE_ENABLED') == 'True':
@demo_blueprint.route('/welcome', methods=['GET'])
def demo_landing():
iris_version = app.config.get('IRIS_VERSION')
demo_domain = app.config.get('DEMO_DOMAIN')
seed_user = app.config.get('DEMO_USERS_SEED')
seed_adm = app.config.get('DEMO_ADM_SEED')
adm_count = int(app.config.get('DEMO_ADM_COUNT', 4))
users_count = int(app.config.get('DEMO_USERS_COUNT', 10))
demo_users = [
{'username': username, 'password': pwd, 'role': 'Admin'} for _, username, pwd, _ in gen_demo_admins(adm_count, seed_adm)
]
demo_users += [
{'username': username, 'password': pwd, 'role': 'User'} for _, username, pwd, _ in gen_demo_users(users_count, seed_user)
]
return render_template(
'demo-landing.html',
iris_version=iris_version,
demo_domain=demo_domain,
demo_users=demo_users
)

View File

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

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