This commit is contained in:
136
iris-web/source/app/__init__.py
Normal file
136
iris-web/source/app/__init__.py
Normal 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
|
||||
|
||||
|
103
iris-web/source/app/alembic.ini
Normal file
103
iris-web/source/app/alembic.ini
Normal 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
|
1
iris-web/source/app/alembic/README
Normal file
1
iris-web/source/app/alembic/README
Normal file
@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
27
iris-web/source/app/alembic/alembic_utils.py
Normal file
27
iris-web/source/app/alembic/alembic_utils.py
Normal 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
|
82
iris-web/source/app/alembic/env.py
Normal file
82
iris-web/source/app/alembic/env.py
Normal 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()
|
24
iris-web/source/app/alembic/script.py.mako
Normal file
24
iris-web/source/app/alembic/script.py.mako
Normal 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"}
|
@ -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
|
@ -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
|
@ -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
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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')
|
@ -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
|
@ -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
|
@ -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'"
|
||||
)
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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')
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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"
|
0
iris-web/source/app/blueprints/__init__.py
Normal file
0
iris-web/source/app/blueprints/__init__.py
Normal file
@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
#
|
||||
# IRIS Source Code
|
||||
# Copyright (C) 2021 - Airbus CyberSecurity (SAS)
|
||||
# ir@cyberactionlab.net
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# IMPORTS ------------------------------------------------
|
||||
import os
|
||||
from flask import Blueprint
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
from flask import url_for
|
||||
from flask_wtf import FlaskForm
|
||||
|
||||
import app
|
||||
from app.datamgmt.activities.activities_db import get_all_users_activities
|
||||
from app.datamgmt.activities.activities_db import get_users_activities
|
||||
from app.models.authorization import Permissions
|
||||
from app.util import ac_api_requires
|
||||
from app.util import ac_requires
|
||||
from app.util import response_success
|
||||
|
||||
activities_blueprint = Blueprint(
|
||||
'activities',
|
||||
__name__,
|
||||
template_folder='templates'
|
||||
)
|
||||
|
||||
basedir = os.path.abspath(os.path.dirname(app.__file__))
|
||||
|
||||
|
||||
# CONTENT ------------------------------------------------
|
||||
@activities_blueprint.route('/activities', methods=['GET'])
|
||||
@ac_requires(Permissions.activities_read, Permissions.all_activities_read)
|
||||
def activities_index(caseid: int, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('activities.activities_index', cid=caseid, redirect=True))
|
||||
|
||||
form = FlaskForm()
|
||||
|
||||
return render_template('activities.html', form=form)
|
||||
|
||||
|
||||
@activities_blueprint.route('/activities/list', methods=['GET'])
|
||||
@ac_api_requires(Permissions.activities_read, Permissions.all_activities_read)
|
||||
def list_activities(caseid):
|
||||
# Get User activities from database
|
||||
|
||||
user_activities = get_users_activities()
|
||||
|
||||
data = [row._asdict() for row in user_activities]
|
||||
data = sorted(data, key=lambda i: i['activity_date'], reverse=True)
|
||||
|
||||
return response_success("", data=data)
|
||||
|
||||
|
||||
@activities_blueprint.route('/activities/list-all', methods=['GET'])
|
||||
@ac_api_requires(Permissions.all_activities_read)
|
||||
def list_all_activities(caseid):
|
||||
# Get User activities from database
|
||||
|
||||
user_activities = get_all_users_activities()
|
||||
|
||||
data = [row._asdict() for row in user_activities]
|
||||
data = sorted(data, key=lambda i: i['activity_date'], reverse=True)
|
||||
|
||||
return response_success("", data=data)
|
@ -0,0 +1,76 @@
|
||||
{% extends "layouts/default.html" %}
|
||||
|
||||
{% block title %} Activities {% endblock title %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<link href="/static/assets/css/dataTables.buttons.min.css" rel="stylesheet">
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="page-inner">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
|
||||
<div class="card" id="card_main_load" style="display:none;">
|
||||
<div class="card-header">
|
||||
<div class="card-title">User activities ( max 10k entries + unbound )
|
||||
<button type="button" class="btn btn-sm btn-outline-dark float-right ml-2" onclick="refresh_activities();">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="checkbox" value="" id="non_case_related_act">
|
||||
<span class="form-check-sign">Show non case related activity</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="table-responsive" id="activities_table_wrapper">
|
||||
<div class="selectgroup">
|
||||
<span id="table_buttons"></span>
|
||||
</div>
|
||||
<table class="table display table-striped table-hover" width="100%" cellspacing="0" id="activities_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>User</th>
|
||||
<th>Case</th>
|
||||
<th>Manual input</th>
|
||||
<th>From API</th>
|
||||
<th>Activity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>User</th>
|
||||
<th>Case</th>
|
||||
<th>Manual input</th>
|
||||
<th>From API</th>
|
||||
<th>Activity</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ form.hidden_tag() }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascripts %}
|
||||
<script src="/static/assets/js/plugin/datatables/dataTables.cellEdit.js"></script>
|
||||
<script src="/static/assets/js/plugin/datatables/dataTables.buttons.min.js"></script>
|
||||
<script src="/static/assets/js/plugin/datatables/buttons.html5.min.js"></script>
|
||||
<script src="/static/assets/js/plugin/datatables/buttons.print.min.js"></script>
|
||||
|
||||
<script src="/static/assets/js/iris/datatablesUtils.js"></script>
|
||||
<script src="/static/assets/js/iris/activities.js"></script>
|
||||
{% endblock javascripts %}
|
0
iris-web/source/app/blueprints/alerts/__init__.py
Normal file
0
iris-web/source/app/blueprints/alerts/__init__.py
Normal file
1002
iris-web/source/app/blueprints/alerts/alerts_routes.py
Normal file
1002
iris-web/source/app/blueprints/alerts/alerts_routes.py
Normal file
File diff suppressed because it is too large
Load Diff
310
iris-web/source/app/blueprints/alerts/templates/alerts.html
Normal file
310
iris-web/source/app/blueprints/alerts/templates/alerts.html
Normal file
@ -0,0 +1,310 @@
|
||||
{% extends "layouts/default.html" %}
|
||||
|
||||
{% block title %} Alerts {% endblock title %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<link rel="stylesheet" href="/static/assets/css/suggestags.css">
|
||||
<link rel="stylesheet" href="/static/assets/css/alerts.css">
|
||||
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-inner">
|
||||
<div class="">
|
||||
{{ form.csrf_token }}
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-between align-items-center">
|
||||
<div class="card-title mb-2">
|
||||
<span id="alertsInfoFilter">Loading..</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-between align-items-center">
|
||||
<div class="col">
|
||||
<div class="card-subtitle mt-2">
|
||||
<div class="d-flex">
|
||||
<button class="btn btn-sm" href="#filterCardBody" title="Filter" data-toggle="collapse" role="button" aria-expanded="false" aria-controls="filterCardBody">Filter</button>
|
||||
<div class="preset-dropdown-container">
|
||||
<div id="savedFiltersDropdown" class="dropdown"></div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-dark ml-2" id="resetFilters" style="display: none;">Clear Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="card-subtitle mt-2">
|
||||
<div class="d-flex">
|
||||
<div id="alerts-batch-actions" style="display:none;" class="mr-4">
|
||||
<button type="button" class="btn btn-sm ml-2 btn-alert-primary" onclick="mergeMultipleAlertsModal();">Merge</button>
|
||||
<div class="dropdown ml-2 d-inline-block">
|
||||
<button type="button" class="btn btn-alert-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Assign
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#" onclick="updateBatchAlerts({alert_owner_id: userWhoami.user_id});">Assign to me</a>
|
||||
<a class="dropdown-item" href="#" onclick="changeBatchAlertOwner();">Assign</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown ml-2 d-inline-block">
|
||||
<button type="button" class="btn btn-alert-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Set status
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#" onclick="changeStatusBatchAlerts('New');">New</a>
|
||||
<a class="dropdown-item" href="#" onclick="changeStatusBatchAlerts('In progress');">In progress</a>
|
||||
<a class="dropdown-item" href="#" onclick="changeStatusBatchAlerts('Pending');">Pending</a>
|
||||
<a class="dropdown-item" href="#" onclick="changeStatusBatchAlerts('Closed');">Closed</a>
|
||||
<a class="dropdown-item" href="#" onclick="changeStatusBatchAlerts('Merged');">Merged</a>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-alert-danger btn-sm ml-2" onclick="deleteBatchAlerts();"><i class="fa fa-trash mr-2"></i>Delete</button>
|
||||
</div>
|
||||
<button class="btn btn-sm mr-2" id="select-deselect-all" style="display:none;">Select all</button>
|
||||
|
||||
<button class="btn btn-sm mr-2" id="toggle-selection-mode">Select</button>
|
||||
<button class="btn btn-sm mr-2" id="orderAlertsBtn"><i class="fas fa-arrow-up-short-wide"></i></button>
|
||||
<button id="toggleAllAlertsBtn" class="btn btn-sm mr-2" onclick="toggleCollapseAllAlerts()" data-is-expanded="false">Expand All</button>
|
||||
<div class="d-inline-block position-relative">
|
||||
<button class="btn btn-sm mr-2 ml-2" onclick="refreshAlerts();">Refresh</button>
|
||||
<span class="badge badge-pill badge-danger position-absolute" id="newAlertsBadge" style="top: -10px; right: -10px; display: none;">0</span>
|
||||
</div>
|
||||
<div class="pagination-dropdown-container ml-3 mt-1">
|
||||
<label for="alertsPerPage">Alerts per page:</label>
|
||||
<select id="alertsPerPage">
|
||||
<option value="5">5</option>
|
||||
<option value="10" selected>10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="200">200</option>
|
||||
<option value="500">500</option>
|
||||
<option value="1000">1000</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse" id="filterCardBody">
|
||||
<form id="alertFilterForm" class="container mt-4">
|
||||
<div class="form-row">
|
||||
<div class="col-md-3 form-group">
|
||||
<label for="alert_title">Title</label>
|
||||
<input type="text" class="form-control" id="alert_title" name="alert_title">
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label for="alert_description">Description</label>
|
||||
<input type="text" class="form-control" id="alert_description" name="alert_description">
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label for="alert_source">Source</label>
|
||||
<input type="text" class="form-control" id="alert_source" name="alert_source">
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label for="alert_tags">Tags</label>
|
||||
<input type="text" class="form-control" id="alert_tags" name="alert_tags">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-3 form-group">
|
||||
<label for="alert_status_id">Status</label>
|
||||
<select class="form-control" id="alert_status_id" name="alert_status_id">
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label for="alert_severity_id">Severity</label>
|
||||
<select class="form-control" id="alert_severity_id" name="alert_severity_id">
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label for="alert_classification_id">Classification</label>
|
||||
<select class="form-control" id="alert_classification_id" name="alert_classification_id">
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label for="alert_customer_id">Customer</label>
|
||||
<select class="form-control" id="alert_customer_id" name="alert_customer_id">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-3 form-group">
|
||||
<label for="source_start_date">Start Date</label>
|
||||
<input type="date" class="form-control" id="source_start_date" name="source_start_date">
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label for="source_end_date">End Date</label>
|
||||
<input type="date" class="form-control" id="source_end_date" name="source_end_date">
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label for="alert_assets">Asset(s) name</label>
|
||||
<input class="form-control" id="alert_assets" name="alert_assets">
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label for="alert_iocs">IOC(s)</label>
|
||||
<input class="form-control" id="alert_iocs" name="alert_iocs">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-3 form-group">
|
||||
<label for="alert_ids">Alert(s) ID</label>
|
||||
<input class="form-control" id="alert_ids" name="alert_ids">
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label for="case_id">Case ID</label>
|
||||
<input type="number" class="form-control" id="case_id" name="case_id">
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label for="alert_owner_id">Owner</label>
|
||||
<select class="form-control" id="alert_owner_id" name="alert_owner_id">
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label for="alert_resolution_id">Resolution Status</label>
|
||||
<select class="form-control" id="alert_resolution_id" name="alert_resolution_id">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row mt-3">
|
||||
<div class="col centered">
|
||||
<button type="submit" class="btn btn-sm btn-primary">Apply Filters</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-success float-right" id="saveFilters">Save as filter</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4 mb-4 ml-1">
|
||||
<div class="col">
|
||||
<span id="alertsInfoFilterTags"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-2">
|
||||
<div class="col">
|
||||
<nav class="mt-3 float-right">
|
||||
<ul class="pagination pagination-container">
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-group alerts-container">
|
||||
</div>
|
||||
<nav class="mt-3 float-right">
|
||||
<ul class="pagination pagination-container">
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="dropdown-menu" id="context-menu-relationships" style="display: none;">
|
||||
<a id="view-alert" class="dropdown-item" style="cursor: pointer;" onclick="viewAlertGraph();">
|
||||
<i class="fas fa-eye mr-2"></i><span id="view-alert-text">View Alert</span></a>
|
||||
</div>
|
||||
|
||||
<div class="modal" role="dialog" tabindex="-1" id="modal_comment" data-backdrop="false">
|
||||
<div class="modal-lg modal-dialog modal-comment" role="document">
|
||||
<div class="modal-content shadow-xl" id="modal_comment_content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal" role="dialog" tabindex="-1" id="modal_graph_options" data-backdrop="false">
|
||||
<div class="modal-lg modal-dialog modal-comment" role="document">
|
||||
<div class="modal-content shadow-xl" id="modal_graph_options_content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal" role="dialog" tabindex="-1" id="modal_alert_history">
|
||||
<div class="modal-lg modal-dialog" role="document">
|
||||
<div class="modal-content shadow-lg">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Alert history</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modal_alert_history_content">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal" id="editAlertModal" tabindex="-1" role="dialog" aria-labelledby="closeAlertModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="closeAlertModalLabel"></h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Resolution status</label><br>
|
||||
<div class="selectgroup ml-auto mr-auto">
|
||||
<label class="selectgroup-item">
|
||||
<input type="radio" name="resolutionStatus" value="not_applicable" class="selectgroup-input" checked="">
|
||||
<span class="selectgroup-button">Not applicable</span>
|
||||
</label>
|
||||
<label class="selectgroup-item">
|
||||
<input type="radio" name="resolutionStatus" value="false_positive" class="selectgroup-input">
|
||||
<span class="selectgroup-button">False positive</span>
|
||||
</label>
|
||||
<label class="selectgroup-item selectgroup-warning">
|
||||
<input type="radio" name="resolutionStatus" value="true_positive_without_impact" class="selectgroup-input">
|
||||
<span class="selectgroup-button">True positive without impact</span>
|
||||
</label>
|
||||
<label class="selectgroup-item">
|
||||
<input type="radio" name="resolutionStatus" value="true_positive_with_impact" class="selectgroup-input">
|
||||
<span class="selectgroup-button">True positive with impact</span>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
<label for="editAlertNote">Note</label>
|
||||
<textarea class="form-control" id="editAlertNote" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editAlertTags">Tags</label>
|
||||
<input type="text" class="form-control" id="editAlertTags">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<div class="form-group alert-edition-part">
|
||||
<label for="editAlertClassification">Classification</label>
|
||||
<select class="form-control" id="editAlertClassification">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group alert-edition-part">
|
||||
<label for="editAlertSeverity">Severity</label>
|
||||
<select class="form-control" id="editAlertSeverity">
|
||||
</select>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button type="button" class="btn btn-primary float-right mr-2" id="confirmAlertEdition">Close Alert</button>
|
||||
<button type="button" class="btn btn-dark float-right mr-2" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% include 'modal_escalate.html' %}
|
||||
{% include 'modal_enrichment.html' %}
|
||||
{% include 'modal_new_alert_owner.html' %}
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascripts %}
|
||||
<script src="/static/assets/js/plugin/vis/vis.min.js"></script>
|
||||
<script src="/static/assets/js/plugin/vis/vis-network.min.js"></script>
|
||||
<script src="/static/assets/js/iris/alerts.js"></script>
|
||||
<script src="/static/assets/js/iris/comments.js"></script>
|
||||
<script src="/static/assets/js/core/socket.io.js"></script>
|
||||
{% endblock javascripts %}
|
||||
|
@ -0,0 +1,18 @@
|
||||
<div class="modal" id="enrichmentModal" tabindex="-1" role="dialog" aria-labelledby="enrichmentModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="enrichmentModalLabel">Enrichment</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="enrichmentData"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-dark" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,86 @@
|
||||
<div class="modal" id="escalateModal" tabindex="-1" role="dialog" aria-labelledby="escalateModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="escalateModalLabel"></h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group row" id="modalAlertTitleContainer">
|
||||
<div class="col">
|
||||
<label for="modalAlertTitle">Alert title</label>
|
||||
<input type="text" class="form-control" id="modalAlertTitle" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Merge options</label><br/>
|
||||
<div class="selectgroup ml-auto mr-auto">
|
||||
|
||||
<label class="selectgroup-item">
|
||||
<input type="radio" name="mergeOption" value="new_case" class="selectgroup-input" checked>
|
||||
<span class="selectgroup-button">Merge into a new case</span>
|
||||
</label>
|
||||
|
||||
<label class="selectgroup-item">
|
||||
<input type="radio" name="mergeOption" value="existing_case" class="selectgroup-input">
|
||||
<span class="selectgroup-button">Merge into existing case</span>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group"><span id="escalateModalExplanation"></span></div>
|
||||
<div class="form-group" id="modalEscalateCaseTitleContainer" style="display:none;">
|
||||
<label>New case title *</label>
|
||||
<input type="text" class="form-control" id="modalEscalateCaseTitle" name="new_case_title">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="mergeAlertCaseSelectSection" style="display:none;">
|
||||
<label for="mergeAlertCaseSelect">Select case to merge into *</label>
|
||||
<select class="selectpicker form-control" data-dropup-auto="false" data-live-search="true" id="mergeAlertCaseSelect">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="mergeAlertCaseTemplateSection">
|
||||
<label for="mergeAlertCaseTemplate">Select case template</label>
|
||||
<select class="selectpicker form-control" data-dropup-auto="false" data-live-search="true" id="mergeAlertCaseTemplateSelect">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group mt-4" id="ioc-container" style="display:none;">
|
||||
<label>IOCs to import</label>
|
||||
<button type="button" class="btn btn-sm btn-light ml-2 float-right" id="toggle-iocs">Deselect All</button>
|
||||
<div class="form-control" id="ioCsList" style="height: auto; overflow-y: scroll; max-height: 150px;"></div>
|
||||
</div>
|
||||
<div class="form-group mt-4" id="asset-container" style="display:none;">
|
||||
<label>Assets to import</label>
|
||||
<button type="button" class="btn btn-sm btn-light ml-2 float-right" id="toggle-assets">Deselect All</button>
|
||||
<div class="form-control" id="assetsList" style="height: auto; overflow-y: scroll; max-height: 150px;"></div>
|
||||
</div>
|
||||
<div class="form-group mt-4">
|
||||
<label for="note">Escalation note</label>
|
||||
<textarea class="form-control" id="note" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="case_tags">Case tags</label>
|
||||
<input type="text" id="case_tags"
|
||||
class="form-control col-md-12"/>
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label mt-3">
|
||||
<input checked="" class="form-check-input" id="importAsEvent" name="import_as_event" type="checkbox" value="y">
|
||||
<span class="form-check-sign"> Add alert as event in the timeline
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-dark" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="escalateOrMergeButton">Merge</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,22 @@
|
||||
<div class="modal" tabindex="-1" id="changeAlertOwnerModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Reassign Alert #<span id="alertIDAssignModal"></span></h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="changeOwnerAlertSelect">New Owner</label>
|
||||
<select class="form-control" id="changeOwnerAlertSelect"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-dark" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="assign-owner-button">Assign</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
0
iris-web/source/app/blueprints/api/__init__.py
Normal file
0
iris-web/source/app/blueprints/api/__init__.py
Normal file
50
iris-web/source/app/blueprints/api/api_routes.py
Normal file
50
iris-web/source/app/blueprints/api/api_routes.py
Normal file
@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# IRIS Source Code
|
||||
# Copyright (C) 2021 - Airbus CyberSecurity (SAS)
|
||||
# ir@cyberactionlab.net
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
from app import app
|
||||
from app.util import ac_api_requires
|
||||
from app.util import response_success
|
||||
|
||||
api_blueprint = Blueprint(
|
||||
'api',
|
||||
__name__,
|
||||
template_folder='templates'
|
||||
)
|
||||
|
||||
|
||||
# CONTENT ------------------------------------------------
|
||||
@api_blueprint.route('/api/ping', methods=['GET'])
|
||||
@ac_api_requires()
|
||||
def api_ping(caseid):
|
||||
return response_success("pong")
|
||||
|
||||
|
||||
@api_blueprint.route('/api/versions', methods=['GET'])
|
||||
@ac_api_requires()
|
||||
def api_version(caseid):
|
||||
versions = {
|
||||
"iris_current": app.config.get('IRIS_VERSION'),
|
||||
"api_min": app.config.get('API_MIN_VERSION'),
|
||||
"api_current": app.config.get('API_MAX_VERSION')
|
||||
}
|
||||
|
||||
return response_success(data=versions)
|
25
iris-web/source/app/blueprints/case/__init__.py
Normal file
25
iris-web/source/app/blueprints/case/__init__.py
Normal file
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# IRIS Source Code
|
||||
# Copyright (C) 2021 - Airbus CyberSecurity (SAS)
|
||||
# ir@cyberactionlab.net
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# IMPORTS ------------------------------------------------
|
||||
|
||||
# VARS ---------------------------------------------------
|
||||
|
||||
# CONTENT ------------------------------------------------
|
503
iris-web/source/app/blueprints/case/case_assets_routes.py
Normal file
503
iris-web/source/app/blueprints/case/case_assets_routes.py
Normal file
@ -0,0 +1,503 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# IRIS Source Code
|
||||
# Copyright (C) 2021 - Airbus CyberSecurity (SAS) - DFIR-IRIS Team
|
||||
# ir@cyberactionlab.net - contact@dfir-iris.org
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import csv
|
||||
# IMPORTS ------------------------------------------------
|
||||
from datetime import datetime
|
||||
|
||||
import marshmallow
|
||||
from flask import Blueprint
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
from flask import request
|
||||
from flask import url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
from app.blueprints.case.case_comments import case_comment_update
|
||||
from app.datamgmt.case.case_assets_db import add_comment_to_asset
|
||||
from app.datamgmt.case.case_assets_db import create_asset
|
||||
from app.datamgmt.case.case_assets_db import delete_asset
|
||||
from app.datamgmt.case.case_assets_db import delete_asset_comment
|
||||
from app.datamgmt.case.case_assets_db import get_analysis_status_list
|
||||
from app.datamgmt.case.case_assets_db import get_asset
|
||||
from app.datamgmt.case.case_assets_db import get_asset_type_id
|
||||
from app.datamgmt.case.case_assets_db import get_assets
|
||||
from app.datamgmt.case.case_assets_db import get_assets_ioc_links
|
||||
from app.datamgmt.case.case_assets_db import get_assets_types
|
||||
from app.datamgmt.case.case_assets_db import get_case_asset_comment
|
||||
from app.datamgmt.case.case_assets_db import get_case_asset_comments
|
||||
from app.datamgmt.case.case_assets_db import get_case_assets_comments_count
|
||||
from app.datamgmt.case.case_assets_db import get_compromise_status_list
|
||||
from app.datamgmt.case.case_assets_db import get_linked_iocs_finfo_from_asset
|
||||
from app.datamgmt.case.case_assets_db import get_linked_iocs_id_from_asset
|
||||
from app.datamgmt.case.case_assets_db import get_similar_assets
|
||||
from app.datamgmt.case.case_assets_db import set_ioc_links
|
||||
from app.datamgmt.case.case_db import get_case
|
||||
from app.datamgmt.case.case_db import get_case_client_id
|
||||
from app.datamgmt.case.case_iocs_db import get_iocs
|
||||
from app.datamgmt.manage.manage_attribute_db import get_default_custom_attributes
|
||||
from app.datamgmt.manage.manage_users_db import get_user_cases_fast
|
||||
from app.datamgmt.states import get_assets_state
|
||||
from app.datamgmt.states import update_assets_state
|
||||
from app.forms import AssetBasicForm
|
||||
from app.forms import ModalAddCaseAssetForm
|
||||
from app.iris_engine.module_handler.module_handler import call_modules_hook
|
||||
from app.iris_engine.utils.tracker import track_activity
|
||||
from app.models import AnalysisStatus
|
||||
from app.models.authorization import CaseAccessLevel
|
||||
from app.schema.marshables import CaseAssetsSchema
|
||||
from app.schema.marshables import CommentSchema
|
||||
from app.util import ac_api_case_requires
|
||||
from app.util import ac_case_requires
|
||||
from app.util import response_error
|
||||
from app.util import response_success
|
||||
|
||||
case_assets_blueprint = Blueprint('case_assets',
|
||||
__name__,
|
||||
template_folder='templates')
|
||||
|
||||
|
||||
@case_assets_blueprint.route('/case/assets', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_assets(caseid, url_redir):
|
||||
"""
|
||||
Returns the page of case assets, with the list of available assets types.
|
||||
:return: The HTML page of case assets
|
||||
"""
|
||||
if url_redir:
|
||||
return redirect(url_for('case_assets.case_assets', cid=caseid, redirect=True))
|
||||
|
||||
form = ModalAddCaseAssetForm()
|
||||
# Get asset types from database
|
||||
form.asset_id.choices = get_assets_types()
|
||||
|
||||
# Retrieve the assets linked to the investigation
|
||||
case = get_case(caseid)
|
||||
|
||||
return render_template("case_assets.html", case=case, form=form)
|
||||
|
||||
|
||||
@case_assets_blueprint.route('/case/assets/list', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_list_assets(caseid):
|
||||
"""
|
||||
Returns the list of assets from the case.
|
||||
:return: A JSON object containing the assets of the case, enhanced with assets seen on other cases.
|
||||
"""
|
||||
|
||||
# Get all assets objects from the case and the customer id
|
||||
assets = get_assets(caseid)
|
||||
customer_id = get_case_client_id(caseid)
|
||||
|
||||
ret = {}
|
||||
ret['assets'] = []
|
||||
|
||||
ioc_links_req = get_assets_ioc_links(caseid)
|
||||
|
||||
cache_ioc_link = {}
|
||||
for ioc in ioc_links_req:
|
||||
|
||||
if ioc.asset_id not in cache_ioc_link:
|
||||
cache_ioc_link[ioc.asset_id] = [ioc._asdict()]
|
||||
else:
|
||||
cache_ioc_link[ioc.asset_id].append(ioc._asdict())
|
||||
|
||||
cases_access = get_user_cases_fast(current_user.id)
|
||||
|
||||
for asset in assets:
|
||||
asset = asset._asdict()
|
||||
|
||||
if len(assets) < 300:
|
||||
# Find similar assets from other cases with the same customer
|
||||
asset['link'] = list(get_similar_assets(
|
||||
asset['asset_name'], asset['asset_type_id'], caseid, customer_id, cases_access))
|
||||
else:
|
||||
asset['link'] = []
|
||||
|
||||
asset['ioc_links'] = cache_ioc_link.get(asset['asset_id'])
|
||||
|
||||
ret['assets'].append(asset)
|
||||
|
||||
ret['state'] = get_assets_state(caseid=caseid)
|
||||
|
||||
return response_success("", data=ret)
|
||||
|
||||
|
||||
@case_assets_blueprint.route('/case/assets/state', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_assets_state(caseid):
|
||||
os = get_assets_state(caseid=caseid)
|
||||
if os:
|
||||
return response_success(data=os)
|
||||
else:
|
||||
return response_error('No assets state for this case.')
|
||||
|
||||
|
||||
@case_assets_blueprint.route('/case/assets/add/modal', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def add_asset_modal(caseid):
|
||||
form = AssetBasicForm()
|
||||
|
||||
form.asset_type_id.choices = get_assets_types()
|
||||
form.analysis_status_id.choices = get_analysis_status_list()
|
||||
form.asset_compromise_status_id.choices = get_compromise_status_list()
|
||||
|
||||
# Get IoCs from the case
|
||||
ioc = get_iocs(caseid)
|
||||
attributes = get_default_custom_attributes('asset')
|
||||
|
||||
return render_template("modal_add_case_multi_asset.html", form=form, asset=None, ioc=ioc, attributes=attributes)
|
||||
|
||||
|
||||
@case_assets_blueprint.route('/case/assets/add', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def add_asset(caseid):
|
||||
|
||||
try:
|
||||
# validate before saving
|
||||
add_asset_schema = CaseAssetsSchema()
|
||||
request_data = call_modules_hook('on_preload_asset_create', data=request.get_json(), caseid=caseid)
|
||||
|
||||
asset = add_asset_schema.load(request_data)
|
||||
|
||||
asset = create_asset(asset=asset,
|
||||
caseid=caseid,
|
||||
user_id=current_user.id
|
||||
)
|
||||
|
||||
if request_data.get('ioc_links'):
|
||||
errors, logs = set_ioc_links(request_data.get('ioc_links'), asset.asset_id)
|
||||
if errors:
|
||||
return response_error(f'Encountered errors while linking IOC. Asset has still been updated.')
|
||||
|
||||
asset = call_modules_hook('on_postload_asset_create', data=asset, caseid=caseid)
|
||||
|
||||
if asset:
|
||||
track_activity(f"added asset \"{asset.asset_name}\"", caseid=caseid)
|
||||
return response_success("Asset added", data=add_asset_schema.dump(asset))
|
||||
|
||||
return response_error("Unable to create asset for internal reasons")
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages, status=400)
|
||||
|
||||
|
||||
@case_assets_blueprint.route('/case/assets/upload', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_upload_ioc(caseid):
|
||||
|
||||
try:
|
||||
# validate before saving
|
||||
add_asset_schema = CaseAssetsSchema()
|
||||
jsdata = request.get_json()
|
||||
|
||||
# get IOC list from request
|
||||
csv_lines = jsdata["CSVData"].splitlines() # unavoidable since the file is passed as a string
|
||||
|
||||
headers = "asset_name,asset_type_name,asset_description,asset_ip,asset_domain,asset_tags"
|
||||
|
||||
if csv_lines[0].lower() != headers:
|
||||
csv_lines.insert(0, headers)
|
||||
|
||||
# convert list of strings into CSV
|
||||
csv_data = csv.DictReader(csv_lines, delimiter=',')
|
||||
|
||||
ret = []
|
||||
errors = []
|
||||
|
||||
analysis_status = AnalysisStatus.query.filter(AnalysisStatus.name == 'Unspecified').first()
|
||||
analysis_status_id = analysis_status.id
|
||||
|
||||
index = 0
|
||||
for row in csv_data:
|
||||
missing_field = False
|
||||
for e in headers.split(','):
|
||||
if row.get(e) is None:
|
||||
errors.append(f"{e} is missing for row {index}")
|
||||
missing_field = True
|
||||
continue
|
||||
|
||||
if missing_field:
|
||||
continue
|
||||
|
||||
# Asset name must not be empty
|
||||
if not row.get("asset_name"):
|
||||
errors.append(f"Empty asset name for row {index}")
|
||||
track_activity(f"Attempted to upload an empty asset name")
|
||||
index += 1
|
||||
continue
|
||||
|
||||
if row.get("asset_tags"):
|
||||
row["asset_tags"] = row.get("asset_tags").replace("|", ",") # Reformat Tags
|
||||
|
||||
if not row.get('asset_type_name'):
|
||||
errors.append(f"Empty asset type for row {index}")
|
||||
track_activity(f"Attempted to upload an empty asset type")
|
||||
index += 1
|
||||
continue
|
||||
|
||||
type_id = get_asset_type_id(row['asset_type_name'].lower())
|
||||
if not type_id:
|
||||
errors.append(f"{row.get('asset_name')} (invalid asset type: {row.get('asset_type_name')}) for row {index}")
|
||||
track_activity(f"Attempted to upload unrecognized asset type \"{row.get('asset_type_name')}\"")
|
||||
index += 1
|
||||
continue
|
||||
|
||||
row['asset_type_id'] = type_id.asset_id
|
||||
row.pop('asset_type_name', None)
|
||||
|
||||
row['analysis_status_id'] = analysis_status_id
|
||||
|
||||
request_data = call_modules_hook('on_preload_asset_create', data=row, caseid=caseid)
|
||||
asset_sc = add_asset_schema.load(request_data)
|
||||
asset_sc.custom_attributes = get_default_custom_attributes('asset')
|
||||
asset = create_asset(asset=asset_sc,
|
||||
caseid=caseid,
|
||||
user_id=current_user.id
|
||||
)
|
||||
|
||||
asset = call_modules_hook('on_postload_asset_create', data=asset, caseid=caseid)
|
||||
|
||||
if not asset:
|
||||
errors.append(f"Unable to add asset for internal reason")
|
||||
index += 1
|
||||
continue
|
||||
|
||||
ret.append(request_data)
|
||||
track_activity(f"added asset {asset.asset_name}", caseid=caseid)
|
||||
|
||||
index += 1
|
||||
|
||||
if len(errors) == 0:
|
||||
msg = "Successfully imported data."
|
||||
else:
|
||||
msg = "Data is imported but we got errors with the following rows:\n- " + "\n- ".join(errors)
|
||||
|
||||
return response_success(msg=msg, data=ret)
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages, status=400)
|
||||
|
||||
|
||||
@case_assets_blueprint.route('/case/assets/<int:cur_id>', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def asset_view(cur_id, caseid):
|
||||
|
||||
# Get IoCs already linked to the asset
|
||||
asset_iocs = get_linked_iocs_finfo_from_asset(cur_id)
|
||||
|
||||
ioc_prefill = [row._asdict() for row in asset_iocs]
|
||||
|
||||
asset = get_asset(cur_id, caseid)
|
||||
if not asset:
|
||||
return response_error("Invalid asset ID for this case")
|
||||
|
||||
asset_schema = CaseAssetsSchema()
|
||||
data = asset_schema.dump(asset)
|
||||
data['linked_ioc'] = ioc_prefill
|
||||
|
||||
return response_success(data=data)
|
||||
|
||||
|
||||
@case_assets_blueprint.route('/case/assets/<int:cur_id>/modal', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def asset_view_modal(cur_id, caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('case_assets.case_assets', cid=caseid, redirect=True))
|
||||
|
||||
# Get IoCs from the case
|
||||
case_iocs = get_iocs(caseid)
|
||||
|
||||
# Get IoCs already linked to the asset
|
||||
asset_iocs = get_linked_iocs_id_from_asset(cur_id)
|
||||
|
||||
ioc_prefill = [row for row in asset_iocs]
|
||||
|
||||
# Build the form
|
||||
form = AssetBasicForm()
|
||||
asset = get_asset(cur_id, caseid)
|
||||
|
||||
form.asset_name.render_kw = {'value': asset.asset_name}
|
||||
form.asset_description.data = asset.asset_description
|
||||
form.asset_info.data = asset.asset_info
|
||||
form.asset_ip.render_kw = {'value': asset.asset_ip}
|
||||
form.asset_domain.render_kw = {'value': asset.asset_domain}
|
||||
form.asset_compromise_status_id.choices = get_compromise_status_list()
|
||||
form.asset_type_id.choices = get_assets_types()
|
||||
form.analysis_status_id.choices = get_analysis_status_list()
|
||||
form.asset_tags.render_kw = {'value': asset.asset_tags}
|
||||
comments_map = get_case_assets_comments_count([cur_id])
|
||||
|
||||
return render_template("modal_add_case_asset.html", form=form, asset=asset, map={}, ioc=case_iocs,
|
||||
ioc_prefill=ioc_prefill, attributes=asset.custom_attributes, comments_map=comments_map)
|
||||
|
||||
|
||||
@case_assets_blueprint.route('/case/assets/update/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def asset_update(cur_id, caseid):
|
||||
|
||||
try:
|
||||
asset = get_asset(cur_id, caseid)
|
||||
if not asset:
|
||||
return response_error("Invalid asset ID for this case")
|
||||
|
||||
# validate before saving
|
||||
add_asset_schema = CaseAssetsSchema()
|
||||
|
||||
request_data = call_modules_hook('on_preload_asset_update', data=request.get_json(), caseid=caseid)
|
||||
|
||||
request_data['asset_id'] = cur_id
|
||||
asset_schema = add_asset_schema.load(request_data, instance=asset)
|
||||
|
||||
update_assets_state(caseid=caseid)
|
||||
db.session.commit()
|
||||
|
||||
if hasattr(asset_schema, 'ioc_links'):
|
||||
errors, logs = set_ioc_links(asset_schema.ioc_links, asset.asset_id)
|
||||
if errors:
|
||||
return response_error(f'Encountered errors while linking IOC. Asset has still been updated.')
|
||||
|
||||
asset_schema = call_modules_hook('on_postload_asset_update', data=asset_schema, caseid=caseid)
|
||||
|
||||
if asset_schema:
|
||||
track_activity(f"updated asset \"{asset_schema.asset_name}\"", caseid=caseid)
|
||||
return response_success("Updated asset {}".format(asset_schema.asset_name),
|
||||
add_asset_schema.dump(asset_schema))
|
||||
|
||||
return response_error("Unable to update asset for internal reasons")
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages, status=400)
|
||||
|
||||
|
||||
@case_assets_blueprint.route('/case/assets/delete/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def asset_delete(cur_id, caseid):
|
||||
|
||||
call_modules_hook('on_preload_asset_delete', data=cur_id, caseid=caseid)
|
||||
|
||||
asset = get_asset(cur_id, caseid)
|
||||
if not asset:
|
||||
return response_error("Invalid asset ID for this case")
|
||||
|
||||
# Deletes an asset and the potential links with the IoCs from the database
|
||||
delete_asset(cur_id, caseid)
|
||||
|
||||
call_modules_hook('on_postload_asset_delete', data=cur_id, caseid=caseid)
|
||||
|
||||
track_activity(f"removed asset ID {asset.asset_name}", caseid=caseid)
|
||||
|
||||
return response_success("Deleted")
|
||||
|
||||
|
||||
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/modal', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_comment_asset_modal(cur_id, caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('case_task.case_task', cid=caseid, redirect=True))
|
||||
|
||||
asset = get_asset(cur_id, caseid=caseid)
|
||||
if not asset:
|
||||
return response_error('Invalid asset ID')
|
||||
|
||||
return render_template("modal_conversation.html", element_id=cur_id, element_type='assets',
|
||||
title=asset.asset_name)
|
||||
|
||||
|
||||
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/list', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_comment_asset_list(cur_id, caseid):
|
||||
|
||||
asset_comments = get_case_asset_comments(cur_id)
|
||||
if asset_comments is None:
|
||||
return response_error('Invalid asset ID')
|
||||
|
||||
# CommentSchema(many=True).dump(task_comments)
|
||||
# res = [com._asdict() for com in task_comments]
|
||||
return response_success(data=CommentSchema(many=True).dump(asset_comments))
|
||||
|
||||
|
||||
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/add', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_comment_asset_add(cur_id, caseid):
|
||||
|
||||
try:
|
||||
asset = get_asset(cur_id, caseid=caseid)
|
||||
if not asset:
|
||||
return response_error('Invalid asset ID')
|
||||
|
||||
comment_schema = CommentSchema()
|
||||
|
||||
comment = comment_schema.load(request.get_json())
|
||||
comment.comment_case_id = caseid
|
||||
comment.comment_user_id = current_user.id
|
||||
comment.comment_date = datetime.now()
|
||||
comment.comment_update_date = datetime.now()
|
||||
db.session.add(comment)
|
||||
db.session.commit()
|
||||
|
||||
add_comment_to_asset(asset.asset_id, comment.comment_id)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
hook_data = {
|
||||
"comment": comment_schema.dump(comment),
|
||||
"asset": CaseAssetsSchema().dump(asset)
|
||||
}
|
||||
call_modules_hook('on_postload_asset_commented', data=hook_data, caseid=caseid)
|
||||
|
||||
track_activity(f"asset \"{asset.asset_name}\" commented", caseid=caseid)
|
||||
return response_success("Asset commented", data=comment_schema.dump(comment))
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.normalized_messages(), status=400)
|
||||
|
||||
|
||||
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_comment_asset_get(cur_id, com_id, caseid):
|
||||
|
||||
comment = get_case_asset_comment(cur_id, com_id)
|
||||
if not comment:
|
||||
return response_error("Invalid comment ID")
|
||||
|
||||
return response_success(data=comment._asdict())
|
||||
|
||||
|
||||
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_comment_asset_edit(cur_id, com_id, caseid):
|
||||
|
||||
return case_comment_update(com_id, 'assets', caseid)
|
||||
|
||||
|
||||
@case_assets_blueprint.route('/case/assets/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_comment_asset_delete(cur_id, com_id, caseid):
|
||||
|
||||
success, msg = delete_asset_comment(cur_id, com_id, caseid)
|
||||
if not success:
|
||||
return response_error(msg)
|
||||
|
||||
call_modules_hook('on_postload_asset_comment_delete', data=com_id, caseid=caseid)
|
||||
|
||||
track_activity(f"comment {com_id} on asset {cur_id} deleted", caseid=caseid)
|
||||
return response_success(msg)
|
59
iris-web/source/app/blueprints/case/case_comments.py
Normal file
59
iris-web/source/app/blueprints/case/case_comments.py
Normal file
@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# IRIS Source Code
|
||||
# DFIR-IRIS Team
|
||||
# contact@dfir-iris.org
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import marshmallow
|
||||
from datetime import datetime
|
||||
|
||||
from flask import request
|
||||
|
||||
from app import db
|
||||
from app.datamgmt.case.case_comments import get_case_comment
|
||||
from app.iris_engine.module_handler.module_handler import call_modules_hook
|
||||
from app.iris_engine.utils.tracker import track_activity
|
||||
from app.schema.marshables import CommentSchema
|
||||
from app.util import response_error
|
||||
from app.util import response_success
|
||||
|
||||
|
||||
def case_comment_update(comment_id, object_type, caseid):
|
||||
comment = get_case_comment(comment_id, caseid=caseid)
|
||||
if not comment:
|
||||
return response_error("Invalid comment ID")
|
||||
|
||||
try:
|
||||
rq_t = request.get_json()
|
||||
comment_text = rq_t.get('comment_text')
|
||||
comment.comment_text = comment_text
|
||||
comment.comment_update_date = datetime.utcnow()
|
||||
comment_schema = CommentSchema()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
hook = object_type
|
||||
if hook.endswith('s'):
|
||||
hook = hook[:-1]
|
||||
|
||||
call_modules_hook(f'on_postload_{hook}_comment_update', data=comment_schema.dump(comment), caseid=caseid)
|
||||
|
||||
track_activity(f"comment {comment.comment_id} on {object_type} edited", caseid=caseid)
|
||||
return response_success("Comment edited", data=comment_schema.dump(comment))
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.normalized_messages(), status=400)
|
152
iris-web/source/app/blueprints/case/case_graphs_routes.py
Normal file
152
iris-web/source/app/blueprints/case/case_graphs_routes.py
Normal file
@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# IRIS Source Code
|
||||
# Copyright (C) 2021 - Airbus CyberSecurity (SAS) - DFIR-IRIS Team
|
||||
# ir@cyberactionlab.net - contact@dfir-iris.org
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import itertools
|
||||
from datetime import datetime
|
||||
from flask import Blueprint
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
from flask import url_for
|
||||
from flask_login import current_user
|
||||
from flask_wtf import FlaskForm
|
||||
|
||||
from app.datamgmt.case.case_db import get_case
|
||||
from app.datamgmt.case.case_events_db import get_case_events_assets_graph
|
||||
from app.datamgmt.case.case_events_db import get_case_events_ioc_graph
|
||||
from app.models.authorization import CaseAccessLevel
|
||||
from app.util import ac_api_case_requires
|
||||
from app.util import ac_case_requires
|
||||
from app.util import response_success
|
||||
|
||||
case_graph_blueprint = Blueprint('case_graph',
|
||||
__name__,
|
||||
template_folder='templates')
|
||||
|
||||
|
||||
# CONTENT ------------------------------------------------
|
||||
@case_graph_blueprint.route('/case/graph', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_graph(caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('case_graph.case_graph', cid=caseid, redirect=True))
|
||||
|
||||
case = get_case(caseid)
|
||||
form = FlaskForm()
|
||||
|
||||
return render_template("case_graph.html", case=case, form=form)
|
||||
|
||||
|
||||
@case_graph_blueprint.route('/case/graph/getdata', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_graph_get_data(caseid):
|
||||
events = get_case_events_assets_graph(caseid)
|
||||
events.extend(get_case_events_ioc_graph(caseid))
|
||||
|
||||
nodes = []
|
||||
edges = []
|
||||
dates = {
|
||||
"human": [],
|
||||
"machine": []
|
||||
}
|
||||
|
||||
tmp = {}
|
||||
for event in events:
|
||||
if hasattr(event, 'asset_compromise_status_id'):
|
||||
if event.asset_compromise_status_id == 1:
|
||||
img = event.asset_icon_compromised
|
||||
|
||||
else:
|
||||
img = event.asset_icon_not_compromised
|
||||
|
||||
if event.asset_ip:
|
||||
title = "{} -{}".format(event.asset_ip, event.asset_description)
|
||||
else:
|
||||
title = "{}".format(event.asset_description)
|
||||
label = event.asset_name
|
||||
idx = f'a{event.asset_id}'
|
||||
node_type = 'asset'
|
||||
|
||||
else:
|
||||
img = 'virus-covid-solid.png'
|
||||
label = event.ioc_value
|
||||
title = event.ioc_description
|
||||
idx = f'b{event.ioc_id}'
|
||||
node_type = 'ioc'
|
||||
|
||||
try:
|
||||
date = "{}-{}-{}".format(event.event_date.day, event.event_date.month, event.event_date.year)
|
||||
except:
|
||||
date = '15-05-2021'
|
||||
|
||||
if date not in dates:
|
||||
dates['human'].append(date)
|
||||
dates['machine'].append(datetime.timestamp(event.event_date))
|
||||
|
||||
new_node = {
|
||||
'id': idx,
|
||||
'label': label,
|
||||
'image': '/static/assets/img/graph/' + img,
|
||||
'shape': 'image',
|
||||
'title': title,
|
||||
'value': 1
|
||||
}
|
||||
|
||||
if current_user.in_dark_mode:
|
||||
new_node['font'] = "12px verdana white"
|
||||
|
||||
if not any(node['id'] == idx for node in nodes):
|
||||
nodes.append(new_node)
|
||||
|
||||
ak = {
|
||||
'node_id': idx,
|
||||
'node_title': "{} - {}".format(event.event_date, event.event_title),
|
||||
'node_name': label,
|
||||
'node_type': node_type
|
||||
}
|
||||
if tmp.get(event.event_id):
|
||||
tmp[event.event_id]['list'].append(ak)
|
||||
|
||||
else:
|
||||
tmp[event.event_id] = {
|
||||
'master_node': [],
|
||||
'list': [ak]
|
||||
}
|
||||
|
||||
for event_id in tmp:
|
||||
for subset in itertools.combinations(tmp[event_id]['list'], 2):
|
||||
|
||||
if subset[0]['node_type'] == 'ioc' and subset[1]['node_type'] == 'ioc' and len(tmp[event_id]['list']) != 2:
|
||||
continue
|
||||
|
||||
edge = {
|
||||
'from': subset[0]['node_id'],
|
||||
'to': subset[1]['node_id'],
|
||||
'title': subset[0]['node_title'],
|
||||
'dashes': subset[0]['node_type'] == 'ioc' or subset[1]['node_type'] == 'ioc'
|
||||
}
|
||||
edges.append(edge)
|
||||
|
||||
resp = {
|
||||
'nodes': nodes,
|
||||
'edges': edges,
|
||||
'dates': dates
|
||||
}
|
||||
|
||||
return response_success("", data=resp)
|
453
iris-web/source/app/blueprints/case/case_ioc_routes.py
Normal file
453
iris-web/source/app/blueprints/case/case_ioc_routes.py
Normal file
@ -0,0 +1,453 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# IRIS Source Code
|
||||
# Copyright (C) 2021 - Airbus CyberSecurity (SAS) - DFIR-IRIS Team
|
||||
# ir@cyberactionlab.net - contact@dfir-iris.org
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# IMPORTS ------------------------------------------------
|
||||
from datetime import datetime
|
||||
|
||||
import csv
|
||||
import logging as log
|
||||
import marshmallow
|
||||
from flask import Blueprint
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
from flask import request
|
||||
from flask import url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
from app.blueprints.case.case_comments import case_comment_update
|
||||
from app.datamgmt.case.case_assets_db import get_assets_types
|
||||
from app.datamgmt.case.case_db import get_case
|
||||
from app.datamgmt.case.case_iocs_db import add_comment_to_ioc
|
||||
from app.datamgmt.case.case_iocs_db import add_ioc
|
||||
from app.datamgmt.case.case_iocs_db import add_ioc_link
|
||||
from app.datamgmt.case.case_iocs_db import check_ioc_type_id
|
||||
from app.datamgmt.case.case_iocs_db import delete_ioc
|
||||
from app.datamgmt.case.case_iocs_db import delete_ioc_comment
|
||||
from app.datamgmt.case.case_iocs_db import get_case_ioc_comment
|
||||
from app.datamgmt.case.case_iocs_db import get_case_ioc_comments
|
||||
from app.datamgmt.case.case_iocs_db import get_case_iocs_comments_count
|
||||
from app.datamgmt.case.case_iocs_db import get_detailed_iocs
|
||||
from app.datamgmt.case.case_iocs_db import get_ioc
|
||||
from app.datamgmt.case.case_iocs_db import get_ioc_links
|
||||
from app.datamgmt.case.case_iocs_db import get_ioc_type_id
|
||||
from app.datamgmt.case.case_iocs_db import get_ioc_types_list
|
||||
from app.datamgmt.case.case_iocs_db import get_tlps
|
||||
from app.datamgmt.case.case_iocs_db import get_tlps_dict
|
||||
from app.datamgmt.manage.manage_attribute_db import get_default_custom_attributes
|
||||
from app.datamgmt.states import get_ioc_state
|
||||
from app.datamgmt.states import update_ioc_state
|
||||
from app.forms import ModalAddCaseAssetForm
|
||||
from app.forms import ModalAddCaseIOCForm
|
||||
from app.iris_engine.module_handler.module_handler import call_modules_hook
|
||||
from app.iris_engine.utils.tracker import track_activity
|
||||
from app.models.authorization import CaseAccessLevel
|
||||
from app.models.models import Ioc
|
||||
from app.schema.marshables import CommentSchema
|
||||
from app.schema.marshables import IocSchema
|
||||
from app.util import ac_api_case_requires
|
||||
from app.util import ac_case_requires
|
||||
from app.util import response_error
|
||||
from app.util import response_success
|
||||
|
||||
case_ioc_blueprint = Blueprint(
|
||||
'case_ioc',
|
||||
__name__,
|
||||
template_folder='templates'
|
||||
)
|
||||
|
||||
|
||||
# CONTENT ------------------------------------------------
|
||||
@case_ioc_blueprint.route('/case/ioc', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_ioc(caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('case_ioc.case_ioc', cid=caseid, redirect=True))
|
||||
|
||||
form = ModalAddCaseAssetForm()
|
||||
form.asset_id.choices = get_assets_types()
|
||||
|
||||
# Retrieve the assets linked to the investigation
|
||||
case = get_case(caseid)
|
||||
|
||||
return render_template("case_ioc.html", case=case, form=form)
|
||||
|
||||
|
||||
@case_ioc_blueprint.route('/case/ioc/list', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_list_ioc(caseid):
|
||||
iocs = get_detailed_iocs(caseid)
|
||||
|
||||
ret = {}
|
||||
ret['ioc'] = []
|
||||
|
||||
for ioc in iocs:
|
||||
out = ioc._asdict()
|
||||
|
||||
# Get links of the IoCs seen in other cases
|
||||
ial = get_ioc_links(ioc.ioc_id, caseid)
|
||||
|
||||
out['link'] = [row._asdict() for row in ial]
|
||||
# Legacy, must be changed next version
|
||||
out['misp_link'] = None
|
||||
|
||||
ret['ioc'].append(out)
|
||||
|
||||
ret['state'] = get_ioc_state(caseid=caseid)
|
||||
|
||||
return response_success("", data=ret)
|
||||
|
||||
|
||||
@case_ioc_blueprint.route('/case/ioc/state', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_ioc_state(caseid):
|
||||
os = get_ioc_state(caseid=caseid)
|
||||
if os:
|
||||
return response_success(data=os)
|
||||
else:
|
||||
return response_error('No IOC state for this case.')
|
||||
|
||||
|
||||
@case_ioc_blueprint.route('/case/ioc/add', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_add_ioc(caseid):
|
||||
try:
|
||||
# validate before saving
|
||||
add_ioc_schema = IocSchema()
|
||||
|
||||
request_data = call_modules_hook('on_preload_ioc_create', data=request.get_json(), caseid=caseid)
|
||||
|
||||
ioc = add_ioc_schema.load(request_data)
|
||||
|
||||
if not check_ioc_type_id(type_id=ioc.ioc_type_id):
|
||||
return response_error("Not a valid IOC type")
|
||||
|
||||
ioc, existed = add_ioc(ioc=ioc,
|
||||
user_id=current_user.id,
|
||||
caseid=caseid
|
||||
)
|
||||
link_existed = add_ioc_link(ioc.ioc_id, caseid)
|
||||
|
||||
if link_existed:
|
||||
return response_success("IOC already exists and linked to this case", data=add_ioc_schema.dump(ioc))
|
||||
|
||||
if not link_existed:
|
||||
ioc = call_modules_hook('on_postload_ioc_create', data=ioc, caseid=caseid)
|
||||
|
||||
if ioc:
|
||||
track_activity("added ioc \"{}\"".format(ioc.ioc_value), caseid=caseid)
|
||||
|
||||
msg = "IOC already existed in DB. Updated with info on DB." if existed else "IOC added"
|
||||
|
||||
return response_success(msg=msg, data=add_ioc_schema.dump(ioc))
|
||||
|
||||
return response_error("Unable to create IOC for internal reasons")
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages, status=400)
|
||||
|
||||
|
||||
@case_ioc_blueprint.route('/case/ioc/upload', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_upload_ioc(caseid):
|
||||
try:
|
||||
# validate before saving
|
||||
add_ioc_schema = IocSchema()
|
||||
jsdata = request.get_json()
|
||||
|
||||
# get IOC list from request
|
||||
headers = "ioc_value,ioc_type,ioc_description,ioc_tags,ioc_tlp"
|
||||
csv_lines = jsdata["CSVData"].splitlines() # unavoidable since the file is passed as a string
|
||||
if csv_lines[0].lower() != headers:
|
||||
csv_lines.insert(0, headers)
|
||||
|
||||
# convert list of strings into CSV
|
||||
csv_data = csv.DictReader(csv_lines, quotechar='"', delimiter=',')
|
||||
|
||||
# build a Dict of possible TLP
|
||||
tlp_dict = get_tlps_dict()
|
||||
ret = []
|
||||
errors = []
|
||||
|
||||
index = 0
|
||||
for row in csv_data:
|
||||
|
||||
for e in headers.split(','):
|
||||
if row.get(e) is None:
|
||||
errors.append(f"{e} is missing for row {index}")
|
||||
index += 1
|
||||
continue
|
||||
|
||||
# IOC value must not be empty
|
||||
if not row.get("ioc_value"):
|
||||
errors.append(f"Empty IOC value for row {index}")
|
||||
track_activity(f"Attempted to upload an empty IOC value")
|
||||
index += 1
|
||||
continue
|
||||
|
||||
row["ioc_tags"] = row["ioc_tags"].replace("|", ",") # Reformat Tags
|
||||
|
||||
# Convert TLP into TLP id
|
||||
if row["ioc_tlp"] in tlp_dict:
|
||||
row["ioc_tlp_id"] = tlp_dict[row["ioc_tlp"]]
|
||||
else:
|
||||
row["ioc_tlp_id"] = ""
|
||||
row.pop("ioc_tlp", None)
|
||||
|
||||
type_id = get_ioc_type_id(row['ioc_type'].lower())
|
||||
if not type_id:
|
||||
errors.append(f"{row['ioc_value']} (invalid ioc type: {row['ioc_type']}) for row {index}")
|
||||
log.error(f'Unrecognised IOC type {row["ioc_type"]}')
|
||||
index += 1
|
||||
continue
|
||||
|
||||
row['ioc_type_id'] = type_id.type_id
|
||||
row.pop('ioc_type', None)
|
||||
|
||||
request_data = call_modules_hook('on_preload_ioc_create', data=row, caseid=caseid)
|
||||
|
||||
ioc = add_ioc_schema.load(request_data)
|
||||
ioc.custom_attributes = get_default_custom_attributes('ioc')
|
||||
ioc, existed = add_ioc(ioc=ioc,
|
||||
user_id=current_user.id,
|
||||
caseid=caseid
|
||||
)
|
||||
link_existed = add_ioc_link(ioc.ioc_id, caseid)
|
||||
|
||||
if link_existed:
|
||||
errors.append(f"{ioc.ioc_value} (already exists and linked to this case)")
|
||||
log.error(f"IOC {ioc.ioc_value} already exists and linked to this case")
|
||||
index += 1
|
||||
continue
|
||||
|
||||
if ioc:
|
||||
ioc = call_modules_hook('on_postload_ioc_create', data=ioc, caseid=caseid)
|
||||
ret.append(request_data)
|
||||
track_activity(f"added ioc \"{ioc.ioc_value}\"", caseid=caseid)
|
||||
|
||||
else:
|
||||
errors.append(f"{ioc.ioc_value} (internal reasons)")
|
||||
log.error(f"Unable to create IOC {ioc.ioc_value} for internal reasons")
|
||||
|
||||
index += 1
|
||||
|
||||
if len(errors) == 0:
|
||||
msg = "Successfully imported data."
|
||||
else:
|
||||
msg = "Data is imported but we got errors with the following rows:\n- " + "\n- ".join(errors)
|
||||
|
||||
return response_success(msg=msg, data=ret)
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages, status=400)
|
||||
|
||||
|
||||
@case_ioc_blueprint.route('/case/ioc/add/modal', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_add_ioc_modal(caseid):
|
||||
|
||||
form = ModalAddCaseIOCForm()
|
||||
form.ioc_type_id.choices = [(row['type_id'], row['type_name']) for row in get_ioc_types_list()]
|
||||
form.ioc_tlp_id.choices = get_tlps()
|
||||
|
||||
attributes = get_default_custom_attributes('ioc')
|
||||
|
||||
return render_template("modal_add_case_ioc.html", form=form, ioc=Ioc(), attributes=attributes)
|
||||
|
||||
|
||||
@case_ioc_blueprint.route('/case/ioc/delete/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_delete_ioc(cur_id, caseid):
|
||||
call_modules_hook('on_preload_ioc_delete', data=cur_id, caseid=caseid)
|
||||
ioc = get_ioc(cur_id, caseid)
|
||||
|
||||
if not ioc:
|
||||
return response_error('Not a valid IOC for this case')
|
||||
|
||||
if not delete_ioc(ioc, caseid):
|
||||
track_activity(f"unlinked IOC ID {ioc.ioc_value}", caseid=caseid)
|
||||
return response_success(f"IOC {cur_id} unlinked")
|
||||
|
||||
call_modules_hook('on_postload_ioc_delete', data=cur_id, caseid=caseid)
|
||||
|
||||
track_activity(f"deleted IOC \"{ioc.ioc_value}\"", caseid=caseid)
|
||||
return response_success(f"IOC {cur_id} deleted")
|
||||
|
||||
|
||||
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/modal', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_view_ioc_modal(cur_id, caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('case_assets.case_assets', cid=caseid, redirect=True))
|
||||
|
||||
form = ModalAddCaseIOCForm()
|
||||
ioc = get_ioc(cur_id, caseid)
|
||||
if not ioc:
|
||||
return response_error("Invalid IOC ID for this case")
|
||||
|
||||
form.ioc_type_id.choices = [(row['type_id'], row['type_name']) for row in get_ioc_types_list()]
|
||||
form.ioc_tlp_id.choices = get_tlps()
|
||||
|
||||
# Render the IOC
|
||||
form.ioc_tags.render_kw = {'value': ioc.ioc_tags}
|
||||
form.ioc_description.data = ioc.ioc_description
|
||||
form.ioc_value.data = ioc.ioc_value
|
||||
comments_map = get_case_iocs_comments_count([cur_id])
|
||||
|
||||
return render_template("modal_add_case_ioc.html", form=form, ioc=ioc, attributes=ioc.custom_attributes,
|
||||
comments_map=comments_map)
|
||||
|
||||
|
||||
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_view_ioc(cur_id, caseid):
|
||||
ioc_schema = IocSchema()
|
||||
ioc = get_ioc(cur_id, caseid)
|
||||
if not ioc:
|
||||
return response_error("Invalid IOC ID for this case")
|
||||
|
||||
return response_success(data=ioc_schema.dump(ioc))
|
||||
|
||||
|
||||
@case_ioc_blueprint.route('/case/ioc/update/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_update_ioc(cur_id, caseid):
|
||||
try:
|
||||
ioc = get_ioc(cur_id, caseid)
|
||||
if not ioc:
|
||||
return response_error("Invalid IOC ID for this case")
|
||||
|
||||
request_data = call_modules_hook('on_preload_ioc_update', data=request.get_json(), caseid=caseid)
|
||||
|
||||
# validate before saving
|
||||
ioc_schema = IocSchema()
|
||||
request_data['ioc_id'] = cur_id
|
||||
ioc_sc = ioc_schema.load(request_data, instance=ioc)
|
||||
ioc_sc.user_id = current_user.id
|
||||
|
||||
if not check_ioc_type_id(type_id=ioc_sc.ioc_type_id):
|
||||
return response_error("Not a valid IOC type")
|
||||
|
||||
update_ioc_state(caseid=caseid)
|
||||
db.session.commit()
|
||||
|
||||
ioc_sc = call_modules_hook('on_postload_ioc_update', data=ioc_sc, caseid=caseid)
|
||||
|
||||
if ioc_sc:
|
||||
track_activity(f"updated ioc \"{ioc_sc.ioc_value}\"", caseid=caseid)
|
||||
return response_success(f"Updated ioc \"{ioc_sc.ioc_value}\"", data=ioc_schema.dump(ioc))
|
||||
|
||||
return response_error("Unable to update ioc for internal reasons")
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages, status=400)
|
||||
|
||||
|
||||
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/modal', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_comment_ioc_modal(cur_id, caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('case_ioc.case_ioc', cid=caseid, redirect=True))
|
||||
|
||||
ioc = get_ioc(cur_id, caseid=caseid)
|
||||
if not ioc:
|
||||
return response_error('Invalid ioc ID')
|
||||
|
||||
return render_template("modal_conversation.html", element_id=cur_id, element_type='ioc',
|
||||
title=ioc.ioc_value)
|
||||
|
||||
|
||||
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/list', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_comment_ioc_list(cur_id, caseid):
|
||||
|
||||
ioc_comments = get_case_ioc_comments(cur_id)
|
||||
if ioc_comments is None:
|
||||
return response_error('Invalid ioc ID')
|
||||
|
||||
return response_success(data=CommentSchema(many=True).dump(ioc_comments))
|
||||
|
||||
|
||||
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/add', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_comment_ioc_add(cur_id, caseid):
|
||||
|
||||
try:
|
||||
ioc = get_ioc(cur_id, caseid=caseid)
|
||||
if not ioc:
|
||||
return response_error('Invalid ioc ID')
|
||||
|
||||
comment_schema = CommentSchema()
|
||||
|
||||
comment = comment_schema.load(request.get_json())
|
||||
comment.comment_case_id = caseid
|
||||
comment.comment_user_id = current_user.id
|
||||
comment.comment_date = datetime.now()
|
||||
comment.comment_update_date = datetime.now()
|
||||
db.session.add(comment)
|
||||
db.session.commit()
|
||||
|
||||
add_comment_to_ioc(ioc.ioc_id, comment.comment_id)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
hook_data = {
|
||||
"comment": comment_schema.dump(comment),
|
||||
"ioc": IocSchema().dump(ioc)
|
||||
}
|
||||
call_modules_hook('on_postload_ioc_commented', data=hook_data, caseid=caseid)
|
||||
|
||||
track_activity(f"ioc \"{ioc.ioc_value}\" commented", caseid=caseid)
|
||||
return response_success("Event commented", data=comment_schema.dump(comment))
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.normalized_messages(), status=400)
|
||||
|
||||
|
||||
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_comment_ioc_get(cur_id, com_id, caseid):
|
||||
|
||||
comment = get_case_ioc_comment(cur_id, com_id)
|
||||
if not comment:
|
||||
return response_error("Invalid comment ID")
|
||||
|
||||
return response_success(data=comment._asdict())
|
||||
|
||||
|
||||
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_comment_ioc_edit(cur_id, com_id, caseid):
|
||||
|
||||
return case_comment_update(com_id, 'ioc', caseid)
|
||||
|
||||
|
||||
@case_ioc_blueprint.route('/case/ioc/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_comment_ioc_delete(cur_id, com_id, caseid):
|
||||
|
||||
success, msg = delete_ioc_comment(cur_id, com_id)
|
||||
if not success:
|
||||
return response_error(msg)
|
||||
|
||||
call_modules_hook('on_postload_ioc_comment_delete', data=com_id, caseid=caseid)
|
||||
|
||||
track_activity(f"comment {com_id} on ioc {cur_id} deleted", caseid=caseid)
|
||||
return response_success(msg)
|
511
iris-web/source/app/blueprints/case/case_notes_routes.py
Normal file
511
iris-web/source/app/blueprints/case/case_notes_routes.py
Normal file
@ -0,0 +1,511 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# IRIS Source Code
|
||||
# Copyright (C) 2021 - Airbus CyberSecurity (SAS) - DFIR-IRIS Team
|
||||
# ir@cyberactionlab.net - contact@dfir-iris.org
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import marshmallow
|
||||
# IMPORTS ------------------------------------------------
|
||||
from datetime import datetime
|
||||
from flask import Blueprint
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
from flask import request
|
||||
from flask import url_for
|
||||
from flask_login import current_user
|
||||
from flask_socketio import emit, join_room, leave_room
|
||||
from flask_wtf import FlaskForm
|
||||
|
||||
from app import db, socket_io
|
||||
from app.blueprints.case.case_comments import case_comment_update
|
||||
from app.datamgmt.case.case_db import case_get_desc_crc
|
||||
from app.datamgmt.case.case_db import get_case
|
||||
from app.datamgmt.case.case_notes_db import add_comment_to_note
|
||||
from app.datamgmt.case.case_notes_db import add_note
|
||||
from app.datamgmt.case.case_notes_db import add_note_group
|
||||
from app.datamgmt.case.case_notes_db import delete_note
|
||||
from app.datamgmt.case.case_notes_db import delete_note_comment
|
||||
from app.datamgmt.case.case_notes_db import delete_note_group
|
||||
from app.datamgmt.case.case_notes_db import find_pattern_in_notes
|
||||
from app.datamgmt.case.case_notes_db import get_case_note_comment
|
||||
from app.datamgmt.case.case_notes_db import get_case_note_comments
|
||||
from app.datamgmt.case.case_notes_db import get_case_notes_comments_count
|
||||
from app.datamgmt.case.case_notes_db import get_group_details
|
||||
from app.datamgmt.case.case_notes_db import get_groups_short
|
||||
from app.datamgmt.case.case_notes_db import get_note
|
||||
from app.datamgmt.case.case_notes_db import get_notes_from_group
|
||||
from app.datamgmt.case.case_notes_db import update_note
|
||||
from app.datamgmt.case.case_notes_db import update_note_group
|
||||
from app.datamgmt.states import get_notes_state
|
||||
from app.forms import CaseNoteForm
|
||||
from app.iris_engine.module_handler.module_handler import call_modules_hook
|
||||
from app.iris_engine.utils.tracker import track_activity
|
||||
from app.models.authorization import CaseAccessLevel
|
||||
from app.schema.marshables import CaseAddNoteSchema
|
||||
from app.schema.marshables import CaseGroupNoteSchema
|
||||
from app.schema.marshables import CaseNoteSchema
|
||||
from app.schema.marshables import CommentSchema
|
||||
from app.util import ac_api_case_requires, ac_socket_requires
|
||||
from app.util import ac_case_requires
|
||||
from app.util import response_error
|
||||
from app.util import response_success
|
||||
|
||||
case_notes_blueprint = Blueprint('case_notes',
|
||||
__name__,
|
||||
template_folder='templates')
|
||||
|
||||
|
||||
# CONTENT ------------------------------------------------
|
||||
@case_notes_blueprint.route('/case/notes', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_notes(caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('case_notes.case_notes', cid=caseid, redirect=True))
|
||||
|
||||
form = FlaskForm()
|
||||
case = get_case(caseid)
|
||||
|
||||
if case:
|
||||
crc32, desc = case_get_desc_crc(caseid)
|
||||
else:
|
||||
crc32 = None
|
||||
desc = None
|
||||
|
||||
return render_template('case_notes.html', case=case, form=form, th_desc=desc, crc=crc32)
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/<int:cur_id>', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_note_detail(cur_id, caseid):
|
||||
try:
|
||||
note = get_note(cur_id, caseid=caseid)
|
||||
if not note:
|
||||
return response_error(msg="Invalid note ID")
|
||||
note_schema = CaseNoteSchema()
|
||||
|
||||
return response_success(data=note_schema.dump(note))
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages, status=400)
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/<int:cur_id>/modal', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_note_detail_modal(cur_id, caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('case_notes.case_notes', cid=caseid, redirect=True))
|
||||
|
||||
form = CaseNoteForm()
|
||||
|
||||
note = get_note(cur_id, caseid)
|
||||
ca = None
|
||||
|
||||
if note:
|
||||
form.content = note.note_content
|
||||
form.title = note.note_title
|
||||
form.note_title.render_kw = {"value": note.note_title}
|
||||
setattr(form, 'note_id', note.note_id)
|
||||
setattr(form, 'note_uuid', note.note_uuid)
|
||||
ca = note.custom_attributes
|
||||
comments_map = get_case_notes_comments_count([cur_id])
|
||||
|
||||
return render_template("modal_note_edit.html", note=form, id=cur_id, attributes=ca,
|
||||
ncid=note.note_case_id, comments_map=comments_map)
|
||||
|
||||
return response_error(f'Unable to find note ID {cur_id} for case {caseid}')
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/delete/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_note_delete(cur_id, caseid):
|
||||
|
||||
call_modules_hook('on_preload_note_delete', data=cur_id, caseid=caseid)
|
||||
|
||||
note = get_note(cur_id, caseid)
|
||||
if not note:
|
||||
return response_error("Invalid note ID for this case")
|
||||
|
||||
try:
|
||||
|
||||
delete_note(cur_id, caseid)
|
||||
|
||||
except Exception as e:
|
||||
return response_error("Unable to remove note", data=e.__traceback__)
|
||||
|
||||
call_modules_hook('on_postload_note_delete', data=cur_id, caseid=caseid)
|
||||
|
||||
track_activity(f"deleted note \"{note.note_title}\"", caseid=caseid)
|
||||
return response_success(f"Note deleted {cur_id}")
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/update/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_note_save(cur_id, caseid):
|
||||
|
||||
try:
|
||||
# validate before saving
|
||||
addnote_schema = CaseAddNoteSchema()
|
||||
|
||||
request_data = call_modules_hook('on_preload_note_update', data=request.get_json(), caseid=caseid)
|
||||
|
||||
request_data['note_id'] = cur_id
|
||||
addnote_schema.load(request_data, partial=['group_id'])
|
||||
|
||||
note = update_note(note_content=request_data.get('note_content'),
|
||||
note_title=request_data.get('note_title'),
|
||||
update_date=datetime.utcnow(),
|
||||
user_id=current_user.id,
|
||||
note_id=cur_id,
|
||||
caseid=caseid
|
||||
)
|
||||
if not note:
|
||||
|
||||
return response_error("Invalid note ID for this case")
|
||||
|
||||
note = call_modules_hook('on_postload_note_update', data=note, caseid=caseid)
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages, status=400)
|
||||
|
||||
track_activity(f"updated note \"{note.note_title}\"", caseid=caseid)
|
||||
return response_success(f"Note ID {cur_id} saved", data=addnote_schema.dump(note))
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/add', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_note_add(caseid):
|
||||
try:
|
||||
# validate before saving
|
||||
addnote_schema = CaseAddNoteSchema()
|
||||
request_data = call_modules_hook('on_preload_note_create', data=request.get_json(), caseid=caseid)
|
||||
|
||||
addnote_schema.verify_group_id(request_data, caseid=caseid)
|
||||
addnote_schema.load(request_data)
|
||||
|
||||
note = add_note(request_data.get('note_title'),
|
||||
datetime.utcnow(),
|
||||
current_user.id,
|
||||
caseid,
|
||||
request_data.get('group_id'),
|
||||
note_content=request_data.get('note_content'))
|
||||
|
||||
note = call_modules_hook('on_postload_note_create', data=note, caseid=caseid)
|
||||
|
||||
if note:
|
||||
casenote_schema = CaseNoteSchema()
|
||||
track_activity(f"added note \"{note.note_title}\"", caseid=caseid)
|
||||
return response_success('Note added', data=casenote_schema.dump(note))
|
||||
|
||||
return response_error("Unable to create note for internal reasons")
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages, status=400)
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/groups/list', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_load_notes_groups(caseid):
|
||||
|
||||
if not get_case(caseid=caseid):
|
||||
return response_error("Invalid case ID")
|
||||
|
||||
groups_short = get_groups_short(caseid)
|
||||
|
||||
sta = []
|
||||
|
||||
for group in groups_short:
|
||||
notes = get_notes_from_group(caseid=caseid, group_id=group.group_id)
|
||||
group_d = group._asdict()
|
||||
group_d['notes'] = [note._asdict() for note in notes]
|
||||
|
||||
sta.append(group_d)
|
||||
|
||||
sta = sorted(sta, key=lambda i: i['group_id'])
|
||||
|
||||
ret = {
|
||||
'groups': sta,
|
||||
'state': get_notes_state(caseid=caseid)
|
||||
}
|
||||
|
||||
return response_success("", data=ret)
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/state', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_notes_state(caseid):
|
||||
os = get_notes_state(caseid=caseid)
|
||||
if os:
|
||||
return response_success(data=os)
|
||||
else:
|
||||
return response_error('No notes state for this case.')
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/search', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_search_notes(caseid):
|
||||
|
||||
if request.is_json:
|
||||
search = request.json.get('search_term')
|
||||
ns = []
|
||||
if search:
|
||||
search = "%{}%".format(search)
|
||||
ns = find_pattern_in_notes(search, caseid)
|
||||
|
||||
ns = [row._asdict() for row in ns]
|
||||
|
||||
return response_success("", data=ns)
|
||||
|
||||
return response_error("Invalid request")
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/groups/add', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_add_notes_groups(caseid):
|
||||
title = ''
|
||||
if request.is_json:
|
||||
title = request.json.get('group_title') or ''
|
||||
|
||||
ng = add_note_group(group_title=title,
|
||||
caseid=caseid,
|
||||
userid=current_user.id,
|
||||
creationdate=datetime.utcnow())
|
||||
|
||||
if ng.group_id:
|
||||
group_schema = CaseGroupNoteSchema()
|
||||
track_activity(f"added group note \"{ng.group_title}\"", caseid=caseid)
|
||||
|
||||
return response_success("Notes group added", data=group_schema.dump(ng))
|
||||
|
||||
else:
|
||||
return response_error("Unable to add a new group")
|
||||
|
||||
return response_error("Invalid request")
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/groups/delete/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_delete_notes_groups(cur_id, caseid):
|
||||
|
||||
if not delete_note_group(cur_id, caseid):
|
||||
return response_error("Invalid group ID")
|
||||
|
||||
track_activity("deleted group note ID {}".format(cur_id), caseid=caseid)
|
||||
|
||||
return response_success("Group ID {} deleted".format(cur_id))
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/groups/<int:cur_id>', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_get_notes_group(cur_id, caseid):
|
||||
|
||||
group = get_group_details(cur_id, caseid)
|
||||
if not group:
|
||||
return response_error(f"Group ID {cur_id} not found")
|
||||
|
||||
return response_success("", data=group)
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/groups/update/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_edit_notes_groups(cur_id, caseid):
|
||||
|
||||
js_data = request.get_json()
|
||||
if not js_data:
|
||||
return response_error("Invalid data")
|
||||
|
||||
group_title = js_data.get('group_title')
|
||||
if not group_title:
|
||||
return response_error("Missing field group_title")
|
||||
|
||||
ng = update_note_group(group_title, cur_id, caseid)
|
||||
|
||||
if ng:
|
||||
# Note group has been properly found and updated in db
|
||||
track_activity("updated group note \"{}\"".format(group_title), caseid=caseid)
|
||||
group_schema = CaseGroupNoteSchema()
|
||||
return response_success("Updated title of group ID {}".format(cur_id), data=group_schema.dump(ng))
|
||||
|
||||
return response_error("Group ID {} not found".format(cur_id))
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/modal', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_comment_note_modal(cur_id, caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('case_note.case_note', cid=caseid, redirect=True))
|
||||
|
||||
note = get_note(cur_id, caseid=caseid)
|
||||
if not note:
|
||||
return response_error('Invalid note ID')
|
||||
|
||||
return render_template("modal_conversation.html", element_id=cur_id, element_type='notes',
|
||||
title=note.note_title)
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/list', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_comment_note_list(cur_id, caseid):
|
||||
|
||||
note_comments = get_case_note_comments(cur_id)
|
||||
if note_comments is None:
|
||||
return response_error('Invalid note ID')
|
||||
|
||||
return response_success(data=CommentSchema(many=True).dump(note_comments))
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/add', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_comment_note_add(cur_id, caseid):
|
||||
|
||||
try:
|
||||
note = get_note(cur_id, caseid=caseid)
|
||||
if not note:
|
||||
return response_error('Invalid note ID')
|
||||
|
||||
comment_schema = CommentSchema()
|
||||
|
||||
comment = comment_schema.load(request.get_json())
|
||||
comment.comment_case_id = caseid
|
||||
comment.comment_user_id = current_user.id
|
||||
comment.comment_date = datetime.now()
|
||||
comment.comment_update_date = datetime.now()
|
||||
db.session.add(comment)
|
||||
db.session.commit()
|
||||
|
||||
add_comment_to_note(note.note_id, comment.comment_id)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
hook_data = {
|
||||
"comment": comment_schema.dump(comment),
|
||||
"note": CaseNoteSchema().dump(note)
|
||||
}
|
||||
call_modules_hook('on_postload_note_commented', data=hook_data, caseid=caseid)
|
||||
|
||||
track_activity("note \"{}\" commented".format(note.note_title), caseid=caseid)
|
||||
return response_success("Event commented", data=comment_schema.dump(comment))
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.normalized_messages(), status=400)
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_comment_note_get(cur_id, com_id, caseid):
|
||||
|
||||
comment = get_case_note_comment(cur_id, com_id)
|
||||
if not comment:
|
||||
return response_error("Invalid comment ID")
|
||||
|
||||
return response_success(data=comment._asdict())
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_comment_note_edit(cur_id, com_id, caseid):
|
||||
|
||||
return case_comment_update(com_id, 'notes', caseid)
|
||||
|
||||
|
||||
@case_notes_blueprint.route('/case/notes/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_comment_note_delete(cur_id, com_id, caseid):
|
||||
|
||||
success, msg = delete_note_comment(cur_id, com_id)
|
||||
if not success:
|
||||
return response_error(msg)
|
||||
|
||||
call_modules_hook('on_postload_note_comment_delete', data=com_id, caseid=caseid)
|
||||
|
||||
track_activity(f"comment {com_id} on note {cur_id} deleted", caseid=caseid)
|
||||
return response_success(msg)
|
||||
|
||||
|
||||
@socket_io.on('change-note')
|
||||
@ac_socket_requires(CaseAccessLevel.full_access)
|
||||
def socket_change_note(data):
|
||||
|
||||
data['last_change'] = current_user.user
|
||||
emit('change-note', data, to=data['channel'], skip_sid=request.sid, room=data['channel'])
|
||||
|
||||
|
||||
@socket_io.on('save-note')
|
||||
@ac_socket_requires(CaseAccessLevel.full_access)
|
||||
def socket_save_note(data):
|
||||
|
||||
data['last_saved'] = current_user.user
|
||||
emit('save-note', data, to=data['channel'], skip_sid=request.sid, room=data['channel'])
|
||||
|
||||
|
||||
@socket_io.on('clear_buffer-note')
|
||||
@ac_socket_requires(CaseAccessLevel.full_access)
|
||||
def socket_clear_buffer_note(message):
|
||||
|
||||
emit('clear_buffer-note', message, room=message['channel'])
|
||||
|
||||
|
||||
@socket_io.on('join-notes')
|
||||
@ac_socket_requires(CaseAccessLevel.full_access)
|
||||
def socket_join_note(data):
|
||||
|
||||
room = data['channel']
|
||||
join_room(room=room)
|
||||
|
||||
emit('join-notes', {
|
||||
'message': f"{current_user.user} just joined",
|
||||
"user": current_user.user
|
||||
}, room=room)
|
||||
|
||||
|
||||
@socket_io.on('ping-note')
|
||||
@ac_socket_requires(CaseAccessLevel.full_access)
|
||||
def socket_ping_note(data):
|
||||
|
||||
emit('ping-note', {"user": current_user.name, "note_id": data['note_id']}, room=data['channel'])
|
||||
|
||||
|
||||
@socket_io.on('pong-note')
|
||||
@ac_socket_requires(CaseAccessLevel.full_access)
|
||||
def socket_ping_note(data):
|
||||
|
||||
emit('pong-note', {"user": current_user.name, "note_id": data['note_id']}, room=data['channel'])
|
||||
|
||||
|
||||
@socket_io.on('overview-map-note')
|
||||
@ac_socket_requires(CaseAccessLevel.full_access)
|
||||
def socket_overview_map_note(data):
|
||||
|
||||
emit('overview-map-note', {"user": current_user.user, "note_id": data['note_id']}, room=data['channel'])
|
||||
|
||||
|
||||
@socket_io.on('join-notes-overview')
|
||||
@ac_socket_requires(CaseAccessLevel.full_access)
|
||||
def socket_join_overview(data):
|
||||
|
||||
room = data['channel']
|
||||
join_room(room=room)
|
||||
|
||||
emit('join-notes-overview', {
|
||||
'message': f"{current_user.user} just joined",
|
||||
"user": current_user.user
|
||||
}, room=room)
|
||||
|
||||
|
||||
@socket_io.on('disconnect')
|
||||
@ac_socket_requires(CaseAccessLevel.full_access)
|
||||
def socket_disconnect(data):
|
||||
emit('disconnect', current_user.user, broadcast=True)
|
306
iris-web/source/app/blueprints/case/case_rfiles_routes.py
Normal file
306
iris-web/source/app/blueprints/case/case_rfiles_routes.py
Normal file
@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# IRIS Source Code
|
||||
# Copyright (C) 2021 - Airbus CyberSecurity (SAS) - DFIR-IRIS Team
|
||||
# ir@cyberactionlab.net - contact@dfir-iris.org
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# IMPORTS ------------------------------------------------
|
||||
from datetime import datetime
|
||||
|
||||
import marshmallow
|
||||
from flask import Blueprint
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
from flask import request
|
||||
from flask import url_for
|
||||
from flask_login import current_user
|
||||
from flask_wtf import FlaskForm
|
||||
|
||||
from app import db
|
||||
from app.blueprints.case.case_comments import case_comment_update
|
||||
from app.datamgmt.case.case_db import get_case
|
||||
from app.datamgmt.case.case_rfiles_db import add_comment_to_evidence
|
||||
from app.datamgmt.case.case_rfiles_db import add_rfile
|
||||
from app.datamgmt.case.case_rfiles_db import delete_evidence_comment
|
||||
from app.datamgmt.case.case_rfiles_db import delete_rfile
|
||||
from app.datamgmt.case.case_rfiles_db import get_case_evidence_comment
|
||||
from app.datamgmt.case.case_rfiles_db import get_case_evidence_comments
|
||||
from app.datamgmt.case.case_rfiles_db import get_case_evidence_comments_count
|
||||
from app.datamgmt.case.case_rfiles_db import get_rfile
|
||||
from app.datamgmt.case.case_rfiles_db import get_rfiles
|
||||
from app.datamgmt.case.case_rfiles_db import update_rfile
|
||||
from app.datamgmt.manage.manage_attribute_db import get_default_custom_attributes
|
||||
from app.datamgmt.states import get_evidences_state
|
||||
from app.iris_engine.module_handler.module_handler import call_modules_hook
|
||||
from app.iris_engine.utils.tracker import track_activity
|
||||
from app.models.authorization import CaseAccessLevel
|
||||
from app.schema.marshables import CaseEvidenceSchema
|
||||
from app.schema.marshables import CommentSchema
|
||||
from app.util import ac_api_case_requires
|
||||
from app.util import ac_case_requires
|
||||
from app.util import response_error
|
||||
from app.util import response_success
|
||||
|
||||
case_rfiles_blueprint = Blueprint(
|
||||
'case_rfiles',
|
||||
__name__,
|
||||
template_folder='templates'
|
||||
)
|
||||
|
||||
|
||||
# CONTENT ------------------------------------------------
|
||||
@case_rfiles_blueprint.route('/case/evidences', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_rfile(caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('case_rfiles.case_rfile', cid=caseid, redirect=True))
|
||||
|
||||
form = FlaskForm()
|
||||
case = get_case(caseid)
|
||||
|
||||
return render_template("case_rfile.html", case=case, form=form)
|
||||
|
||||
|
||||
@case_rfiles_blueprint.route('/case/evidences/list', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_list_rfiles(caseid):
|
||||
crf = get_rfiles(caseid)
|
||||
|
||||
ret = {
|
||||
"evidences": [row._asdict() for row in crf],
|
||||
"state": get_evidences_state(caseid=caseid)
|
||||
}
|
||||
|
||||
return response_success("", data=ret)
|
||||
|
||||
|
||||
@case_rfiles_blueprint.route('/case/evidences/state', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_rfiles_state(caseid):
|
||||
os = get_evidences_state(caseid=caseid)
|
||||
if os:
|
||||
return response_success(data=os)
|
||||
else:
|
||||
return response_error('No evidences state for this case.')
|
||||
|
||||
|
||||
@case_rfiles_blueprint.route('/case/evidences/add', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_add_rfile(caseid):
|
||||
|
||||
try:
|
||||
# validate before saving
|
||||
evidence_schema = CaseEvidenceSchema()
|
||||
|
||||
request_data = call_modules_hook('on_preload_evidence_create', data=request.get_json(), caseid=caseid)
|
||||
|
||||
evidence = evidence_schema.load(request_data)
|
||||
|
||||
crf = add_rfile(evidence=evidence,
|
||||
user_id=current_user.id,
|
||||
caseid=caseid
|
||||
)
|
||||
|
||||
crf = call_modules_hook('on_postload_evidence_create', data=crf, caseid=caseid)
|
||||
|
||||
if crf:
|
||||
track_activity(f"added evidence \"{crf.filename}\"", caseid=caseid)
|
||||
return response_success("Evidence added", data=evidence_schema.dump(crf))
|
||||
|
||||
return response_error("Unable to create task for internal reasons")
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages, status=400)
|
||||
|
||||
|
||||
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_get_evidence(cur_id, caseid):
|
||||
crf = get_rfile(cur_id, caseid)
|
||||
if not crf:
|
||||
return response_error("Invalid evidence ID for this case")
|
||||
|
||||
evidence_schema = CaseEvidenceSchema()
|
||||
return response_success(data=evidence_schema.dump(crf))
|
||||
|
||||
|
||||
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/modal', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_edit_rfile_modal(cur_id, caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('case_rfiles.case_rfile', cid=caseid, redirect=True))
|
||||
|
||||
crf = get_rfile(cur_id, caseid)
|
||||
if not crf:
|
||||
return response_error("Invalid evidence ID for this case")
|
||||
|
||||
comments_map = get_case_evidence_comments_count([cur_id])
|
||||
|
||||
return render_template("modal_add_case_rfile.html", rfile=crf, attributes=crf.custom_attributes,
|
||||
comments_map=comments_map)
|
||||
|
||||
|
||||
@case_rfiles_blueprint.route('/case/evidences/add/modal', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_add_rfile_modal(caseid):
|
||||
|
||||
return render_template("modal_add_case_rfile.html", rfile=None, attributes=get_default_custom_attributes('evidence'))
|
||||
|
||||
|
||||
@case_rfiles_blueprint.route('/case/evidences/update/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_edit_rfile(cur_id, caseid):
|
||||
|
||||
try:
|
||||
# validate before saving
|
||||
evidence_schema = CaseEvidenceSchema()
|
||||
|
||||
request_data = call_modules_hook('on_preload_evidence_update', data=request.get_json(), caseid=caseid)
|
||||
|
||||
crf = get_rfile(cur_id, caseid)
|
||||
if not crf:
|
||||
return response_error("Invalid evidence ID for this case")
|
||||
|
||||
request_data['id'] = cur_id
|
||||
evidence = evidence_schema.load(request_data, instance=crf)
|
||||
|
||||
evd = update_rfile(evidence=evidence,
|
||||
user_id=current_user.id,
|
||||
caseid=caseid
|
||||
)
|
||||
|
||||
evd = call_modules_hook('on_postload_evidence_update', data=evd, caseid=caseid)
|
||||
|
||||
if evd:
|
||||
track_activity(f"updated evidence \"{evd.filename}\"", caseid=caseid)
|
||||
return response_success("Evidence {} updated".format(evd.filename), data=evidence_schema.dump(evd))
|
||||
|
||||
return response_error("Unable to update task for internal reasons")
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages, status=400)
|
||||
|
||||
|
||||
@case_rfiles_blueprint.route('/case/evidences/delete/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_delete_rfile(cur_id, caseid):
|
||||
|
||||
call_modules_hook('on_preload_evidence_delete', data=cur_id, caseid=caseid)
|
||||
crf = get_rfile(cur_id, caseid)
|
||||
if not crf:
|
||||
return response_error("Invalid evidence ID for this case")
|
||||
|
||||
delete_rfile(cur_id, caseid=caseid)
|
||||
|
||||
call_modules_hook('on_postload_evidence_delete', data=cur_id, caseid=caseid)
|
||||
|
||||
track_activity(f"deleted evidence \"{crf.filename}\" from registry", caseid)
|
||||
|
||||
return response_success("Evidence deleted")
|
||||
|
||||
|
||||
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/modal', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_comment_evidence_modal(cur_id, caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('case_task.case_task', cid=caseid, redirect=True))
|
||||
|
||||
evidence = get_rfile(cur_id, caseid=caseid)
|
||||
if not evidence:
|
||||
return response_error('Invalid evidence ID')
|
||||
|
||||
return render_template("modal_conversation.html", element_id=cur_id, element_type='evidences',
|
||||
title=evidence.filename)
|
||||
|
||||
|
||||
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/list', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_comment_evidence_list(cur_id, caseid):
|
||||
|
||||
evidence_comments = get_case_evidence_comments(cur_id)
|
||||
if evidence_comments is None:
|
||||
return response_error('Invalid evidence ID')
|
||||
|
||||
return response_success(data=CommentSchema(many=True).dump(evidence_comments))
|
||||
|
||||
|
||||
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/add', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_comment_evidence_add(cur_id, caseid):
|
||||
|
||||
try:
|
||||
evidence = get_rfile(cur_id, caseid=caseid)
|
||||
if not evidence:
|
||||
return response_error('Invalid evidence ID')
|
||||
|
||||
comment_schema = CommentSchema()
|
||||
|
||||
comment = comment_schema.load(request.get_json())
|
||||
comment.comment_case_id = caseid
|
||||
comment.comment_user_id = current_user.id
|
||||
comment.comment_date = datetime.now()
|
||||
comment.comment_update_date = datetime.now()
|
||||
db.session.add(comment)
|
||||
db.session.commit()
|
||||
|
||||
add_comment_to_evidence(evidence.id, comment.comment_id)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
hook_data = {
|
||||
"comment": comment_schema.dump(comment),
|
||||
"evidence": CaseEvidenceSchema().dump(evidence)
|
||||
}
|
||||
call_modules_hook('on_postload_evidence_commented', data=hook_data, caseid=caseid)
|
||||
|
||||
track_activity(f"evidence \"{evidence.filename}\" commented", caseid=caseid)
|
||||
return response_success("Event commented", data=comment_schema.dump(comment))
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.normalized_messages(), status=400)
|
||||
|
||||
|
||||
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_comment_evidence_get(cur_id, com_id, caseid):
|
||||
|
||||
comment = get_case_evidence_comment(cur_id, com_id)
|
||||
if not comment:
|
||||
return response_error("Invalid comment ID")
|
||||
|
||||
return response_success(data=comment._asdict())
|
||||
|
||||
|
||||
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_comment_evidence_edit(cur_id, com_id, caseid):
|
||||
|
||||
return case_comment_update(com_id, 'tasks', caseid)
|
||||
|
||||
|
||||
@case_rfiles_blueprint.route('/case/evidences/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_comment_evidence_delete(cur_id, com_id, caseid):
|
||||
|
||||
success, msg = delete_evidence_comment(cur_id, com_id)
|
||||
if not success:
|
||||
return response_error(msg)
|
||||
|
||||
call_modules_hook('on_postload_evidence_comment_delete', data=com_id, caseid=caseid)
|
||||
|
||||
track_activity(f"comment {com_id} on evidence {cur_id} deleted", caseid=caseid)
|
||||
return response_success(msg)
|
449
iris-web/source/app/blueprints/case/case_routes.py
Normal file
449
iris-web/source/app/blueprints/case/case_routes.py
Normal file
@ -0,0 +1,449 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# IRIS Source Code
|
||||
# Copyright (C) 2021 - Airbus CyberSecurity (SAS) - DFIR-IRIS Team
|
||||
# ir@cyberactionlab.net - contact@dfir-iris.org
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import binascii
|
||||
import marshmallow
|
||||
# IMPORTS ------------------------------------------------
|
||||
import traceback
|
||||
from flask import Blueprint
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
from flask import request
|
||||
from flask import url_for
|
||||
from flask_login import current_user
|
||||
from flask_socketio import emit
|
||||
from flask_socketio import join_room
|
||||
from flask_wtf import FlaskForm
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy import desc
|
||||
|
||||
from app import app
|
||||
from app import db
|
||||
from app import socket_io
|
||||
from app.blueprints.case.case_assets_routes import case_assets_blueprint
|
||||
from app.blueprints.case.case_graphs_routes import case_graph_blueprint
|
||||
from app.blueprints.case.case_ioc_routes import case_ioc_blueprint
|
||||
from app.blueprints.case.case_notes_routes import case_notes_blueprint
|
||||
from app.blueprints.case.case_rfiles_routes import case_rfiles_blueprint
|
||||
from app.blueprints.case.case_tasks_routes import case_tasks_blueprint
|
||||
from app.blueprints.case.case_timeline_routes import case_timeline_blueprint
|
||||
from app.datamgmt.case.case_db import case_exists, get_review_id_from_name
|
||||
from app.datamgmt.case.case_db import case_get_desc_crc
|
||||
from app.datamgmt.case.case_db import get_activities_report_template
|
||||
from app.datamgmt.case.case_db import get_case
|
||||
from app.datamgmt.case.case_db import get_case_report_template
|
||||
from app.datamgmt.case.case_db import get_case_tags
|
||||
from app.datamgmt.manage.manage_groups_db import add_case_access_to_group
|
||||
from app.datamgmt.manage.manage_groups_db import get_group_with_members
|
||||
from app.datamgmt.manage.manage_groups_db import get_groups_list
|
||||
from app.datamgmt.manage.manage_users_db import get_user
|
||||
from app.datamgmt.manage.manage_users_db import get_users_list_restricted_from_case
|
||||
from app.datamgmt.manage.manage_users_db import set_user_case_access
|
||||
from app.datamgmt.reporter.report_db import export_case_json
|
||||
from app.forms import PipelinesCaseForm
|
||||
from app.iris_engine.access_control.utils import ac_get_all_access_level, ac_fast_check_current_user_has_case_access, \
|
||||
ac_fast_check_user_has_case_access
|
||||
from app.iris_engine.access_control.utils import ac_set_case_access_for_users
|
||||
from app.iris_engine.module_handler.module_handler import list_available_pipelines
|
||||
from app.iris_engine.utils.tracker import track_activity
|
||||
from app.models import CaseStatus, ReviewStatusList
|
||||
from app.models import UserActivity
|
||||
from app.models.authorization import CaseAccessLevel
|
||||
from app.models.authorization import User
|
||||
from app.schema.marshables import TaskLogSchema, CaseSchema
|
||||
from app.util import ac_api_case_requires, add_obj_history_entry
|
||||
from app.util import ac_case_requires
|
||||
from app.util import ac_socket_requires
|
||||
from app.util import response_error
|
||||
from app.util import response_success
|
||||
|
||||
app.register_blueprint(case_timeline_blueprint)
|
||||
app.register_blueprint(case_notes_blueprint)
|
||||
app.register_blueprint(case_assets_blueprint)
|
||||
app.register_blueprint(case_ioc_blueprint)
|
||||
app.register_blueprint(case_rfiles_blueprint)
|
||||
app.register_blueprint(case_graph_blueprint)
|
||||
app.register_blueprint(case_tasks_blueprint)
|
||||
|
||||
case_blueprint = Blueprint('case',
|
||||
__name__,
|
||||
template_folder='templates')
|
||||
|
||||
event_tags = ["Network", "Server", "ActiveDirectory", "Computer", "Malware", "User Interaction"]
|
||||
|
||||
|
||||
log = app.logger
|
||||
|
||||
|
||||
# CONTENT ------------------------------------------------
|
||||
@case_blueprint.route('/case', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_r(caseid, url_redir):
|
||||
|
||||
if url_redir:
|
||||
return redirect(url_for('case.case_r', cid=caseid, redirect=True))
|
||||
|
||||
case = get_case(caseid)
|
||||
setattr(case, 'case_tags', get_case_tags(caseid))
|
||||
form = FlaskForm()
|
||||
|
||||
reports = get_case_report_template()
|
||||
reports = [row for row in reports]
|
||||
|
||||
reports_act = get_activities_report_template()
|
||||
reports_act = [row for row in reports_act]
|
||||
|
||||
if not case:
|
||||
return render_template('select_case.html')
|
||||
|
||||
desc_crc32, description = case_get_desc_crc(caseid)
|
||||
setattr(case, 'status_name', CaseStatus(case.status_id).name.replace('_', ' ').title())
|
||||
|
||||
return render_template('case.html', case=case, desc=description, crc=desc_crc32,
|
||||
reports=reports, reports_act=reports_act, form=form)
|
||||
|
||||
|
||||
@case_blueprint.route('/case/exists', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_exists_r(caseid):
|
||||
|
||||
if case_exists(caseid):
|
||||
return response_success('Case exists')
|
||||
else:
|
||||
return response_error('Case does not exist', 404)
|
||||
|
||||
|
||||
@case_blueprint.route('/case/pipelines-modal', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.full_access)
|
||||
def case_pipelines_modal(caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('case.case_r', cid=caseid, redirect=True))
|
||||
|
||||
case = get_case(caseid)
|
||||
|
||||
form = PipelinesCaseForm()
|
||||
|
||||
pl = list_available_pipelines()
|
||||
|
||||
form.pipeline.choices = [("{}-{}".format(ap[0], ap[1]['pipeline_internal_name']),
|
||||
ap[1]['pipeline_human_name'])for ap in pl]
|
||||
|
||||
# Return default page of case management
|
||||
pipeline_args = [("{}-{}".format(ap[0], ap[1]['pipeline_internal_name']),
|
||||
ap[1]['pipeline_human_name'], ap[1]['pipeline_args'])for ap in pl]
|
||||
|
||||
return render_template('modal_case_pipelines.html', case=case, form=form, pipeline_args=pipeline_args)
|
||||
|
||||
|
||||
@socket_io.on('change')
|
||||
@ac_socket_requires(CaseAccessLevel.full_access)
|
||||
def socket_summary_onchange(data):
|
||||
|
||||
data['last_change'] = current_user.user
|
||||
emit('change', data, to=data['channel'], skip_sid=request.sid)
|
||||
|
||||
|
||||
@socket_io.on('save')
|
||||
@ac_socket_requires(CaseAccessLevel.full_access)
|
||||
def socket_summary_onsave(data):
|
||||
|
||||
data['last_saved'] = current_user.user
|
||||
emit('save', data, to=data['channel'], skip_sid=request.sid)
|
||||
|
||||
|
||||
@socket_io.on('clear_buffer')
|
||||
@ac_socket_requires(CaseAccessLevel.full_access)
|
||||
def socket_summary_onchange(message):
|
||||
|
||||
emit('clear_buffer', message)
|
||||
|
||||
|
||||
@socket_io.on('join')
|
||||
@ac_socket_requires(CaseAccessLevel.full_access)
|
||||
def get_message(data):
|
||||
|
||||
room = data['channel']
|
||||
join_room(room=room)
|
||||
emit('join', {'message': f"{current_user.user} just joined"}, room=room)
|
||||
|
||||
|
||||
@case_blueprint.route('/case/summary/update', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def desc_fetch(caseid):
|
||||
|
||||
js_data = request.get_json()
|
||||
case = get_case(caseid)
|
||||
if not case:
|
||||
return response_error('Invalid case ID')
|
||||
|
||||
case.description = js_data.get('case_description')
|
||||
crc = binascii.crc32(case.description.encode('utf-8'))
|
||||
db.session.commit()
|
||||
track_activity("updated summary", caseid)
|
||||
|
||||
if not request.cookies.get('session'):
|
||||
# API call so we propagate the message to everyone
|
||||
data = {
|
||||
"case_description": case.description,
|
||||
"last_saved": current_user.user
|
||||
}
|
||||
socket_io.emit('save', data, to=f"case-{caseid}")
|
||||
|
||||
return response_success("Summary updated", data=crc)
|
||||
|
||||
|
||||
@case_blueprint.route('/case/summary/fetch', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def summary_fetch(caseid):
|
||||
desc_crc32, description = case_get_desc_crc(caseid)
|
||||
|
||||
return response_success("Summary fetch", data={'case_description': description, 'crc32': desc_crc32})
|
||||
|
||||
|
||||
@case_blueprint.route('/case/activities/list', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def activity_fetch(caseid):
|
||||
ua = UserActivity.query.with_entities(
|
||||
UserActivity.activity_date,
|
||||
User.name,
|
||||
UserActivity.activity_desc,
|
||||
UserActivity.is_from_api
|
||||
).filter(and_(
|
||||
UserActivity.case_id == caseid,
|
||||
UserActivity.display_in_ui == True
|
||||
)).join(
|
||||
UserActivity.user
|
||||
).order_by(
|
||||
desc(UserActivity.activity_date)
|
||||
).limit(40).all()
|
||||
|
||||
output = [a._asdict() for a in ua]
|
||||
|
||||
return response_success("", data=output)
|
||||
|
||||
|
||||
@case_blueprint.route("/case/export", methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def export_case(caseid):
|
||||
return response_success('', data=export_case_json(caseid))
|
||||
|
||||
|
||||
@case_blueprint.route('/case/tasklog/add', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_add_tasklog(caseid):
|
||||
|
||||
log_schema = TaskLogSchema()
|
||||
|
||||
try:
|
||||
|
||||
log_data = log_schema.load(request.get_json())
|
||||
|
||||
ua = track_activity(log_data.get('log_content'), caseid, user_input=True)
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages, status=400)
|
||||
|
||||
return response_success("Log saved", data=ua)
|
||||
|
||||
|
||||
@case_blueprint.route('/case/users/list', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_get_users(caseid):
|
||||
|
||||
users = get_users_list_restricted_from_case(caseid)
|
||||
|
||||
return response_success(data=users)
|
||||
|
||||
|
||||
@case_blueprint.route('/case/groups/access/modal', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.full_access)
|
||||
def groups_cac_view(caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('case.case_r', cid=caseid, redirect=True))
|
||||
|
||||
groups = get_groups_list()
|
||||
access_levels = ac_get_all_access_level()
|
||||
|
||||
return render_template('modal_cac_to_groups.html', groups=groups, access_levels=access_levels, caseid=caseid)
|
||||
|
||||
|
||||
@case_blueprint.route('/case/access/set-group', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def group_cac_set_case(caseid):
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return response_error("Invalid request")
|
||||
|
||||
if data.get('case_id') != caseid:
|
||||
return response_error("Inconsistent case ID")
|
||||
|
||||
case = get_case(caseid)
|
||||
if not case:
|
||||
return response_error("Invalid case ID")
|
||||
|
||||
group_id = data.get('group_id')
|
||||
access_level = data.get('access_level')
|
||||
|
||||
group = get_group_with_members(group_id)
|
||||
|
||||
try:
|
||||
|
||||
success, logs = add_case_access_to_group(group, [data.get('case_id')], access_level)
|
||||
|
||||
if success:
|
||||
success, logs = ac_set_case_access_for_users(group.group_members, caseid, access_level)
|
||||
|
||||
except Exception as e:
|
||||
log.error("Error while setting case access for group: {}".format(e))
|
||||
log.error(traceback.format_exc())
|
||||
return response_error(msg=str(e))
|
||||
|
||||
if success:
|
||||
track_activity("case access set to {} for group {}".format(data.get('access_level'), group_id), caseid)
|
||||
add_obj_history_entry(case, "access changed to {} for group {}".format(data.get('access_level'), group_id),
|
||||
commit=True)
|
||||
|
||||
return response_success(msg=logs)
|
||||
|
||||
return response_error(msg=logs)
|
||||
|
||||
|
||||
@case_blueprint.route('/case/access/set-user', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def user_cac_set_case(caseid):
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return response_error("Invalid request")
|
||||
|
||||
if data.get('user_id') == current_user.id:
|
||||
return response_error("I can't let you do that, Dave")
|
||||
|
||||
user = get_user(data.get('user_id'))
|
||||
if not user:
|
||||
return response_error("Invalid user ID")
|
||||
|
||||
if data.get('case_id') != caseid:
|
||||
return response_error("Inconsistent case ID")
|
||||
|
||||
case = get_case(caseid)
|
||||
if not case:
|
||||
return response_error('Invalid case ID')
|
||||
|
||||
try:
|
||||
|
||||
success, logs = set_user_case_access(user.id, data.get('case_id'), data.get('access_level'))
|
||||
track_activity("case access set to {} for user {}".format(data.get('access_level'), user.name), caseid)
|
||||
add_obj_history_entry(case, "access changed to {} for user {}".format(data.get('access_level'), user.name))
|
||||
|
||||
db.session.commit()
|
||||
|
||||
except Exception as e:
|
||||
log.error("Error while setting case access for user: {}".format(e))
|
||||
log.error(traceback.format_exc())
|
||||
return response_error(msg=str(e))
|
||||
|
||||
if success:
|
||||
return response_success(msg=logs)
|
||||
|
||||
return response_error(msg=logs)
|
||||
|
||||
|
||||
@case_blueprint.route('/case/update-status', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_update_status(caseid):
|
||||
|
||||
case = get_case(caseid)
|
||||
if not case:
|
||||
return response_error('Invalid case ID')
|
||||
|
||||
status = request.get_json().get('status_id')
|
||||
case_status = set(item.value for item in CaseStatus)
|
||||
|
||||
try:
|
||||
status = int(status)
|
||||
except ValueError:
|
||||
return response_error('Invalid status')
|
||||
except TypeError:
|
||||
return response_error('Invalid status. Expected int')
|
||||
|
||||
if status not in case_status:
|
||||
return response_error('Invalid status')
|
||||
|
||||
case.status_id = status
|
||||
add_obj_history_entry(case, f'status updated to {CaseStatus(status).name}')
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return response_success("Case status updated", data=case.status_id)
|
||||
|
||||
|
||||
@case_blueprint.route('/case/md-helper', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_md_helper(caseid, url_redir):
|
||||
|
||||
return render_template('case_md_helper.html')
|
||||
|
||||
|
||||
@case_blueprint.route('/case/review/update', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_review(caseid):
|
||||
|
||||
case = get_case(caseid)
|
||||
if not case:
|
||||
return response_error('Invalid case ID')
|
||||
|
||||
action = request.get_json().get('action')
|
||||
reviewer_id = request.get_json().get('reviewer_id')
|
||||
|
||||
if action == 'start':
|
||||
review_name = ReviewStatusList.review_in_progress
|
||||
elif action == 'cancel' or action == 'request':
|
||||
review_name = ReviewStatusList.pending_review
|
||||
elif action == 'no_review':
|
||||
review_name = ReviewStatusList.no_review_required
|
||||
elif action == 'to_review':
|
||||
review_name = ReviewStatusList.not_reviewed
|
||||
elif action == 'done':
|
||||
review_name = ReviewStatusList.reviewed
|
||||
else:
|
||||
return response_error('Invalid action')
|
||||
|
||||
case.review_status_id = get_review_id_from_name(review_name)
|
||||
if reviewer_id:
|
||||
try:
|
||||
reviewer_id = int(reviewer_id)
|
||||
except ValueError:
|
||||
return response_error('Invalid reviewer ID')
|
||||
|
||||
if not ac_fast_check_user_has_case_access(reviewer_id, caseid, [CaseAccessLevel.full_access]):
|
||||
return response_error('Invalid reviewer ID')
|
||||
|
||||
case.reviewer_id = reviewer_id
|
||||
|
||||
db.session.commit()
|
||||
|
||||
add_obj_history_entry(case, f'review status updated to {review_name}')
|
||||
track_activity(f'review status updated to {review_name}', caseid)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return response_success("Case review updated", data=CaseSchema().dump(case))
|
368
iris-web/source/app/blueprints/case/case_tasks_routes.py
Normal file
368
iris-web/source/app/blueprints/case/case_tasks_routes.py
Normal file
@ -0,0 +1,368 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# IRIS Source Code
|
||||
# Copyright (C) 2021 - Airbus CyberSecurity (SAS) - DFIR-IRIS Team
|
||||
# ir@cyberactionlab.net - contact@dfir-iris.org
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# IMPORTS ------------------------------------------------
|
||||
from datetime import datetime
|
||||
|
||||
import marshmallow
|
||||
from flask import Blueprint
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
from flask import request
|
||||
from flask import url_for
|
||||
from flask_login import current_user
|
||||
from flask_wtf import FlaskForm
|
||||
|
||||
from app import db
|
||||
from app.blueprints.case.case_comments import case_comment_update
|
||||
from app.datamgmt.case.case_db import get_case
|
||||
from app.datamgmt.case.case_tasks_db import add_comment_to_task
|
||||
from app.datamgmt.case.case_tasks_db import add_task
|
||||
from app.datamgmt.case.case_tasks_db import delete_task
|
||||
from app.datamgmt.case.case_tasks_db import delete_task_comment
|
||||
from app.datamgmt.case.case_tasks_db import get_case_task_comment
|
||||
from app.datamgmt.case.case_tasks_db import get_case_task_comments
|
||||
from app.datamgmt.case.case_tasks_db import get_case_tasks_comments_count
|
||||
from app.datamgmt.case.case_tasks_db import get_task
|
||||
from app.datamgmt.case.case_tasks_db import get_task_with_assignees
|
||||
from app.datamgmt.case.case_tasks_db import get_tasks_status
|
||||
from app.datamgmt.case.case_tasks_db import get_tasks_with_assignees
|
||||
from app.datamgmt.case.case_tasks_db import update_task_assignees
|
||||
from app.datamgmt.case.case_tasks_db import update_task_status
|
||||
from app.datamgmt.manage.manage_attribute_db import get_default_custom_attributes
|
||||
from app.datamgmt.states import get_tasks_state
|
||||
from app.datamgmt.states import update_tasks_state
|
||||
from app.forms import CaseTaskForm
|
||||
from app.iris_engine.module_handler.module_handler import call_modules_hook
|
||||
from app.iris_engine.utils.tracker import track_activity
|
||||
from app.models.authorization import CaseAccessLevel
|
||||
from app.models.authorization import User
|
||||
from app.models.models import CaseTasks
|
||||
from app.schema.marshables import CaseTaskSchema
|
||||
from app.schema.marshables import CommentSchema
|
||||
from app.util import ac_api_case_requires
|
||||
from app.util import ac_case_requires
|
||||
from app.util import response_error
|
||||
from app.util import response_success
|
||||
|
||||
case_tasks_blueprint = Blueprint('case_tasks',
|
||||
__name__,
|
||||
template_folder='templates')
|
||||
|
||||
|
||||
# CONTENT ------------------------------------------------
|
||||
@case_tasks_blueprint.route('/case/tasks', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_tasks(caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('case_tasks.case_tasks', cid=caseid, redirect=True))
|
||||
|
||||
form = FlaskForm()
|
||||
case = get_case(caseid)
|
||||
|
||||
return render_template("case_tasks.html", case=case, form=form)
|
||||
|
||||
|
||||
@case_tasks_blueprint.route('/case/tasks/list', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_get_tasks(caseid):
|
||||
ct = get_tasks_with_assignees(caseid)
|
||||
|
||||
if not ct:
|
||||
output = []
|
||||
else:
|
||||
output = ct
|
||||
|
||||
ret = {
|
||||
"tasks_status": get_tasks_status(),
|
||||
"tasks": output,
|
||||
"state": get_tasks_state(caseid=caseid)
|
||||
}
|
||||
|
||||
return response_success("", data=ret)
|
||||
|
||||
|
||||
@case_tasks_blueprint.route('/case/tasks/state', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_get_tasks_state(caseid):
|
||||
os = get_tasks_state(caseid=caseid)
|
||||
if os:
|
||||
return response_success(data=os)
|
||||
else:
|
||||
return response_error('No tasks state for this case.')
|
||||
|
||||
|
||||
@case_tasks_blueprint.route('/case/tasks/status/update/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_task_statusupdate(cur_id, caseid):
|
||||
task = get_task(task_id=cur_id, caseid=caseid)
|
||||
if not task:
|
||||
return response_error("Invalid task ID for this case")
|
||||
|
||||
if request.is_json:
|
||||
|
||||
if update_task_status(request.json.get('task_status_id'), cur_id, caseid):
|
||||
task_schema = CaseTaskSchema()
|
||||
|
||||
return response_success("Task status updated", data=task_schema.dump(task))
|
||||
else:
|
||||
return response_error("Invalid status")
|
||||
|
||||
else:
|
||||
return response_error("Invalid request")
|
||||
|
||||
|
||||
@case_tasks_blueprint.route('/case/tasks/add/modal', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_add_task_modal(caseid):
|
||||
|
||||
task = CaseTasks()
|
||||
task.custom_attributes = get_default_custom_attributes('task')
|
||||
form = CaseTaskForm()
|
||||
form.task_status_id.choices = [(a.id, a.status_name) for a in get_tasks_status()]
|
||||
form.task_assignees_id.choices = []
|
||||
|
||||
return render_template("modal_add_case_task.html", form=form, task=task, uid=current_user.id, user_name=None,
|
||||
attributes=task.custom_attributes)
|
||||
|
||||
|
||||
@case_tasks_blueprint.route('/case/tasks/add', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_add_task(caseid):
|
||||
try:
|
||||
# validate before saving
|
||||
task_schema = CaseTaskSchema()
|
||||
request_data = call_modules_hook('on_preload_task_create', data=request.get_json(), caseid=caseid)
|
||||
|
||||
if 'task_assignee_id' in request_data or 'task_assignees_id' not in request_data:
|
||||
return response_error('task_assignee_id is not valid anymore since v1.5.0')
|
||||
|
||||
task_assignee_list = request_data['task_assignees_id']
|
||||
del request_data['task_assignees_id']
|
||||
task = task_schema.load(request_data)
|
||||
|
||||
ctask = add_task(task=task,
|
||||
assignee_id_list=task_assignee_list,
|
||||
user_id=current_user.id,
|
||||
caseid=caseid
|
||||
)
|
||||
|
||||
ctask = call_modules_hook('on_postload_task_create', data=ctask, caseid=caseid)
|
||||
|
||||
if ctask:
|
||||
track_activity(f"added task \"{ctask.task_title}\"", caseid=caseid)
|
||||
return response_success("Task '{}' added".format(ctask.task_title), data=task_schema.dump(ctask))
|
||||
|
||||
return response_error("Unable to create task for internal reasons")
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages, status=400)
|
||||
|
||||
|
||||
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_task_view(cur_id, caseid):
|
||||
task = get_task_with_assignees(task_id=cur_id, case_id=caseid)
|
||||
if not task:
|
||||
return response_error("Invalid task ID for this case")
|
||||
|
||||
task_schema = CaseTaskSchema()
|
||||
|
||||
return response_success(data=task_schema.dump(task))
|
||||
|
||||
|
||||
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/modal', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_task_view_modal(cur_id, caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('case_tasks.case_tasks', cid=caseid, redirect=True))
|
||||
|
||||
form = CaseTaskForm()
|
||||
|
||||
task = get_task_with_assignees(task_id=cur_id, case_id=caseid)
|
||||
form.task_status_id.choices = [(a.id, a.status_name) for a in get_tasks_status()]
|
||||
form.task_assignees_id.choices = []
|
||||
|
||||
if not task:
|
||||
return response_error("Invalid task ID for this case")
|
||||
|
||||
form.task_title.render_kw = {'value': task.task_title}
|
||||
form.task_description.data = task.task_description
|
||||
user_name, = User.query.with_entities(User.name).filter(User.id == task.task_userid_update).first()
|
||||
comments_map = get_case_tasks_comments_count([task.id])
|
||||
|
||||
return render_template("modal_add_case_task.html", form=form, task=task, user_name=user_name,
|
||||
comments_map=comments_map, attributes=task.custom_attributes)
|
||||
|
||||
|
||||
@case_tasks_blueprint.route('/case/tasks/update/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_edit_task(cur_id, caseid):
|
||||
try:
|
||||
task = get_task_with_assignees(task_id=cur_id, case_id=caseid)
|
||||
if not task:
|
||||
return response_error("Invalid task ID for this case")
|
||||
|
||||
request_data = call_modules_hook('on_preload_task_update', data=request.get_json(), caseid=caseid)
|
||||
|
||||
if 'task_assignee_id' in request_data or 'task_assignees_id' not in request_data:
|
||||
return response_error('task_assignee_id is not valid anymore since v1.5.0')
|
||||
|
||||
# validate before saving
|
||||
task_assignee_list = request_data['task_assignees_id']
|
||||
del request_data['task_assignees_id']
|
||||
task_schema = CaseTaskSchema()
|
||||
|
||||
request_data['id'] = cur_id
|
||||
task = task_schema.load(request_data, instance=task)
|
||||
|
||||
task.task_userid_update = current_user.id
|
||||
task.task_last_update = datetime.utcnow()
|
||||
|
||||
update_task_assignees(task, task_assignee_list, caseid)
|
||||
|
||||
update_tasks_state(caseid=caseid)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
task = call_modules_hook('on_postload_task_update', data=task, caseid=caseid)
|
||||
|
||||
if task:
|
||||
track_activity(f"updated task \"{task.task_title}\" (status {task.task_status_id})",
|
||||
caseid=caseid)
|
||||
return response_success("Task '{}' updated".format(task.task_title), data=task_schema.dump(task))
|
||||
|
||||
return response_error("Unable to update task for internal reasons")
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages, status=400)
|
||||
|
||||
|
||||
@case_tasks_blueprint.route('/case/tasks/delete/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_delete_task(cur_id, caseid):
|
||||
call_modules_hook('on_preload_task_delete', data=cur_id, caseid=caseid)
|
||||
task = get_task_with_assignees(task_id=cur_id, case_id=caseid)
|
||||
if not task:
|
||||
return response_error("Invalid task ID for this case")
|
||||
|
||||
delete_task(task.id)
|
||||
|
||||
update_tasks_state(caseid=caseid)
|
||||
|
||||
call_modules_hook('on_postload_task_delete', data=cur_id, caseid=caseid)
|
||||
|
||||
track_activity(f"deleted task \"{task.task_title}\"")
|
||||
|
||||
return response_success("Task deleted")
|
||||
|
||||
|
||||
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/modal', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_comment_task_modal(cur_id, caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('case_task.case_task', cid=caseid, redirect=True))
|
||||
|
||||
task = get_task(cur_id, caseid=caseid)
|
||||
if not task:
|
||||
return response_error('Invalid task ID')
|
||||
|
||||
return render_template("modal_conversation.html", element_id=cur_id, element_type='tasks',
|
||||
title=task.task_title)
|
||||
|
||||
|
||||
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/list', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_comment_task_list(cur_id, caseid):
|
||||
|
||||
task_comments = get_case_task_comments(cur_id)
|
||||
if task_comments is None:
|
||||
return response_error('Invalid task ID')
|
||||
|
||||
return response_success(data=CommentSchema(many=True).dump(task_comments))
|
||||
|
||||
|
||||
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/add', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_comment_task_add(cur_id, caseid):
|
||||
|
||||
try:
|
||||
task = get_task(cur_id, caseid=caseid)
|
||||
if not task:
|
||||
return response_error('Invalid task ID')
|
||||
|
||||
comment_schema = CommentSchema()
|
||||
|
||||
comment = comment_schema.load(request.get_json())
|
||||
comment.comment_case_id = caseid
|
||||
comment.comment_user_id = current_user.id
|
||||
comment.comment_date = datetime.now()
|
||||
comment.comment_update_date = datetime.now()
|
||||
db.session.add(comment)
|
||||
db.session.commit()
|
||||
|
||||
add_comment_to_task(task.id, comment.comment_id)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
hook_data = {
|
||||
"comment": comment_schema.dump(comment),
|
||||
"task": CaseTaskSchema().dump(task)
|
||||
}
|
||||
call_modules_hook('on_postload_task_commented', data=hook_data, caseid=caseid)
|
||||
|
||||
track_activity(f"task \"{task.task_title}\" commented", caseid=caseid)
|
||||
return response_success("Task commented", data=comment_schema.dump(comment))
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.normalized_messages(), status=400)
|
||||
|
||||
|
||||
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def case_comment_task_get(cur_id, com_id, caseid):
|
||||
|
||||
comment = get_case_task_comment(cur_id, com_id)
|
||||
if not comment:
|
||||
return response_error("Invalid comment ID")
|
||||
|
||||
return response_success(data=comment._asdict())
|
||||
|
||||
|
||||
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_comment_task_edit(cur_id, com_id, caseid):
|
||||
|
||||
return case_comment_update(com_id, 'tasks', caseid)
|
||||
|
||||
|
||||
@case_tasks_blueprint.route('/case/tasks/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def case_comment_task_delete(cur_id, com_id, caseid):
|
||||
|
||||
success, msg = delete_task_comment(cur_id, com_id)
|
||||
if not success:
|
||||
return response_error(msg)
|
||||
|
||||
call_modules_hook('on_postload_task_comment_delete', data=com_id, caseid=caseid)
|
||||
|
||||
track_activity(f"comment {com_id} on task {cur_id} deleted", caseid=caseid)
|
||||
return response_success(msg)
|
||||
|
1155
iris-web/source/app/blueprints/case/case_timeline_routes.py
Normal file
1155
iris-web/source/app/blueprints/case/case_timeline_routes.py
Normal file
File diff suppressed because it is too large
Load Diff
49
iris-web/source/app/blueprints/case/templates/case-nav.html
Normal file
49
iris-web/source/app/blueprints/case/templates/case-nav.html
Normal file
@ -0,0 +1,49 @@
|
||||
<nav class="nav-bottom rounded" id="h_nav_tab">
|
||||
<div class="container-fluid" >
|
||||
<ul class="navbar-nav rounded" style="background-color:#4c72a130;">
|
||||
<li class="nav-item {{ active if page == 'case' }}">
|
||||
<a class="nav-link" href="/case?cid={{session['current_case'].case_id}}">
|
||||
<span class="menu-title">Summary</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/case/notes?cid={{session['current_case'].case_id}}">
|
||||
<span class="menu-title">Notes</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/case/assets?cid={{session['current_case'].case_id}}">
|
||||
<span class="menu-title">Assets</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/case/ioc?cid={{session['current_case'].case_id}}">
|
||||
<span class="menu-title">IOC</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/case/timeline?cid={{session['current_case'].case_id}}">
|
||||
<span class="menu-title">Timeline</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/case/graph?cid={{session['current_case'].case_id}}">
|
||||
<span class="menu-title">Graph</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/case/tasks?cid={{session['current_case'].case_id}}">
|
||||
<span class="menu-title">Tasks</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/case/evidences?cid={{session['current_case'].case_id}}">
|
||||
<span class="menu-title">Evidences</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
|
@ -0,0 +1,51 @@
|
||||
<div class="secondary-header">
|
||||
<div class="nav-bottom ">
|
||||
<div class="container-fluid float-left">
|
||||
<div class="row">
|
||||
<div class="col col-md-ml-12 col-sm-12">
|
||||
<ul class="nav page-navigation page-navigation-style-2 page-navigation-primary">
|
||||
<li class="nav-item {{ active if page == 'case' }}">
|
||||
<button class="btn btn-sm btn-light" href="#" onclick="case_detail('{{ case.case_id }}');">
|
||||
<i class="fa-solid fa-gear mr-1"></i>
|
||||
<span class="">Manage</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-light float-left ml-2" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
<span aria-hidden="true"><i class="fa-solid fa-bolt mr-1"></i> Processors</span>
|
||||
</button>
|
||||
<div class="dropdown-menu pull-right" id="case_modal_quick_actions" aria-labelledby="dropdownMenuButton">
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="btn btn-sm btn-light float-left ml-2" id="case_pipeline" onclick="case_pipeline_popup();">
|
||||
<span aria-hidden="true"><i class="fa-solid fa-upload mr-1"></i>Pipelines</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item ml-auto">
|
||||
{% if case.review_status.status_name == "Not reviewed" or not case.review_status %}
|
||||
<button class="btn btn-sm btn-light float-left ml-2" id="request_review">
|
||||
<span aria-hidden="true"><i class="fa-solid fa-clipboard-check mr-1"></i>Request review</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<btn href="#" onclick="report_template_selector();" class="btn btn-dark btn-sm float-right ml-2">
|
||||
<span class="btn-label">
|
||||
<i class="fa fa-file-arrow-down"></i>
|
||||
</span>
|
||||
Generate report
|
||||
</btn>
|
||||
<btn href="#" onclick="act_report_template_selector();" class="btn btn-sm btn-dark float-right ml-2">
|
||||
<span class="btn-label">
|
||||
<i class="fa fa-chart-line"></i>
|
||||
</span>
|
||||
Activity report
|
||||
</btn>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
337
iris-web/source/app/blueprints/case/templates/case.html
Normal file
337
iris-web/source/app/blueprints/case/templates/case.html
Normal file
@ -0,0 +1,337 @@
|
||||
{% extends "layouts/default_ext.html" %} {% block title %} Case summary {% endblock title %}
|
||||
{% block stylesheets %}
|
||||
{% include 'includes/header_case.html' %}
|
||||
<link rel="stylesheet" href="/static/assets/css/select2.css">
|
||||
<link rel="stylesheet" href="/static/assets/css/bootstrap-multiselect.min.css">
|
||||
<link rel="stylesheet" href="/static/assets/css/bootstrap-select.min.css">
|
||||
{% endblock stylesheets %}
|
||||
{% block content %}
|
||||
{% if current_user.is_authenticated %}
|
||||
|
||||
{% include 'includes/navigation_ext.html' %}
|
||||
{% include 'includes/sidenav.html' %}
|
||||
|
||||
<div class="main-panel">
|
||||
|
||||
<div class="content">
|
||||
{% if case.close_date %}
|
||||
<div class="panel-header bg-close-gradient">
|
||||
{% else %}
|
||||
<div class="panel-header bg-info-gradient">
|
||||
{% endif %}
|
||||
<div class="page-inner py-5">
|
||||
<div class="d-flex align-items-left align-items-md-center flex-column flex-md-row mt--3">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<h2 class="text-white pb-2 fw-bold case-name"> <i class="icon-big flaticon-network mr-2"></i> {{ case.name|unquote }}
|
||||
</h2>
|
||||
</div>
|
||||
<h5 class="text-white op-7 mb-1"><b>Open on</b> {{ case.open_date }} by {{ case.user.name }}</h5>
|
||||
<h5 class="text-white op-7 mb-3"><b>Owned by</b> {{ case.owner.name }}</h5>
|
||||
{% if case.close_date %}
|
||||
<h5 class="text-warning mb-1">Closed on {{ case.close_date }}</h5>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<div class="col mt-4">
|
||||
<div class="row">
|
||||
<span title="Case outcome" class="float-right btn btn-rounded badge-pill hidden-caret ml-auto btn-xs mr-2 mb-2 {% if case.status_id == 1%}badge-success{% elif case.status_id == 2 %}badge-danger{% else %}btn-light{% endif %}"
|
||||
onclick="case_detail('{{ case.case_id }}', true);"
|
||||
><i class="fa-solid fa-group-arrows-rotate mr-2"></i>{{ case.status_name }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="ml-auto">
|
||||
<div class="row">
|
||||
<h5 class="text-white op-7 mb-2 float-right mr-4"><b>Customer</b> : {{ case.client.name }}</h5>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% if case.soc_id %} <h5 class="text-white op-7 mb-2 mr-4"><b>SOC ID :</b> {{ case.soc_id }}</h5> {% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2 mb--2">
|
||||
|
||||
<div class="ml-2 col">
|
||||
<div class="row ml-1">
|
||||
{% if case.state %}<h5 title="Case state" onclick="case_detail('{{ case.case_id }}', true);" style="cursor:pointer;"><span class="btn-rounded badge-pill hidden-caret btn-sm btn-light"><i class="fa-solid fa-business-time mr-1"></i> {{ case.state.state_name }}</span></h5>{% endif %}
|
||||
{% if case.classification %}<h5 title="Classification" onclick="case_detail('{{ case.case_id }}', true);" style="cursor:pointer;"><span class="btn-rounded badge-pill hidden-caret btn-sm btn-light ml-2"><i class="fa-solid fa-shield-virus mr-1"></i>{{ case.classification.name_expanded }}</span></h5>{% endif %}
|
||||
{% if case.alerts| length > 0 %}<h5 title="Alerts"><a class="btn-rounded badge-pill hidden-caret btn-sm btn-dark ml-2 badge-warning" href="/alerts?cid={{ case.case_id }}&sort=desc&case_id={{ case.case_id }}" target="_blank" rel="noopener"><i class="fa-solid fa-bell mr-1"></i> {{ case.alerts| length }} related alerts</a></h5>{% endif %}
|
||||
{% if case.review_status.status_name == "Reviewed" %}
|
||||
<h5 title="Reviewed"> <a class="text-white btn-rounded badge-pill hidden-caret btn-sm ml-2 badge-success"><i class="fa-regular fa-circle-check mr-2"></i>Case reviewed by {% if case.reviewer.id == current_user.id %} you {% else %} {{ case.reviewer.name }} {% endif %}</a></h5>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mr-2">
|
||||
{% if case.case_tags %}
|
||||
{% for tag in case.case_tags %}
|
||||
<span class="badge badge-pill badge-light ml-1 pull-right"><i class="fa fa-tag mr-1"></i> {{ tag }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-inner mt--5">
|
||||
<div class="row row-card-no-pd" style="padding-top: 0px;padding-bottom: 3px;">
|
||||
{% include 'case-nav_landing.html' %}
|
||||
</div>
|
||||
<div id="caseReviewState" data-review-state="{{ case.review_status.status_name }}" data-reviewer-id="{{ case.reviewer_id }}" data-reviewer-name="{{ case.reviewer.name }}" style="display: none;"></div>
|
||||
|
||||
{% if case.reviewer_id == current_user.id and case.review_status.status_name != "Reviewed" and case.review_status.status_name != "Not reviewed" %}
|
||||
<div class="row row-card-no-pd review-card mt--3 mb--3 bg-warning-gradient" style="display: none;">
|
||||
<div class="col-md-12">
|
||||
<h4 class="font-weight-bold"><i class="fa-solid fa-triangle-exclamation text-danger ml-2 mr-2"></i>Review requested
|
||||
<button class="btn btn-sm float-right btn-dark mr-3 mt-2 btn-start-review">Start review</button>
|
||||
<button class="btn btn-sm float-right btn-success mr-3 mt-2 btn-confirm-review" style="display:none">Confirm review</button>
|
||||
<button class="btn btn-sm float-right btn-light mr-3 mt-2 btn-cancel-review" style="display:none">Cancel review</button></h4>
|
||||
|
||||
<span class="ml-2" id="reviewSubtitle">You have been requested to review this case.</span>
|
||||
</div>
|
||||
</div>
|
||||
{% elif case.review_status.status_name == "Review in progress" %}
|
||||
<div class="row row-card-no-pd mt--3 mb--3 bg-warning-gradient">
|
||||
<div class="col-md-12">
|
||||
<h4 class="font-weight-bold mt-1"><i class="fa-solid fa-list-check ml-2 mr-2"></i>Review by {{ case.reviewer.name }} in progress</h4>
|
||||
</div>
|
||||
</div>
|
||||
{% elif case.review_status.status_name == "Pending review" %}
|
||||
<div class="row row-card-no-pd mt--3 mb--3 bg-warning-gradient">
|
||||
<div class="col-md-12">
|
||||
<h4 class="font-weight-bold mt-1"><i class="fa-solid fa-triangle-exclamation text-danger ml-2 mr-2"></i>Review by {{ case.reviewer.name }} pending</h4>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row row-card-no-pd">
|
||||
<div class="col-md-12">
|
||||
<div class="card mb-4" id="rescard1">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
{{ form.hidden_tag() }}
|
||||
<a href="#case_summary_card" class="d-block nav-link mr-auto" data-toggle="collapse" aria-expanded="true" aria-controls="case_summary_card">
|
||||
<h4 class="m-0 font-weight-bold">Case summary {{ "(Syncing with DB )" if case.id }}</h4>
|
||||
</a>
|
||||
<div class="mr-0 float-right">
|
||||
<small id="content_typing" class="mr-3 mt-1"></small>
|
||||
<small id="content_last_saved_by" class="mr-3 mt-1"></small>
|
||||
<span id="last_saved" class="badge mr-3 ml-2"></span>
|
||||
<small id="content_last_sync"></small>
|
||||
<button class="btn btn-sm mr-2 ml-3" onclick="edit_case_summary();" id="sum_edit_btn" >Edit</button>
|
||||
<button type="button" id="sum_refresh_btn" class="btn btn-sm btn-outline-default mr-3" onclick="sync_editor();">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapsed" id="case_summary_card">
|
||||
<div class="card-body">
|
||||
<div class="row mb-1">
|
||||
<div class="col" id="summary_edition_btn" style="display:none;">
|
||||
<div class="btn btn-sm btn-light mr-1 " title="CTRL-B" onclick="editor.insertSnippet('**${1:$SELECTION}**');editor.focus();"><i class="fa-solid fa-bold"></i></div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="CTRL-I" onclick="editor.insertSnippet('*${1:$SELECTION}*');editor.focus();"><i class="fa-solid fa-italic"></i></div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="CTRL-SHIFT-1" onclick="editor.insertSnippet('# ${1:$SELECTION}');editor.focus();">H1</div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="CTRL-SHIFT-2" onclick="editor.insertSnippet('## ${1:$SELECTION}');editor.focus();">H2</div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="CTRL-SHIFT-3" onclick="editor.insertSnippet('### ${1:$SELECTION}');editor.focus();">H3</div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="CTRL-SHIFT-4" onclick="editor.insertSnippet('#### ${1:$SELECTION}');editor.focus();">H4</div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="Insert code" onclick="editor.insertSnippet('```${1:$SELECTION}```');editor.focus();"><i class="fa-solid fa-code"></i></div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="Insert link" onclick="editor.insertSnippet('[${1:$SELECTION}](url)');editor.focus();"><i class="fa-solid fa-link"></i></div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="Insert table" onclick="editor.insertSnippet('|\t|\t|\t|\n|--|--|--|\n|\t|\t|\t|\n|\t|\t|\t|');editor.focus();"><i class="fa-solid fa-table"></i></div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="Insert bullet list" onclick="editor.insertSnippet('\n- \n- \n- ');editor.focus();"><i class="fa-solid fa-list"></i></div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="Insert numbered list" onclick="editor.insertSnippet('\n1. a \n2. b \n3. c ');editor.focus();"><i class="fa-solid fa-list-ol"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6" id="container_editor_summary">
|
||||
<div style="display: none" id="fetched_crc"></div>
|
||||
<div id="editor_summary" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}"></div>
|
||||
<textarea id="case_summary" rows="10" cols="82" style="display: none"></textarea>
|
||||
</div>
|
||||
<div class="col-md-6" id="ctrd_casesum">
|
||||
<div id="targetDiv"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal " tabindex="-1" role="dialog" id="modal_select_report" data-backdrop="true">
|
||||
<div class="modal-lg modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5>Select report template</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% if reports| length == 0 %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h4 class="alert-heading">No report template found</h4>
|
||||
<p>Report templates are configured in <a href="/manage/templates?cid={{case.case_id}}">the management section</a>.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col">
|
||||
<p>Since IRIS v2.0.0, the report generation supports images. Integration of images might fail depending on the situation.<br/><code>Safe Mode</code> can be used to generate the report without them.</p>
|
||||
</div>
|
||||
|
||||
<select class="selectpicker form-control bg-outline-success dropdown-submenu" data-show-subtext="true" data-live-search="true" id="select_report">
|
||||
{% for report in reports %}
|
||||
<option data-toggle="tooltip" value="{{ report[0] }}" data-subtext="{{ report[3] }}">{{ report[1] }} ({{ report[2].capitalize() }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn btn-light float-left mt-2 mr-auto" onclick="gen_report(true);">
|
||||
<span class="btn-label">
|
||||
<i class="fa fa-file-download"></i>
|
||||
</span>
|
||||
Generate in Safe Mode
|
||||
</a>
|
||||
<a href="#" class="btn btn-light float-right mt-2 ml-2" onclick="gen_report(false);">
|
||||
<span class="btn-label">
|
||||
<i class="fa fa-file-download"></i>
|
||||
</span>
|
||||
Generate
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div>
|
||||
<div class="modal " tabindex="-1" role="dialog" id="modal_select_report_act" data-backdrop="true">
|
||||
<div class="modal-lg modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5>Select activity report template</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% if reports| length == 0 %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h4 class="alert-heading">No report template found</h4>
|
||||
<p>Report templates are configured in <a href="/manage/templates?cid={{case.case_id}}">the management section</a>.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col">
|
||||
<p>Since IRIS v2.0.0, the report generation supports images. Integration of images might fail depending on the situation.<br/><code>Safe Mode</code> can be used to generate the report without them.</p>
|
||||
</div>
|
||||
<select class="selectpicker form-control bg-outline-success dropdown-submenu mb-2" data-show-subtext="true" data-live-search="true" id="select_report_act">
|
||||
{% for report in reports_act %}
|
||||
<option data-toggle="tooltip" value="{{ report[0] }}" data-subtext="{{ report[3] }}">{{ report[1] }} ({{ report[2].capitalize() }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn btn-light float-left mt-2 mr-auto" onclick="gen_act_report(true);">
|
||||
<span class="btn-label">
|
||||
<i class="fa fa-file-download"></i>
|
||||
</span>
|
||||
Generate in Safe Mode
|
||||
</a>
|
||||
<a href="#" class="btn btn-light float-right mt-2 ml-2" onclick="gen_act_report(false);">
|
||||
<span class="btn-label">
|
||||
<i class="fa fa-file-download"></i>
|
||||
</span>
|
||||
Generate
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div>
|
||||
<div class="modal " tabindex="-1" role="dialog" id="modal_choose_reviewer" data-backdrop="true">
|
||||
<div class="modal-lg modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="" id="form_choose_reviewer">
|
||||
<div class="modal-header">
|
||||
<h5>Choose reviewer</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row mb-2">
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
<select class="selectpicker form-control" data-dropup-auto="false" data-live-search="true" id="reviewer_id">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-4">
|
||||
<div class="col-12 d-flex">
|
||||
<button type="button" class="btn btn-default mr-auto" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-outline-success ml-auto" id="submit_set_reviewer">Request</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div>
|
||||
<div class="modal" tabindex="-1" role="dialog" id="modal_case_detail" data-backdrop="true">
|
||||
<div class="modal-xl modal-dialog" role="document">
|
||||
<div class="modal-content" id="info_case_modal_content">
|
||||
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div>
|
||||
<div class="modal bg-shadow-gradient" tabindex="-1" role="dialog" id="modal_ac_additional" data-backdrop="true">
|
||||
</div>
|
||||
<div class="modal bg-shadow-gradient" tabindex="-1" role="dialog" id="modal_case_review" data-backdrop="static" data-keyboard="false">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Case Review</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p>Do you confirm that the case has been reviewed?</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<button type="button" class="btn btn-danger float-left" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success float-right" id="confirmReview">Confirm Review</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'includes/footer.html' %}
|
||||
{% endif %} {% endblock content %} {% block javascripts %}
|
||||
|
||||
<script src="/static/assets/js/plugin/ace/src-noconflict/ace.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="/static/assets/js/plugin/ace/src-noconflict/ext-language_tools.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="/static/assets/js/core/socket.io.js"></script>
|
||||
<script src="/static/assets/js/plugin/select/select2.js"></script>
|
||||
|
||||
<script src="/static/assets/js/plugin/showdown/showdown.min.js"></script>
|
||||
<script src="/static/assets/js/iris/crc32.js"></script>
|
||||
<script src="/static/assets/js/iris/datatablesUtils.js"></script>
|
||||
<script src="/static/assets/js/iris/case.js"></script>
|
||||
<script src="/static/assets/js/iris/manage.cases.common.js"></script>
|
||||
<script src="/static/assets/js/iris/case.summary.js"></script>
|
||||
<script src="/static/assets/js/plugin/select/bootstrap-select.min.js"></script>
|
||||
<script src="/static/assets/js/plugin/select/bootstrap-multiselect.min.js"></script>
|
||||
|
||||
<script>
|
||||
$('#modal_select_report').selectpicker();
|
||||
load_menu_mod_options_modal([{{case.case_id}}], 'case', $("#case_modal_quick_actions"));
|
||||
</script>
|
||||
|
||||
{% endblock javascripts %}
|
148
iris-web/source/app/blueprints/case/templates/case_assets.html
Normal file
148
iris-web/source/app/blueprints/case/templates/case_assets.html
Normal file
@ -0,0 +1,148 @@
|
||||
{% extends "layouts/default_ext.html" %} {% block title %} Case Assets {% endblock title %} {% block stylesheets %}
|
||||
{% include 'includes/header_case.html' %}
|
||||
<link rel="stylesheet" href="/static/assets/css/bootstrap-select.min.css">
|
||||
{% endblock stylesheets %}
|
||||
{% block content %}
|
||||
{% include 'includes/navigation_ext.html' %}
|
||||
|
||||
{% include 'includes/sidenav.html' %}
|
||||
<div class="main-panel">
|
||||
|
||||
<div class="content">
|
||||
{% if current_user.is_authenticated %}
|
||||
{{ form.hidden_tag() }}
|
||||
<nav class="navbar navbar-header navbar-expand-lg pt-2 pb-2 bg-primary-gradient">
|
||||
<div class="container-fluid">
|
||||
<div class="collapse" id="search-nav">
|
||||
<div id="tables_button"></div>
|
||||
</div>
|
||||
<ul class="navbar-nav topbar-nav ml-md-auto align-items-center page-navigation page-navigation-style-2 page-navigation-secondary">
|
||||
<li class="nav-item ml-2">
|
||||
<span class="text-white text-sm mr-2" id="last_resfresh">Loading</span>
|
||||
<span class="text-warning text-sm mr-2" id="page_warning"></span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="btn btn-primary btn-sm" onclick="reload_assets();">
|
||||
<span class="menu-title">Refresh</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="btn btn-dark btn-sm" onclick="add_assets();">
|
||||
<span class="menu-title">Add assets</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-border btn-black" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="menu-title"><i class="fas fa-ellipsis-v"></i></span>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item" href="#" onclick="fire_upload_assets();">Upload CSV of assets</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</nav>
|
||||
|
||||
<div class="page-inner ">
|
||||
<div class="row ">
|
||||
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
|
||||
<div class="col-md-12">
|
||||
<div class="card" id="card_main_load" style="display:none;">
|
||||
<div class="card-body">
|
||||
<div class="col" id="assets_table_wrapper">
|
||||
<table class="table display table-striped table-hover responsive" width="100%" cellspacing="0" id="assets_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>IP</th>
|
||||
<th>Compromised</th>
|
||||
<th>IOC</th>
|
||||
<th>Tags</th>
|
||||
<th>Analysis</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>IP</th>
|
||||
<th>Compromised</th>
|
||||
<th>IOC</th>
|
||||
<th>Tags</th>
|
||||
<th>Analysis</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal modal-case-focus" tabindex="-1" role="dialog" id="modal_add_asset" data-backdrop="true">
|
||||
<div class="modal-xl modal-dialog" role="document">
|
||||
<div class="modal-content" id="modal_add_asset_content">
|
||||
|
||||
</div>
|
||||
<!-- /.modal-content -->
|
||||
</div>
|
||||
<!-- /.modal-dialog -->
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="modal " tabindex="-1" role="dialog" id="modal_upload_assets" data-backdrop="true">
|
||||
<div class="modal-lg modal-dialog" role="document">
|
||||
<form method="post" action="" id="form_upload_assets">
|
||||
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5>Upload assets list (CSV format)</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="ioc_format" class="placeholder">Expected CSV File format</label>
|
||||
<textarea class="form-control col-md-12 col-sm-12 sizable-textarea" rows="2" disabled>asset_name,asset_type_name,asset_description,asset_ip,asset_domain,asset_tags (separated with "|")</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="placeholder">CSV File format example</label>
|
||||
<textarea class="form-control col-md-12 col-sm-12 sizable-textarea" rows="3" disabled>asset_name,asset_type_name,asset_description,asset_ip,asset_domain,asset_tags
|
||||
"My computer","Mac - Computer","Computer of Mme Michu","192.168.15.5","iris.local","Compta|Mac"
|
||||
"XCAS","Windows - Server","Xcas server","192.168.15.48","iris.local",""</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="placeholder">Choose CSV file to import : </label>
|
||||
<input id="input_upload_assets" type="file" accept="text/csv">
|
||||
</div>
|
||||
</div>
|
||||
<div class='invalid-feedback' id='ioc-invalid-msg'></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-dark mr-auto" onclick="generate_sample_csv();">Download sample CSV</button>
|
||||
<button type="button" class="btn btn-outline-success" onclick="upload_assets();">Upload</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</form>
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div>
|
||||
{% include 'includes/footer.html' %}
|
||||
|
||||
</div>
|
||||
{% endblock content %}
|
||||
{% block javascripts %}
|
||||
{% include 'includes/footer_case.html' %}
|
||||
<script src="/static/assets/js/plugin/sortable/sortable.js "></script>
|
||||
<script src="/static/assets/js/iris/case.asset.js"></script>
|
||||
|
||||
|
||||
{% endblock javascripts %}
|
@ -0,0 +1,71 @@
|
||||
{% extends "layouts/default_ext.html" %}
|
||||
{% block title %} Case Graph {% endblock title %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<link rel="stylesheet" href="/static/assets/css/bootstrap-slider.min.css">
|
||||
<link rel="stylesheet" href="/static/assets/css/select2.css">
|
||||
<link rel="stylesheet" href="/static/assets/css/bootstrap-select.min.css">
|
||||
|
||||
<link href="/static/assets/css/vis.min.css" rel="stylesheet" type="text/css" />
|
||||
{% endblock stylesheets %}
|
||||
{% block content %}
|
||||
|
||||
{% include 'includes/navigation_ext.html' %}
|
||||
|
||||
{% include 'includes/sidenav.html' %}
|
||||
<div class="main-panel">
|
||||
|
||||
<div class="content">
|
||||
<nav class="navbar navbar-header navbar-expand-lg bg-primary-gradient">
|
||||
<ul class="container-fluid mt-3 mb--2">
|
||||
|
||||
<ul class="navbar-nav topbar-nav ml-md-auto align-items-center page-navigation page-navigation-style-2 page-navigation-secondary">
|
||||
<li class="nav-item ml-2">
|
||||
<span class="text-white text-sm mr-2" id="last_resfresh">Loading</span>
|
||||
</li>
|
||||
<li class="nav-item hidden-caret">
|
||||
<button class="btn btn-primary btn-sm" onclick="get_case_graph();">
|
||||
<span class="menu-title">Refresh</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</nav>
|
||||
{% if current_user.is_authenticated %}
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="page-inner">
|
||||
<div class="row">
|
||||
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
|
||||
<div class="card mb-4 col-md-12" id="card_main_load" style="display:none;">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div id='graph-container'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% include 'includes/footer.html' %}
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascripts %}
|
||||
|
||||
<script src="/static/assets/js/plugin/vis/vis.min.js"></script>
|
||||
<script src="/static/assets/js/plugin/vis/vis-network.min.js"></script>
|
||||
<script src="/static/assets/js/plugin/bootstrap-slider/bootstrap-slider.min.js"></script>
|
||||
|
||||
<script src="/static/assets/js/plugin/select/select2.js"></script>
|
||||
<script src="/static/assets/js/plugin/select/bootstrap-select.min.js"></script>
|
||||
|
||||
<script src="/static/assets/js/iris/case.js"></script>
|
||||
<script src="/static/assets/js/iris/case.graph.js"></script>
|
||||
{% endblock javascripts %}
|
||||
|
||||
|
||||
|
@ -0,0 +1,86 @@
|
||||
{% extends "layouts/default_ext.html" %}
|
||||
|
||||
{% block title %} Case Graph Timeline {% endblock title %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<link rel="stylesheet" href="/static/assets/css/bootstrap-datetime.css">
|
||||
<link rel="stylesheet" href="/static/assets/css/suggestags.css">
|
||||
<link rel="stylesheet" href="/static/assets/css/bootstrap-select.min.css">
|
||||
<link rel="stylesheet" href="/static/assets/css/select2.css">
|
||||
<link rel="stylesheet" href="/static/assets/css/vis.graph.css">
|
||||
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include 'includes/navigation_ext.html' %}
|
||||
|
||||
|
||||
{% include 'includes/sidenav.html' %}
|
||||
<div class="main-panel">
|
||||
<div class="content">
|
||||
<nav class="navbar navbar-header navbar-expand-lg bg-primary-gradient">
|
||||
<ul class="container-fluid mt-3 mb--2">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item hidden-caret">
|
||||
<a class="menu-title btn btn-dark btn-sm" href="visualize?cid={{session['current_case'].case_id}}">No group</a>
|
||||
</li>
|
||||
<li class="nav-item hidden-caret">
|
||||
<a class="menu-title btn btn-dark btn-sm" href="visualize?cid={{session['current_case'].case_id}}&group-by=asset"><span class="text-decoration-none">Group by asset</span></a>
|
||||
</li>
|
||||
<li class="nav-item hidden-caret">
|
||||
<a class="menu-title btn btn-dark btn-sm" href="visualize?cid={{session['current_case'].case_id}}&group-by=category">Group by category</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav topbar-nav ml-md-auto align-items-center page-navigation page-navigation-style-2 page-navigation-secondary">
|
||||
<li class="nav-item ml-2">
|
||||
<span class="text-white text-sm mr-2" id="last_resfresh">Loading</span>
|
||||
</li>
|
||||
<li class="nav-item hidden-caret">
|
||||
<button class="btn btn-primary btn-sm" onclick="refresh_timeline_graph();">
|
||||
<span class="menu-title">Refresh</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
|
||||
<div class="page-inner">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
|
||||
<div class="card" id="card_main_load" style="display:none;">
|
||||
<div class="card-body">
|
||||
<div id='visualization'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'includes/footer.html' %}
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascripts %}
|
||||
|
||||
<script src="/static/assets/js/core/moments.min.js"></script>
|
||||
<script src="/static/assets/js/core/bootstrap-datetimepicker.min.js"></script>
|
||||
|
||||
<script src="/static/assets/js/plugin/tagsinput/suggesttag.js"></script>
|
||||
<script src="/static/assets/js/plugin/select/select2.js"></script>
|
||||
<script src="/static/assets/js/plugin/select/bootstrap-select.min.js"></script>
|
||||
|
||||
<script src="/static/assets/js/iris/case.js"></script>
|
||||
<script src="/static/assets/js/iris/case.timeline.visu.js"></script>
|
||||
<script src="/static/assets/js/plugin/vis/vis.graph.js"></script>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
show_loader();
|
||||
refresh_timeline_graph();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock javascripts %}
|
149
iris-web/source/app/blueprints/case/templates/case_ioc.html
Normal file
149
iris-web/source/app/blueprints/case/templates/case_ioc.html
Normal file
@ -0,0 +1,149 @@
|
||||
{% extends "layouts/default_ext.html" %}
|
||||
{% block title %} Case IOC {% endblock title %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{% include 'includes/header_case.html' %}
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include 'includes/navigation_ext.html' %}
|
||||
|
||||
{% include 'includes/sidenav.html' %}
|
||||
<div class="main-panel">
|
||||
|
||||
<div class="content">
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{{ form.hidden_tag() }}
|
||||
<nav class="navbar navbar-header navbar-expand-lg pt-2 pb-2 bg-primary-gradient">
|
||||
<div class="container-fluid">
|
||||
<div class="collapse" id="search-nav">
|
||||
<div id="tables_button"></div>
|
||||
</div>
|
||||
<ul class="navbar-nav topbar-nav ml-md-auto align-items-center page-navigation page-navigation-style-2 page-navigation-secondary">
|
||||
<li class="nav-item ml-2">
|
||||
<span class="text-white text-sm mr-2" id="last_resfresh">Loading</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="btn btn-primary btn-sm" onclick="reload_iocs();">
|
||||
<span class="menu-title">Refresh</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="btn btn-dark btn-sm" onclick="add_ioc();">
|
||||
<span class="menu-title">Add IOC</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-border btn-black" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="menu-title"><i class="fas fa-ellipsis-v"></i></span>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item" href="#" onclick="fire_upload_iocs();">Upload CSV of IOCs</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</nav>
|
||||
|
||||
<div class="page-inner">
|
||||
<div class="row">
|
||||
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
|
||||
<div class="col-md-12">
|
||||
<div class="card" id="card_main_load" style="display:none;">
|
||||
<div class="card-body">
|
||||
<table class="table display wrap col-border table-striped table-hover" width="100%" cellspacing="0" id="ioc_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Value</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Tags</th>
|
||||
<th>Linked cases</th>
|
||||
<th>TLP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th>Value</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Tags</th>
|
||||
<th>Linked cases</th>
|
||||
<th>TLP</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% include 'includes/footer.html' %}
|
||||
</div>
|
||||
<div class="modal" tabindex="-1" role="dialog" id="modal_add_ioc" data-backdrop="true">
|
||||
<div class="modal-xl modal-dialog" role="document">
|
||||
<div class="modal-content" id="modal_add_ioc_content">
|
||||
|
||||
</div>
|
||||
<!-- /.modal-content -->
|
||||
</div>
|
||||
<!-- /.modal-dialog -->
|
||||
</div>
|
||||
|
||||
<div class="modal " tabindex="-1" role="dialog" id="modal_upload_ioc" data-backdrop="true">
|
||||
<div class="modal-lg modal-dialog" role="document">
|
||||
<form method="post" action="" id="form_upload_ioc">
|
||||
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5>Upload IOC list (CSV format)</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="ioc_format" class="placeholder">Expected CSV File format</label>
|
||||
<textarea class="form-control col-md-12 col-sm-12 sizable-textarea" rows="2" disabled>ioc_value,ioc_type,ioc_description,ioc_tags,ioc_tlp
|
||||
<Value>,<Type>,<Description>,<TLP>,Tags separated with "|"</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="placeholder">CSV File format example</label>
|
||||
<textarea class="form-control col-md-12 col-sm-12 sizable-textarea" rows="3" disabled>ioc_value,ioc_type,ioc_description,ioc_tags,ioc_tlp
|
||||
1.1.1.1,IP,Cloudfare DNS IP address,Cloudfare|DNS,green
|
||||
wannacry.exe,File,Wannacry sample found,Wannacry|Malware|PE,amber</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="placeholder">Choose CSV file to import : </label>
|
||||
<input id="input_upload_ioc" type="file" accept="text/csv">
|
||||
</div>
|
||||
</div>
|
||||
<div class='invalid-feedback' id='ioc-invalid-msg'></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-dark mr-auto" onclick="generate_sample_csv();">Download sample CSV</button>
|
||||
<button type="button" class="btn btn-outline-success" onclick="upload_ioc();">Upload</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</form>
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascripts %}
|
||||
|
||||
{% include 'includes/footer_case.html' %}
|
||||
<script src="/static/assets/js/iris/case.ioc.js"></script>
|
||||
|
||||
{% endblock javascripts %}
|
@ -0,0 +1,59 @@
|
||||
<div class="modal shortcut_modal bg-shadow-gradient" id="shortcutModal" tabindex="-1" aria-labelledby="shortcutModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="shortcutModalLabel">Shortcuts</h5>
|
||||
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true"><i class="fa fa-times"></i></span></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Shortcut</th>
|
||||
<th scope="col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>CTRL-S</td>
|
||||
<td>Save note</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CTRL-B</td>
|
||||
<td>Bold</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CTRL-I</td>
|
||||
<td>Italic</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CTRL-SHIFT-1</td>
|
||||
<td>Heading 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CTRL-SHIFT-2</td>
|
||||
<td>Heading 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CTRL-SHIFT-3</td>
|
||||
<td>Heading 3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CTRL-SHIFT-4</td>
|
||||
<td>Heading 4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CTRL-`</td>
|
||||
<td>Insert code</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CTRL-K</td>
|
||||
<td>Insert link</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
134
iris-web/source/app/blueprints/case/templates/case_notes.html
Normal file
134
iris-web/source/app/blueprints/case/templates/case_notes.html
Normal file
@ -0,0 +1,134 @@
|
||||
{% extends "layouts/default_ext.html" %} {% block title %} Case notes {% endblock title %} {% block stylesheets %}
|
||||
{% include 'includes/header_case.html' %}
|
||||
<link rel="stylesheet" href="/static/assets/css/bootstrap-select.min.css">
|
||||
{% endblock stylesheets %}
|
||||
{% block content %}
|
||||
{% include 'includes/navigation_ext.html' %}
|
||||
|
||||
{% include 'includes/sidenav.html' %}
|
||||
<div class="main-panel">
|
||||
|
||||
<div class="content">
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{{ form.hidden_tag() }}
|
||||
<nav class="navbar navbar-header navbar-expand-lg pt-2 pb-2 bg-primary-gradient">
|
||||
<div class="container-fluid">
|
||||
<div class="collapse search-flex" id="search-nav">
|
||||
<ul class="list-group list-group-bordered hidden-caret" id="notes_search_list"></ul>
|
||||
<input type="text" class="form-control mr-3" style="max-width:400px;" id="search_note_input" onkeyup="search_notes()" placeholder="Search in notes..">
|
||||
</div>
|
||||
<ul class="navbar-nav topbar-nav ml-md-auto align-items-center page-navigation page-navigation-style-2 page-navigation-secondary">
|
||||
<li class="nav-item ml-2">
|
||||
<span class="text-white text-sm mr-2" id="last_resfresh">Loading</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="btn btn-primary btn-sm" onclick="draw_kanban();">
|
||||
<span class="menu-title">Refresh</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="btn btn-dark btn-sm" onclick="add_remote_groupnote();">
|
||||
<span class="menu-title">Add notes group</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="page-inner">
|
||||
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
|
||||
<div id="empty-set-notes" style="display:none;">
|
||||
<h4 class="text-dark text-sm text-center ml-mr-auto">It looks pretty empty <i class="fa-solid fa-mug-hot ml-2"></i></h4>
|
||||
<h4 class="text-dark text-sm text-center ml-mr-auto"><a href="#" onclick="add_remote_groupnote();">Click here to add the first note group</a></h4>
|
||||
</div>
|
||||
<div class="row" id="card_main_load" style="display:none;">
|
||||
<div class="container-fluid">
|
||||
<div class="float-right mt-2 col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div id="myKanban" class="board">
|
||||
</div>
|
||||
</div>
|
||||
<div id="side_timeline">
|
||||
<button class="btn btn-round btn-primary-success btn_over_page_i" onclick="add_remote_groupnote();"><i class="fas fa-plus-circle"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kanban-item row" id="_subnote_" style="display: none;" onclick="edit_note(this);" title="">
|
||||
<a href="#" class="kanban-title text-truncate w-100" draggable="false">New note</a><br />
|
||||
<em><small href="#" class="text-sm text-muted kanban-info" draggable="false"><i
|
||||
class="flaticon-tool mr-1"></i>Hello</small></em>
|
||||
<iris_note style="display: none;" id="xqx00qxq">New note</iris_note>
|
||||
|
||||
<div class="kanban-badge avatar-group-note col-12" id="kanban_badge_">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div data-id="_todo" class="kanban-board" id="group_" title="" draggable="false" style="display: none;">
|
||||
<header class="kanban-board-header">
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<div contenteditable="true" maxlength="25" class="kanban-title-board" onclick="">Note group</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="kanban-title-button">
|
||||
<div class="row mr-1">
|
||||
<button class="mr-2" onclick="" style="display: none;"><i
|
||||
class="fas fa-check-circle text-success"></i></button>
|
||||
<button class="mr-2" onclick=""><i class="fas fa-plus-circle "></i></button>
|
||||
<div class="dropdown dropdown-kanban ">
|
||||
|
||||
<button class="dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
<i class="icon-options-vertical"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton ">
|
||||
<a class="dropdown-item" href="#" draggable="false">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="kanban-drag" id="_main">
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="modal modal-case-focus" tabindex="-1" role="dialog" id="modal_note_detail" data-backdrop="true">
|
||||
<div class="modal-dialog modal-xxl modal-xl" role="document">
|
||||
<div class="modal-content" id="info_note_modal_content">
|
||||
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% include 'includes/footer.html' %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
{% block javascripts %}
|
||||
{% include 'includes/footer_case.html' %}
|
||||
<script src="/static/assets/js/plugin/sortable/sortable.js "></script>
|
||||
<script src="/static/assets/js/core/socket.io.js"></script>
|
||||
<script src="/static/assets/js/iris/case.notes.js "></script>
|
||||
|
||||
<script>
|
||||
/* Wait for document to be ready before loading the kanban board */
|
||||
$(document).ready(function () {
|
||||
/* load board */
|
||||
boardNotes.init();
|
||||
setInterval(function() { check_update('notes/state'); }, 3000);
|
||||
draw_kanban();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock javascripts %}
|
105
iris-web/source/app/blueprints/case/templates/case_rfile.html
Normal file
105
iris-web/source/app/blueprints/case/templates/case_rfile.html
Normal file
@ -0,0 +1,105 @@
|
||||
{% extends "layouts/default_ext.html" %} {% block title %} Case Evidences {% endblock title %} {% block stylesheets %}
|
||||
{% include 'includes/header_case.html' %}
|
||||
{% endblock stylesheets %}
|
||||
{% block content %}
|
||||
|
||||
{% include 'includes/navigation_ext.html' %}
|
||||
|
||||
{% include 'includes/sidenav.html' %}
|
||||
<div class="main-panel">
|
||||
|
||||
<div class="content">
|
||||
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{{ form.hidden_tag() }}
|
||||
<nav class="navbar navbar-header navbar-expand-lg pt-2 pb-2 bg-primary-gradient">
|
||||
<div class="container-fluid">
|
||||
<div class="collapse" id="search-nav">
|
||||
<div id="tables_button"></div>
|
||||
</div>
|
||||
<ul class="navbar-nav topbar-nav ml-md-auto align-items-center page-navigation page-navigation-style-2 page-navigation-secondary">
|
||||
<li class="nav-item ml-2">
|
||||
<span class="text-white text-sm mr-2" id="last_resfresh">Loading</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="btn btn-primary btn-sm" onclick="reload_rfiles(true);">
|
||||
<span class="menu-title">Refresh</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button onclick="add_modal_rfile()" class="btn btn-dark btn-sm">
|
||||
<span class="menu-title">Register Evidence</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</nav>
|
||||
<div class="page-inner">
|
||||
<div class="row">
|
||||
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
|
||||
<div class="col-md-12">
|
||||
<div class="card" id="card_main_load" style="display:none;">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive" id="rfiles_table_wrapper">
|
||||
<div class="selectgroup">
|
||||
<span id="table_buttons"></span>
|
||||
</div>
|
||||
<table class="table display wrap col-border table-striped table-hover dataTable" width="100%"
|
||||
cellspacing="0" id="rfiles_table">
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Date</th>
|
||||
<th>Hash</th>
|
||||
<th>Size (bytes)</th>
|
||||
<th>Description</th>
|
||||
<th>Added by</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Date</th>
|
||||
<th>Hash</th>
|
||||
<th>Size(bytes)</th>
|
||||
<th>Description</th>
|
||||
<th>Added by</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" tabindex="-1" role="dialog" id="modal_add_rfiles" data-backdrop="true">
|
||||
<div class="modal-lg modal-dialog" role="document">
|
||||
<div class="modal-content" id="modal_add_rfiles_content">
|
||||
|
||||
</div>
|
||||
<!-- /.modal-content -->
|
||||
</div>
|
||||
<!-- /.modal-dialog -->
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% include 'includes/footer.html' %}
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
{% block javascripts %}
|
||||
{% include 'includes/footer_case.html' %}
|
||||
|
||||
<script src="/static/assets/js/iris/case.rfiles.js"></script>
|
||||
<script>
|
||||
</script>
|
||||
|
||||
{% endblock javascripts %}
|
109
iris-web/source/app/blueprints/case/templates/case_tasks.html
Normal file
109
iris-web/source/app/blueprints/case/templates/case_tasks.html
Normal file
@ -0,0 +1,109 @@
|
||||
{% extends "layouts/default_ext.html" %}
|
||||
|
||||
{% block title %} Case Tasks {% endblock title %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{% include 'includes/header_case.html' %}
|
||||
<link rel="stylesheet" href="/static/assets/css/bootstrap-multiselect.min.css">
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include 'includes/navigation_ext.html' %}
|
||||
|
||||
|
||||
{% include 'includes/sidenav.html' %}
|
||||
<div class="main-panel">
|
||||
|
||||
<div class="content">
|
||||
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{{ form.hidden_tag() }}
|
||||
<nav class="navbar navbar-header navbar-expand-lg pt-2 pb-2 bg-primary-gradient">
|
||||
<div class="container-fluid">
|
||||
<div class="collapse" id="search-nav">
|
||||
<div id="tables_button"></div>
|
||||
</div>
|
||||
<ul class="navbar-nav topbar-nav ml-md-auto align-items-center page-navigation page-navigation-style-2 page-navigation-secondary">
|
||||
<li class="nav-item ml-2">
|
||||
<span class="text-white text-sm mr-2" id="last_resfresh">Loading</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="btn btn-primary btn-sm" onclick="get_tasks();">
|
||||
<span class="menu-title">Refresh</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="btn btn-dark btn-sm" onclick="add_task();">
|
||||
<span class="menu-title">Add task</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="page-inner ">
|
||||
<div class="row ">
|
||||
<div class="col-md-12">
|
||||
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
|
||||
<section class="card" id="card_main_load" style="display:none;">
|
||||
|
||||
<div class="card-body">
|
||||
<div class="table-responsive" id="tasks_table_wrapper">
|
||||
<table class="table display wrap col-border table-striped table-hover dataTable" width="100%"
|
||||
cellspacing="0" id="tasks_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Status</th>
|
||||
<th>Assigned to</th>
|
||||
<th>Open date</th>
|
||||
<th>Tags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Status</th>
|
||||
<th>Assigned to</th>
|
||||
<th>Open date</th>
|
||||
<th>Tags</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal " tabindex="-1" role="dialog" id="modal_add_task" data-backdrop="true">
|
||||
<div class="modal-xxl modal-dialog" role="document">
|
||||
<div class="modal-content" id="modal_add_task_content">
|
||||
|
||||
</div>
|
||||
<!-- /.modal-content -->
|
||||
</div>
|
||||
<!-- /.modal-dialog -->
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% include 'includes/footer.html' %}
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascripts %}
|
||||
{% include 'includes/footer_case.html' %}
|
||||
|
||||
<script src="/static/assets/js/plugin/datatables/dataTables.cellEdit.js"></script>
|
||||
<script src="/static/assets/js/plugin/select/bootstrap-multiselect.min.js"></script>
|
||||
<script src="/static/assets/js/iris/case.tasks.js"></script>
|
||||
|
||||
|
||||
{% endblock javascripts %}
|
178
iris-web/source/app/blueprints/case/templates/case_timeline.html
Normal file
178
iris-web/source/app/blueprints/case/templates/case_timeline.html
Normal file
@ -0,0 +1,178 @@
|
||||
{% extends "layouts/default_ext.html" %}
|
||||
|
||||
{% block title %} Case Timeline {% endblock title %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<link rel="stylesheet" href="/static/assets/css/bootstrap-datetime.css">
|
||||
<link rel="stylesheet" href="/static/assets/css/suggestags.css">
|
||||
<link rel="stylesheet" href="/static/assets/css/bootstrap-select.min.css">
|
||||
<link rel="stylesheet" href="/static/assets/css/select2.css">
|
||||
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% include 'includes/navigation_ext.html' %}
|
||||
|
||||
|
||||
{% include 'includes/sidenav.html' %}
|
||||
<div class="main-panel">
|
||||
|
||||
<div class="content">
|
||||
<!-- Navbar Header -->
|
||||
<nav class="navbar navbar-header navbar-expand-lg bg-primary-gradient">
|
||||
{{ form.hidden_tag() }}
|
||||
<ul class="container-fluid mt-3 mb--2">
|
||||
<ul class="navbar-nav col-8">
|
||||
<li class="nav-item hidden-caret col-12">
|
||||
<div class="row">
|
||||
<div id='timeline_filtering' class="col-9 pt-2 pl-2" style="border-radius:3px;" ></div>
|
||||
<button class="btn btn-sm btn-light ml-2 pt-2" onclick="filter_timeline();">
|
||||
Apply filter
|
||||
</button>
|
||||
<button class="btn btn-sm btn-light ml-1 pt-2" onclick="reset_filters();">
|
||||
Reset
|
||||
</button>
|
||||
<i class="ml-1 mt-1 fa-regular text-white fa-circle-question" title="Filter help" style="cursor:pointer;" onclick="show_timeline_filter_help();"></i>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav topbar-nav ml-md-auto align-items-center page-navigation page-navigation-style-2 page-navigation-secondary">
|
||||
<li class="nav-item ml-2">
|
||||
<span class="text-white text-sm mr-2" id="last_resfresh">Loading</span>
|
||||
</li>
|
||||
<li class="nav-item hidden-caret">
|
||||
<button class="btn btn-primary btn-sm" onclick="get_or_filter_tm();">
|
||||
<span class="menu-title">Refresh</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li class="nav-item hidden-caret">
|
||||
<button class="btn btn-dark btn-sm" onclick="add_event();">
|
||||
<span class="menu-title">Add event</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-border btn-black" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="menu-title"><i class="fas fa-ellipsis-v"></i></span>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item" href="timeline/visualize?cid={{session['current_case'].case_id}}"> Visualize</a>
|
||||
<a class="dropdown-item" href="timeline/visualize?cid={{session['current_case'].case_id}}&group-by=asset"> Visualize by asset</a>
|
||||
<a class="dropdown-item" href="timeline/visualize?cid={{session['current_case'].case_id}}&group-by=category">Visualize by category</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" onclick="timelineToCsv();"><small class="fa fa-download mr-2"></small> Download as CSV</a>
|
||||
<a class="dropdown-item" href="#" onclick="timelineToCsvWithUI();"><small class="fa fa-download mr-2"></small> Download as CSV with user info</a>
|
||||
<!-- BEGIN_RS_CODE -->
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" onclick="fire_upload_csv_events();"><small class="fa fa-upload mr-2"></small> Upload CSV of events</a>
|
||||
<!-- END_RS_CODE -->
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
|
||||
<div class="page-inner">
|
||||
<div class="row">
|
||||
<div class="loader1 text-center ml-mr-auto" id="loading_msg">Loading...</div>
|
||||
<div class="col-md-12" id="card_main_load" style="display:none;">
|
||||
<div id="paginator"></div>
|
||||
<ul class="timeline" id="timeline_list">
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="side_timeline">
|
||||
|
||||
<div class="btn_over_page_a">
|
||||
<button class="btn btn-round btn-light btn-conditional-2 colorinput-color bg-white" title="color1" onclick="events_set_attribute('event_color', '#fff')"></button>
|
||||
<button class="btn btn-round btn-light btn-conditional-2 colorinput-color bg-primary" title="color2" onclick="events_set_attribute('event_color', '#1572E899')"></button>
|
||||
<button class="btn btn-round btn-light btn-conditional-2 colorinput-color bg-secondary" title="color3" onclick="events_set_attribute('event_color', '#6861CE99')"></button>
|
||||
<button class="btn btn-round btn-light btn-conditional-2 colorinput-color bg-info" title="color4" onclick="events_set_attribute('event_color', '#48ABF799')"></button>
|
||||
<button class="btn btn-round btn-light btn-conditional-2 colorinput-color bg-success" title="color5" onclick="events_set_attribute('event_color', '#31CE3699')"></button>
|
||||
<button class="btn btn-round btn-light btn-conditional-2 colorinput-color bg-danger" title="color5" onclick="events_set_attribute('event_color', '#F2596199')"></button>
|
||||
<button class="btn btn-round btn-light btn-conditional-2 colorinput-color bg-warning" title="color5" onclick="events_set_attribute('event_color', '#FFAD4699')"></button>
|
||||
<button class="btn btn-round btn-light btn-conditional" title="Change color" onclick="toggle_colors()"><i class="fas fa-tint"></i></button>
|
||||
</div>
|
||||
<button class="btn btn-round btn-light btn_over_page_delete btn-conditional" title="Delete selected events" onclick="events_bulk_delete();"><i class="fas fa-trash text-danger"></i></button>
|
||||
<button class="btn btn-round btn-light btn_over_page_b btn-conditional" title="Toggle Summary" onclick="events_set_attribute('event_in_summary')"><i class="fas fa-newspaper"></i></button>
|
||||
<button class="btn btn-round btn-light btn_over_page_c btn-conditional" title="Toggle Graph" onclick="events_set_attribute('event_in_graph')"><i class="fas fa-share-alt"></i></button>
|
||||
<button class="btn btn-round btn-light btn_over_page_d" title="Select rows" onclick="toggle_selector();" id="selector-btn"><i class="fas fa-check"></i></button>
|
||||
<button class="btn btn-round btn-light btn_over_page_e" title="Add new event" onclick="add_event();"><i class="fas fa-plus-circle"></i></button>
|
||||
<button class="btn btn-round btn-light btn_over_page_f" title="Refresh" onclick="get_or_filter_tm();"><i class="fas fa-redo-alt"></i></button>
|
||||
<button class="btn btn-round btn-light btn_over_page_g" title="Go at the top" onclick="to_page_up();"><i class="fas fa-arrow-up"></i></button>
|
||||
<button class="btn btn-round btn-light btn_over_page_h" title="Go at the bottom" onclick="to_page_down();"><i class="fas fa-arrow-down"></i></button>
|
||||
<button class="btn btn-round btn-light btn_over_page_i" title="Toggle compact view" onclick="toggle_compact_view();"><i class="fas fa-list"></i></button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal shadow-lg" tabindex="-1" id="modal_add_event" data-backdrop="true">
|
||||
<div class="modal-xl modal-dialog" role="document">
|
||||
<div class="modal-content" id="modal_add_event_content">
|
||||
|
||||
</div>
|
||||
<!-- /.modal-content -->
|
||||
</div>
|
||||
<!-- /.modal-dialog -->
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- BEGIN RS_CODE -->
|
||||
<div class="modal " tabindex="-1" role="dialog" id="modal_upload_csv_events" data-backdrop="true">
|
||||
<div class="modal-lg modal-dialog" role="document">
|
||||
<form method="post" action="" id="form_upload_csv_events">
|
||||
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5>Upload events list (CSV format)</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="csv_format" class="placeholder">Expected Events CSV File format</label>
|
||||
<textarea id="csv_format" class="form-control col-md-12 col-sm-12 sizable-textarea" rows="2" disabled>event_timestamp,event_title,event_description,linked_assets,linked_IoCs,event_tags (separated with "|"),event_color,event_raw_data</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="placeholder">Events CSV File format example</label>
|
||||
<textarea class="form-control col-md-12 col-sm-12 sizable-textarea" rows="3" disabled>
|
||||
event_date,event_tz,event_title,event_category,event_content,event_raw,event_source,event_assets,event_iocs,event_tags
|
||||
"2023-03-26T03:00:30.000","+00:00","An event","Unspecified","Event description","raw","source","","","defender|malicious"
|
||||
"2023-03-23T12:00:35.000","+00:00","An event","Legitimate","Event description","raw","source","","","admin_action"
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="placeholder">Choose CSV file to import : </label>
|
||||
<input id="input_upload_csv_events" type="file" accept="text/csv">
|
||||
</div>
|
||||
</div>
|
||||
<div class='invalid-feedback' id='ioc-invalid-msg'></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-dark mr-auto" onclick="generate_events_sample_csv();">Download sample CSV</button>
|
||||
<button type="button" class="btn btn-outline-success" onclick="upload_csv_events();">Upload</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</form>
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div>
|
||||
<!-- END_RS_CODE -->
|
||||
{% include 'includes/footer.html' %}
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
{% block javascripts %}
|
||||
|
||||
{% include 'includes/footer_case.html' %}
|
||||
<script src="/static/assets/js/iris/case.timeline.js"></script>
|
||||
<script src="/static/assets/js/timeline.js"></script>
|
||||
|
||||
{% endblock javascripts %}
|
@ -0,0 +1,201 @@
|
||||
<div class="modal-header">
|
||||
<div class="col md-12">
|
||||
<div class="row">
|
||||
<div class="col align-self-center">
|
||||
<h4 class="modal-title mr-4">{{ "Asset #{}".format(asset.asset_id) if asset.asset_name else "Add asset" }}</h4>
|
||||
<small><a class="text-muted">{{ "#{}".format(asset.asset_uuid) if asset.asset_uuid else "" }}</a></small>
|
||||
</div>
|
||||
{% include 'modals/modal_attributes_nav.html' %}
|
||||
<div class="col">
|
||||
<div class="row float-right">
|
||||
{% if asset.asset_id %}
|
||||
<div class="dropdown">
|
||||
<button class="btn bg-transparent pull-right" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
<span aria-hidden="true"><i class="fas fa-ellipsis-v"></i></span>
|
||||
</button>
|
||||
<div class="dropdown-menu pull-right" id="asset_modal_quick_actions" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item" href="#" onclick='copy_object_link({{asset.asset_id}});return false;'><i class="fa fa-share mr-2"></i>Share</a>
|
||||
<a class="dropdown-item" href="#" onclick='copy_object_link_md("asset", {{asset.asset_id}});return false;'><i class="fa-brands fa-markdown mr-2"></i>Markdown Link</a>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn bg-transparent btn-xs" onclick="comment_element({{ asset.asset_id }}, 'assets')" title="Comments">
|
||||
<span class="btn-label">
|
||||
<i class="fa-solid fa-comments"></i><span class="notification" id="object_comments_number">{{ comments_map|length }}</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<button class="float-right btn bg-transparent" title="Minimize" onclick="modal_minimized('modal_add_asset', '{{ "Asset {}".format(asset.asset_name) if asset.asset_name else "Add asset" }}');"> <i class='fa fa-minus'></i> </button>
|
||||
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true"><i class="fa fa-times"></i></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div role="tabpanel">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="details">
|
||||
<div class="container col-md-12">
|
||||
<form method="post" action="" id="form_new_asset">
|
||||
<div class="col-md-12 col-lg-12 col-sm-12">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="form-row ml-2">
|
||||
<div class="form-group col-6">
|
||||
<label for="name" class="placeholder">Asset Name *</label>
|
||||
{{ form.asset_name(class='form-control', autocomplete="off") }}
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="asset_type" class="placeholder">Asset Type *</label>
|
||||
{{ form.asset_type_id(class="selectpicker form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mt-3">
|
||||
<label for="asset_description" class="placeholder">Description</label>
|
||||
<div class="md_description_field">
|
||||
<div class="form-group mt--2">
|
||||
<button type="button" class="float-right icon-note btn btn-circle btn-sm mt-2" onclick="edit_in_asset_desc();">
|
||||
</button>
|
||||
<button type="button" style="display: none;" class="btn btn-dark btn-sm float-right mr-2 mt-2" onclick="preview_asset_description();" id="asset_preview_button"><i class="fa-solid fa-eye"></i></button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col mb--2 ml--2" id="asset_edition_btn" style="display:none;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-left:0px;">
|
||||
<div class="col-12" id="container_asset_desc_content">
|
||||
<div id="asset_description" class="mr-2" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}">{% if asset %}{{ asset.asset_description }}{% endif %}</div>
|
||||
<textarea id="asset_desc_content" rows="10" cols="82" style="display: none"></textarea>
|
||||
</div>
|
||||
<div class="col-12" id="container_asset_description" style="display:none">
|
||||
<div id="target_asset_desc"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row ml-2">
|
||||
<div class="form-group col-6">
|
||||
<label for="asset_domain" class="placeholder">Domain</label>
|
||||
{{ form.asset_domain(class='form-control', autocomplete="off") }}
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="asset_ip" class="placeholder">IP</label>
|
||||
{{ form.asset_ip(class='form-control', autocomplete="off") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<a class="btn btn-light btn-sm" data-toggle="collapse" href="#collapseAddInfo" role="button" aria-expanded="false" aria-controls="collapseAddInfo">> Additional information</a>
|
||||
<div class="collapse" id="collapseAddInfo">
|
||||
<div class="card card-body">
|
||||
<label for="asset_info" class="placeholder">Additional information</label>
|
||||
{{ form.asset_info(class='form-control col-md-12 col-sm-12 sizable-textarea', autocomplete="off") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row ml-2">
|
||||
<div class="form-group col-6">
|
||||
<label for="asset_compromise_status_id" class="placeholder mt-2">Compromise Status </label>
|
||||
{{ form.asset_compromise_status_id(class="selectpicker col-9") }}
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="analysis_status_id" class="placeholder mt-2">Analysis Status </label>
|
||||
{{ form.analysis_status_id(class="selectpicker col-9 float-right") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="asset_tags">Asset tags
|
||||
</label>
|
||||
<input type="text" id="asset_tags"
|
||||
class="form-control col-md-12" {% if asset.asset_tags %} value="{{ asset.asset_tags }}" {% endif %}/>
|
||||
</div>
|
||||
<div class="form-group" data-select2-id="7">
|
||||
<label>Related IOC</label>
|
||||
<div class="select2-input ml-12" data-select2-id="6">
|
||||
<select id="ioc_links" name="ioc_links" class="form-control select2-hidden-accessible ml-12" multiple="" data-select2-id="ioc_links" tabindex="-1" aria-hidden="true" style="width: 100%">
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% include 'modals/modal_attributes_tabs.html' %}
|
||||
</div>
|
||||
{% if asset.asset_id %}
|
||||
<button type="button" class="btn btn-outline-danger ml-4 mt-5"
|
||||
onclick="delete_asset({{ asset.asset_id }});">Delete</button>
|
||||
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right" id="submit_new_asset">Update</button>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right" id="submit_new_asset">Save</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$('form#form_new_case').validate();
|
||||
$('#asset_tags').amsifySuggestags({
|
||||
printValues: false,
|
||||
suggestions: [ {% for tag in tags %} '{{ tag }}', {% endfor %} ]
|
||||
});
|
||||
$('#asset_type_id').selectpicker({
|
||||
liveSearch: true,
|
||||
title: "None",
|
||||
style: "btn-outline-white",
|
||||
|
||||
});
|
||||
$('#analysis_status_id').selectpicker({
|
||||
liveSearch: true,
|
||||
title: "None",
|
||||
style: "btn-outline-white"
|
||||
});
|
||||
$('#analysis_status_id').selectpicker('val', '1');
|
||||
|
||||
$('#asset_compromise_status_id').selectpicker({
|
||||
liveSearch: true,
|
||||
title: "To be determined",
|
||||
style: "btn-outline-white"
|
||||
});
|
||||
$('#asset_compromise_status_id').selectpicker('val', '0');
|
||||
</script>
|
||||
|
||||
{% if asset.asset_id %}
|
||||
<script>
|
||||
$('#asset_type_id').selectpicker('val', '{{ asset.asset_type_id }}');
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if asset.analysis_status_id %}
|
||||
<script>
|
||||
$('#analysis_status_id').selectpicker('val', '{{ asset.analysis_status_id }}');
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if asset.asset_compromise_status_id %}
|
||||
<script>
|
||||
$('#asset_compromise_status_id').selectpicker('val', '{{ asset.asset_compromise_status_id }}');
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if ioc %}
|
||||
<script>
|
||||
var data = [
|
||||
{% for e in ioc %}
|
||||
{
|
||||
id: {{ e.ioc_id }},
|
||||
text: {{ e.ioc_value| tojson }}
|
||||
},
|
||||
{% endfor %}
|
||||
];
|
||||
$('#ioc_links').select2({ data: data });
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if ioc_prefill %}
|
||||
<script>
|
||||
$('#ioc_links').val([
|
||||
{% for ioc in ioc_prefill %} {{ ioc[0] }}, {% endfor %}
|
||||
]);
|
||||
$('#ioc_links').trigger('change');
|
||||
</script>
|
||||
{% endif %}
|
@ -0,0 +1,327 @@
|
||||
<div class="modal-header">
|
||||
<div class="col md-12">
|
||||
<div class="row">
|
||||
<div class="col align-self-center">
|
||||
<h4 class="modal-title mr-4">{% if event.event_id %} Event ID #{{ event.event_id }} {% else %} Add event {% endif %}
|
||||
{% if event.modification_history %}
|
||||
<i class="fa-solid fa-clock-rotate-left ml-3 mt-2" data-toggle="popover" data-html="true" id="pop_history" style="cursor: pointer;"
|
||||
title="Modifications history"
|
||||
data-content="<small>{% for mod in event.modification_history %}<code>{{ mod|format_datetime('%Y-%m-%d %H:%M') }}</code> - {{ event.modification_history[mod].action }} by {{ event.modification_history[mod].user }}<br/>{% endfor %}</small>">
|
||||
</i>
|
||||
{% endif %}
|
||||
</h4>
|
||||
<small><i class="text-muted">{% if event.event_uuid %}#{{ event.event_uuid }}{% endif %}</i></small>
|
||||
</div>
|
||||
{% include 'modals/modal_attributes_nav.html' %}
|
||||
<div class="col ">
|
||||
<div class="row float-right">
|
||||
{% if event.event_id %}
|
||||
<div class="dropdown">
|
||||
<button class="btn bg-transparent pull-right" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
<span aria-hidden="true"><i class="fas fa-ellipsis-v"></i></span>
|
||||
</button>
|
||||
<div class="dropdown-menu pull-right" id="event_modal_quick_actions" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item" href="#" onclick='copy_object_link({{event.event_id}});return false;'><i class="fa fa-share mr-2"></i>Share</a>
|
||||
<a class="dropdown-item" href="#" onclick='copy_object_link_md("event", {{event.event_id}});return false;'><i class="fa-brands fa-markdown mr-2"></i>Markdown Link</a>
|
||||
<a class="dropdown-item" href="#" onclick='duplicate_event({{event.event_id}});return false;'><i class="fa fa-clone mr-2"></i>Duplicate</a>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn bg-transparent btn-xs" onclick="comment_element({{ event.event_id }}, 'timeline/events')" title="Comments">
|
||||
<span class="btn-label">
|
||||
<i class="fa-solid fa-comments"></i><span class="notification" id="object_comments_number">{{ comments_map|length }}</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button class="float-right btn bg-transparent" title="Minimize" onclick="modal_minimized('modal_add_event', '{% if event.event_id %} Event ID #{{ event.event_id }} {% else %} Add event {% endif %}');"> <i class='fa fa-minus'></i> </button>
|
||||
<button type="button" class="float-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true"><i class="fa fa-times"></i></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div role="tabpanel">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="details">
|
||||
<div class="container col-md-12">
|
||||
<form method="post" action="" id="form_new_event">
|
||||
<div class="col-md-12 col-lg-12 col-sm-12">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="row">
|
||||
<div class="form-group col-xl-5 col-md-12">
|
||||
<label for="event_title" class="placeholder">{{ form.event_title.label.text }} *</label>
|
||||
{{ form.event_title(class='form-control col-md-12 col-sm-12', autocomplete="off") }}
|
||||
</div>
|
||||
<div class="form-group col-xl-7 col-md-12">
|
||||
<label for="event_timetitle" class="placeholder ml-2">Event Time *</label>
|
||||
<div class="row ml-2" id="event_date_inputs">
|
||||
<input class="form-control col-5 mr-2" type="date" id="event_date" {% if event.event_date_wtz %} value="{{ event.event_date_wtz.strftime('%Y-%m-%d') }}"{% endif %}>
|
||||
<span></span>
|
||||
<input class="form-control col-4" type="time" step="0.001" id="event_time" {% if event.event_date_wtz %} value="{{ event.event_date_wtz.strftime('%H:%M:%S.%f')[:-3] }}" {% else %} value="00:00:00.000" {% endif %}>
|
||||
<span></span>
|
||||
<input class="form-control col-2" type="text" id="event_tz" {% if event.event_tz %} value="{{ event.event_tz }}" {% else %} value="+00:00" {% endif %}>
|
||||
<button class="btn btn-sm btn-outline-white" type="button" onclick="show_time_converter();return false;"><i class="fas fa-magic"></i></button>
|
||||
</div>
|
||||
<div class="row ml-2" id="event_date_convert" style="display:none;">
|
||||
<div class="input-group ">
|
||||
<input class="form-control col-9" type="text" id="event_date_convert_input" placeholder="Enter date in any format and submit to try auto-parsing">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-sm btn-outline-secondary mr-2" type="button" onclick="time_converter();return false;">Submit</button>
|
||||
<button class="btn btn-sm btn-outline" type="button" onclick="hide_time_converter();return false;"><i class="fas fa-magic"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<span id="convert_bad_feedback" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group mt-3 col-12">
|
||||
<label for="event_content" class="placeholder">Event description</label>
|
||||
<div class="md_description_field">
|
||||
<div class="form-group mt--2">
|
||||
<button type="button" class="float-right icon-note btn btn-circle btn-sm mt-2" onclick="edit_in_event_desc();" >
|
||||
</button>
|
||||
<button type="button" style="display: none;" class="btn btn-dark btn-sm float-right mr-2 mt-2"
|
||||
onclick="preview_event_description();" id="event_preview_button"><i class="fa-solid fa-eye"></i></button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col mb--2 ml--2" id="event_edition_btn" style="display:none;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-left:0px;">
|
||||
<div class="col-12" id="container_event_desc_content">
|
||||
<div id="event_description" class="mr-2" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}">{% if event.event_content %}{{ event.event_content }}{% endif %}</div>
|
||||
<textarea id="event_desc_content" rows="10" cols="82" style="display: none"></textarea>
|
||||
</div>
|
||||
<div class="col-12" id="container_event_description" style="display:none">
|
||||
<div id="target_event_desc"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-12 mt-1">
|
||||
<a class="btn btn-light btn-sm" data-toggle="collapse" href="#collapseRawEvent" role="button" aria-expanded="false" aria-controls="collapseRawEvent">> Edit raw event data</a>
|
||||
<div class="collapse" id="collapseRawEvent">
|
||||
<div class="card card-body">
|
||||
<label for="event_raw" class="placeholder">{{ form.event_raw.label.text }}</label>
|
||||
{{ form.event_raw(class='form-control sizable-textarea', autocomplete="off") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-xl-6 col-md-12">
|
||||
<label for="event_title" class="placeholder">{{ form.event_source.label.text }}</label>
|
||||
{{ form.event_source(class='form-control col-md-12 col-sm-12', autocomplete="off") }}
|
||||
</div>
|
||||
<div class="form-group col-xl-6 col-md-12">
|
||||
<label for="event_tags">Event tags
|
||||
</label>
|
||||
<input type="text" id="event_tags"
|
||||
class="form-control col-md-12" {% if event.event_tags %} value="{{ event.event_tags }}" {% endif %}/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-12">
|
||||
<label for="event_assets">Link to assets
|
||||
</label>
|
||||
<div class="select2-input ml-12" data-select2-id="6">
|
||||
<select id="event_assets" name="event_assets" class="form-control select2-hidden-accessible ml-12" multiple="" data-select2-id="event_assets" tabindex="-1" aria-hidden="true" style="width: 100%">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-10">
|
||||
<label for="event_assets">Link to IOCs
|
||||
</label>
|
||||
<div class="select2-input ml-12" data-select2-id="6">
|
||||
<select id="event_iocs" name="event_iocs" class="form-control select2-hidden-accessible ml-12" multiple="" data-select2-id="event_iocs" tabindex="-1" aria-hidden="true" style="width: 100%">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-2">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label mt-3">
|
||||
<input class="form-check-input" type="checkbox" id="event_sync_iocs_assets" checked>
|
||||
<span class="form-check-sign"> Push IOCs to assets
|
||||
<i class="ml-1 mt-1 fa-regular fa-circle-question" title="If checked, the IOCs related to this event will be associated with the specified assets" style="cursor:pointer;"></i>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-5">
|
||||
<label for="event_category_id" class="form-label">{{ form.event_category_id.label.text }}</label>
|
||||
<div class="row col-12">
|
||||
{{ form.event_category_id(class="selectpicker") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xl-2 col-md-12">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label mt-3">
|
||||
{{ form.event_in_summary(class="form-check-input", type="checkbox") }}
|
||||
<span class="form-check-sign"> Add to summary
|
||||
<i class="ml-1 mt-1 fa-regular fa-circle-question" title="If checked, the event will be integrated in the Timeline Visualization" style="cursor:pointer;"></i>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xl-2 col-md-12">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label mt-3">
|
||||
{{ form.event_in_graph(class="form-check-input", type="checkbox") }}
|
||||
<span class="form-check-sign"> Display in graph
|
||||
<i class="ml-1 mt-1 fa-regular fa-circle-question" title="If checked, the event will be integrated in the Graph section of the case" style="cursor:pointer;"></i>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xl-3 col-md-12">
|
||||
<label class="form-label">Event color</label>
|
||||
<div class="row gutters-xs">
|
||||
<div class="col-auto">
|
||||
<label class="selectgroup-item">
|
||||
<input name="event_color" type="radio" value="#fff" {% if event.event_color == "#fff" %} checked="checked" {% endif %} class="colorinput-input">
|
||||
<span class="colorinput-color bg-white"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="selectgroup-item">
|
||||
<input name="event_color" type="radio" value="#1572E899" {% if event.event_color == "#1572E899" %} checked="checked" {% endif %} class="colorinput-input">
|
||||
<span class="colorinput-color bg-primary"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="selectgroup-item">
|
||||
<input name="event_color" type="radio" value="#6861CE99" {% if event.event_color == "#6861CE99" %} checked="checked" {% endif %} class="colorinput-input">
|
||||
<span class="colorinput-color bg-secondary"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="selectgroup-item">
|
||||
<input name="event_color" type="radio" value="#48ABF799" {% if event.event_color == "#48ABF799" %} checked="checked" {% endif %}class="colorinput-input">
|
||||
<span class="colorinput-color bg-info"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="selectgroup-item">
|
||||
<input name="event_color" type="radio" value="#31CE3699" {% if event.event_color == "#31CE3699" %} checked="checked" {% endif %} class="colorinput-input">
|
||||
<span class="colorinput-color bg-success"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="selectgroup-item">
|
||||
<input name="event_color" type="radio" value="#F2596199" {% if event.event_color == "#F2596199" %} checked="checked" {% endif %} class="colorinput-input">
|
||||
<span class="colorinput-color bg-danger"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="selectgroup-item">
|
||||
<input name="event_color" type="radio" value="#FFAD4699" {% if event.event_color == "#FFAD4699" %} checked="checked" {% endif %} class="colorinput-input">
|
||||
<span class="colorinput-color bg-warning"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'modals/modal_attributes_tabs.html' %}
|
||||
</div>
|
||||
{% if event.event_id %}
|
||||
<button type="button" class="btn btn-outline-danger mt-5"
|
||||
onclick="delete_event({{ event.event_id }} );">Delete</button>
|
||||
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right" id="submit_new_event"
|
||||
onclick="update_event({{ event.event_id }} );">Update</button>
|
||||
|
||||
{% else %}
|
||||
|
||||
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
|
||||
id="submit_new_event">Save</button>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$('#event_tags').amsifySuggestags({
|
||||
printValues: true,
|
||||
suggestions: [ {% for tag in tags %} '{{ tag }}', {% endfor %} ],
|
||||
whiteList: false,
|
||||
selectOnHover: false,
|
||||
});
|
||||
$('[data-toggle="popover"]').popover();
|
||||
$('#event_category_id').selectpicker({
|
||||
width:"100%",
|
||||
liveSearch: true,
|
||||
title: "None",
|
||||
style: "btn-outline-white",
|
||||
});
|
||||
</script>
|
||||
|
||||
{% if assets %}
|
||||
<script>
|
||||
var data = [
|
||||
{% for e in assets %}
|
||||
{
|
||||
id: {{ e.asset_id }},
|
||||
text: {{ e.asset_name| tojson }}
|
||||
},
|
||||
{% endfor %}
|
||||
];
|
||||
$('#event_assets').select2({ data: data });
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if iocs %}
|
||||
<script>
|
||||
var data = [
|
||||
{% for e in iocs %}
|
||||
{
|
||||
id: {{ e.ioc_id }},
|
||||
text: {{ e.ioc_value| tojson }}
|
||||
},
|
||||
{% endfor %}
|
||||
];
|
||||
$('#event_iocs').select2({ data: data });
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if category %}
|
||||
<script>
|
||||
$('#event_category_id').val([
|
||||
{{ category[0].id }},
|
||||
]);
|
||||
$('#event_category_id').trigger('change');
|
||||
</script>
|
||||
{% else %}
|
||||
<script>
|
||||
$('#event_category_id').val(1);
|
||||
$('#event_category_id').trigger('change');
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if assets_prefill %}
|
||||
<script>
|
||||
$('#event_assets').val([
|
||||
{% for asset in assets_prefill %} {{ asset }}, {% endfor %}
|
||||
]);
|
||||
$('#event_assets').trigger('change');
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if iocs_prefill %}
|
||||
<script>
|
||||
$('#event_iocs').val([
|
||||
{% for ioc in iocs_prefill %} {{ ioc }}, {% endfor %}
|
||||
]);
|
||||
$('#event_iocs').trigger('change');
|
||||
</script>
|
||||
{% endif %}
|
@ -0,0 +1,150 @@
|
||||
<div class="modal-header">
|
||||
<div class="col md-12">
|
||||
<div class="row">
|
||||
<div class="col align-self-center">
|
||||
<h4 class="modal-title mr-4">{% if ioc.ioc_id %}Edit IOC #{{ ioc.ioc_id }}{% else %} Add IOC {% endif %}</h4>
|
||||
<small><i class="text-muted">{% if ioc.ioc_uuid %}#{{ ioc.ioc_uuid }}{% endif %}</i></small>
|
||||
</div>
|
||||
{% include 'modals/modal_attributes_nav.html' %}
|
||||
<div class="col">
|
||||
<div class="row float-right">
|
||||
{% if ioc.ioc_id %}
|
||||
<div class="dropdown">
|
||||
<button class="btn bg-transparent pull-right" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
<span aria-hidden="true"><i class="fas fa-ellipsis-v"></i></span>
|
||||
</button>
|
||||
<div class="dropdown-menu pull-right" id="ioc_modal_quick_actions" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item" href="#" onclick='copy_object_link({{ioc.ioc_id}});return false;'><i class="fa fa-share mr-2"></i>Share</a>
|
||||
<a class="dropdown-item" href="#" onclick='copy_object_link_md("ioc", {{ioc.ioc_id}});return false;'><i class="fa-brands fa-markdown mr-2"></i>Markdown Link</a>
|
||||
</div>
|
||||
<div class="dropdown-menu pull-right" aria-labelledby="dropdownMenuButton">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn bg-transparent btn-xs" onclick="comment_element({{ ioc.ioc_id }}, 'ioc')" title="Comments">
|
||||
<span class="btn-label">
|
||||
<i class="fa-solid fa-comments"></i><span class="notification" id="object_comments_number">{{ comments_map|length }}</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<button class="float-right btn bg-transparent" title="Minimize" onclick="modal_minimized('modal_add_ioc', '{% if ioc.ioc_id %}Edit IOC #{{ ioc.ioc_id }} {% else %} Add IOC {% endif %}');"> <i class='fa fa-minus'></i> </button>
|
||||
<button type="button" class="float-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true"><i class="fa fa-times"></i></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div role="tabpanel">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="details">
|
||||
<div class="container col-md-12">
|
||||
<form method="post" action="" id="form_new_ioc">
|
||||
<div class="col-md-12 col-lg-12 col-sm-12">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="form-group row">
|
||||
<div class="col-6">
|
||||
<label for="ioc_type" class="mr-4">Type *
|
||||
</label>
|
||||
{{ form.ioc_type_id(class="selectpicker pl--6 col-10") }}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="ioc_type" class="mr-4">TLP *
|
||||
</label>
|
||||
{{ form.ioc_tlp_id(class="selectpicker pl--6 col-10") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ioc_value" class="placeholder">{{ form.ioc_value.label.text }} *</label>
|
||||
{{ form.ioc_value(class='form-control col-md-12 col-sm-12 sizable-textarea', autocomplete="off") }}
|
||||
</div>
|
||||
{% if not ioc.ioc_id %}
|
||||
<div class="form-group col-2">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label mt-3">
|
||||
<input class="form-check-input" type="checkbox" id="ioc_one_per_line" checked>
|
||||
<span class="form-check-sign"> One IOC per line
|
||||
<i class="ml-1 mt-1 fa-regular fa-circle-question" title="If checked, each new line will create a new IOC" style="cursor:pointer;"></i>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-group mt-3">
|
||||
<label for="ioc_description" class="placeholder">Description</label>
|
||||
<div class="md_description_field">
|
||||
<div class="form-group mt--2">
|
||||
<button type="button" class="float-right icon-note btn btn-circle btn-sm mt-2" onclick="edit_in_ioc_desc();">
|
||||
</button>
|
||||
<button type="button" style="display: none;" class="btn btn-dark btn-sm float-right mr-2 mt-2"
|
||||
onclick="preview_ioc_description();" id="ioc_preview_button"><i class="fa-solid fa-eye"></i></button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col mb--2 ml--2" id="ioc_edition_btn" style="display:none;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-left:0px;">
|
||||
<div class="col-12" id="container_ioc_desc_content">
|
||||
<div id="ioc_description" class="mr-2" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}">{% if ioc and ioc.ioc_description %}{{ ioc.ioc_description }}{% endif %}</div>
|
||||
<textarea id="ioc_desc_content" rows="10" cols="82" style="display: none"></textarea>
|
||||
</div>
|
||||
<div class="col-12" id="container_ioc_description" style="display:none">
|
||||
<div id="target_ioc_desc"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ioc_tags">IOC tags
|
||||
</label>
|
||||
<input type="text" id="ioc_tags"
|
||||
class="form-control col-md-12" {% if ioc.ioc_tags %} value="{{ ioc.ioc_tags }}" {% endif %}/>
|
||||
</div>
|
||||
|
||||
<div class='invalid-feedback' id='ioc-invalid-msg'></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'modals/modal_attributes_tabs.html' %}
|
||||
</div>
|
||||
{% if ioc.ioc_id %}
|
||||
<button type="button" class="btn btn-outline-danger mt-5"
|
||||
onclick="delete_ioc('{{ ioc.ioc_id }}');">Delete</button>
|
||||
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right" id="submit_new_ioc"
|
||||
onclick="update_ioc('{{ ioc.ioc_id }}');">Update</button>
|
||||
|
||||
{% else %}
|
||||
|
||||
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
|
||||
id="submit_new_ioc">Save</button>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
<script>
|
||||
$('form#form_new_ioc').validate();
|
||||
$('#ioc_tags').amsifySuggestags({
|
||||
printValues: false,
|
||||
suggestions: [ {% for tag in tags %} '{{ tag }}', {% endfor %} ]
|
||||
});
|
||||
|
||||
$('#ioc_type_id').selectpicker({
|
||||
liveSearch: true,
|
||||
title: "None",
|
||||
style: "btn-outline-white",
|
||||
size: 10
|
||||
});
|
||||
|
||||
$('#ioc_tlp_id').selectpicker({
|
||||
liveSearch: true,
|
||||
title: "None",
|
||||
style: "btn-outline-white",
|
||||
});
|
||||
|
||||
{% if ioc.ioc_id %}
|
||||
$('#ioc_type_id').selectpicker('val', '{{ioc.ioc_type_id}}');
|
||||
$('#ioc_tlp_id').selectpicker('val', '{{ioc.ioc_tlp_id}}');
|
||||
{% else %}
|
||||
$('#ioc_tlp_id').selectpicker('val', '2');
|
||||
{% endif %}
|
||||
</script>
|
@ -0,0 +1,162 @@
|
||||
<div class="modal-header">
|
||||
<div class="col md-12">
|
||||
<div class="row">
|
||||
<div class="col align-self-center">
|
||||
<h4 class="modal-title mr-4">Add multiple assets</h4>
|
||||
<small><a class="text-muted"></a></small>
|
||||
</div>
|
||||
{% include 'modals/modal_attributes_nav.html' %}
|
||||
<div class="col">
|
||||
<div class="row float-right">
|
||||
<button class="float-right btn bg-transparent" title="Minimize" onclick="modal_minimized('modal_add_asset', 'Add asset');"> <i class='fa fa-minus'></i> </button>
|
||||
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true"><i class="fa fa-times"></i></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div role="tabpanel">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="details">
|
||||
<div class="container col-md-12">
|
||||
<form method="post" action="" id="form_new_assets">
|
||||
<div class="col-md-12 col-lg-12 col-sm-12">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="form-row ml-2">
|
||||
<div class="form-group col-12">
|
||||
<label for="asset_type" class="placeholder">Assets Type *</label>
|
||||
{{ form.asset_type_id(class="selectpicker form-control") }}
|
||||
</div>
|
||||
<div class="form-group col-12">
|
||||
<label for="name" class="placeholder">Assets Name *</label>
|
||||
<textarea class="form-control sizable-textarea" autocomplete="off" rows="1" name="assets_name" id="assets_name" placeholder="One asset per line"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mt-3">
|
||||
<label for="asset_description" class="placeholder">Description</label>
|
||||
<div class="md_description_field">
|
||||
<div class="form-group mt--2">
|
||||
<button type="button" class="float-right icon-note btn btn-circle btn-sm mt-2" onclick="edit_in_asset_desc();">
|
||||
</button>
|
||||
<button type="button" style="display: none;" class="btn btn-dark btn-sm float-right mr-2 mt-2" onclick="preview_asset_description();" id="asset_preview_button"><i class="fa-solid fa-eye"></i></button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col mb--2 ml--2" id="asset_edition_btn" style="display:none;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-left:0px;">
|
||||
<div class="col-12" id="container_asset_desc_content">
|
||||
<div id="asset_description" class="mr-2" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}">{% if asset %}{{ asset.asset_description }}{% endif %}</div>
|
||||
<textarea id="asset_desc_content" rows="10" cols="82" style="display: none"></textarea>
|
||||
</div>
|
||||
<div class="col-12" id="container_asset_description" style="display:none">
|
||||
<div id="target_asset_desc"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row ml-2">
|
||||
<div class="form-group col-6">
|
||||
<label for="asset_domain" class="placeholder">Domain</label>
|
||||
{{ form.asset_domain(class='form-control', autocomplete="off") }}
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="asset_ip" class="placeholder">IP</label>
|
||||
{{ form.asset_ip(class='form-control', autocomplete="off") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<a class="btn btn-light btn-sm" data-toggle="collapse" href="#collapseAddInfo" role="button" aria-expanded="false" aria-controls="collapseAddInfo">> Additional information</a>
|
||||
<div class="collapse" id="collapseAddInfo">
|
||||
<div class="card card-body">
|
||||
<label for="asset_info" class="placeholder">Additional information</label>
|
||||
{{ form.asset_info(class='form-control col-md-12 col-sm-12 sizable-textarea', autocomplete="off") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row ml-2">
|
||||
<div class="form-group col-6">
|
||||
<label for="asset_compromise_status_id" class="placeholder mt-2">Compromise Status </label>
|
||||
{{ form.asset_compromise_status_id(class="selectpicker col-9") }}
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="analysis_status_id" class="placeholder mt-2">Analysis Status </label>
|
||||
{{ form.analysis_status_id(class="selectpicker col-9 float-right") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="asset_tags">Asset tags
|
||||
</label>
|
||||
<input type="text" id="asset_tags"
|
||||
class="form-control col-md-12"/>
|
||||
</div>
|
||||
<div class="form-group" data-select2-id="7">
|
||||
<label>Related IOC</label>
|
||||
<div class="select2-input ml-12" data-select2-id="6">
|
||||
<select id="ioc_links" name="ioc_links" class="form-control select2-hidden-accessible ml-12" multiple="" data-select2-id="ioc_links" tabindex="-1" aria-hidden="true" style="width: 100%">
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% include 'modals/modal_attributes_tabs.html' %}
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right" id="submit_new_assets">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$('form#form_new_case').validate();
|
||||
$('#asset_tags').amsifySuggestags({
|
||||
printValues: false,
|
||||
suggestions: [ {% for tag in tags %} '{{ tag }}', {% endfor %} ]
|
||||
});
|
||||
$('#asset_type_id').selectpicker({
|
||||
liveSearch: true,
|
||||
title: "None",
|
||||
style: "btn-outline-white",
|
||||
|
||||
});
|
||||
$('#analysis_status_id').selectpicker({
|
||||
liveSearch: true,
|
||||
title: "None",
|
||||
style: "btn-outline-white"
|
||||
});
|
||||
$('#analysis_status_id').selectpicker('val', '1');
|
||||
|
||||
$('#asset_compromise_status_id').selectpicker({
|
||||
liveSearch: true,
|
||||
title: "To be determined",
|
||||
style: "btn-outline-white"
|
||||
});
|
||||
$('#asset_compromise_status_id').selectpicker('val', '0');
|
||||
</script>
|
||||
|
||||
|
||||
{% if ioc %}
|
||||
<script>
|
||||
var data = [
|
||||
{% for e in ioc %}
|
||||
{
|
||||
id: {{ e.ioc_id }},
|
||||
text: {{ e.ioc_value| tojson }}
|
||||
},
|
||||
{% endfor %}
|
||||
];
|
||||
$('#ioc_links').select2({ data: data });
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if ioc_prefill %}
|
||||
<script>
|
||||
$('#ioc_links').val([
|
||||
{% for ioc in ioc_prefill %} {{ ioc[0] }}, {% endfor %}
|
||||
]);
|
||||
$('#ioc_links').trigger('change');
|
||||
</script>
|
||||
{% endif %}
|
@ -0,0 +1,98 @@
|
||||
<div class="modal-header">
|
||||
<div class="col md-12">
|
||||
<div class="row">
|
||||
<div class="col align-self-center">
|
||||
<h4 class="modal-title mr-4">{% if rfile.id %}Edit evidence #{{rfile.id}}{% else %}Register evidence{% endif %}</h4>
|
||||
{% if rfile.id %}<small><i class="text-muted">#{{ rfile.file_uuid }}</i></small>{% endif %}
|
||||
</div>
|
||||
{% include 'modals/modal_attributes_nav.html' %}
|
||||
<div class="col">
|
||||
<div class="row float-right">
|
||||
{% if rfile.id %}
|
||||
<div class="dropdown">
|
||||
<button class="btn bg-transparent pull-right" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
<span aria-hidden="true"><i class="fas fa-ellipsis-v"></i></span>
|
||||
</button>
|
||||
<div class="dropdown-menu pull-right" id="evidence_modal_quick_actions" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item" href="#" onclick='copy_object_link({{rfile.id}});return false;'><i class="fa fa-share mr-2"></i>Share</a>
|
||||
<a class="dropdown-item" href="#" onclick='copy_object_link_md("evidence", {{rfile.id}});return false;'><i class="fa-brands fa-markdown mr-2"></i>Markdown Link</a>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn bg-transparent btn-xs" onclick="comment_element({{ rfile.id }}, 'evidences')" title="Comments">
|
||||
<span class="btn-label">
|
||||
<i class="fa-solid fa-comments"></i><span class="notification" id="object_comments_number">{{ comments_map|length }}</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<button class="float-right btn bg-transparent" title="Minimize" onclick="modal_minimized('modal_add_rfiles', 'Edit evidence #{{rfile.id}}');"> <i class='fa fa-minus'></i> </button>
|
||||
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true"><i class="fa fa-times"></i></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div role="tabpanel">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="details">
|
||||
<div class="container col-md-12">
|
||||
<form method="post" action="" id="form_edit_rfile">
|
||||
<div class="col-md-12 col-lg-12 col-sm-12">
|
||||
<div class="form-group">
|
||||
<label for="rfile_filename" class="placeholder">Filename *</label>
|
||||
<input class="form-control" placeholder="Filename" id="filename" required name="filename" value="{{ rfile.filename }}"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="rfile_size" class="placeholder">File size (bytes) *</label>
|
||||
<input class="form-control" placeholder="Size in bytes" id="file_size" name="file_size" value="{{ rfile.file_size }}"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="rfile_hash" class="placeholder">File Hash</label>
|
||||
<input class="form-control" placeholder="Hash" id="file_hash" name="file_hash" value="{{ rfile.file_hash }}"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="rfile_desc" class="placeholder">File description</label>
|
||||
<div class="md_description_field">
|
||||
<div class="form-group mt--2">
|
||||
<button type="button" class="float-right icon-note btn btn-circle btn-sm mt-2" onclick="edit_in_evidence_desc();" >
|
||||
</button>
|
||||
<button type="button" style="display: none;" class="btn btn-dark btn-sm float-right mr-2 mt-2"
|
||||
onclick="preview_evidence_description();" id="evidence_preview_button"><i class="fa-solid fa-eye"></i></button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col mb--2 ml--2" id="evidence_edition_btn" style="display:none;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-left:0px;">
|
||||
<div class="col-12" id="container_evidence_desc_content">
|
||||
<div id="evidence_description" class="mr-2" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}">{% if rfile %}{{ rfile.file_description }}{% endif %}</div>
|
||||
<textarea id="evidence_desc_content" rows="10" cols="82" style="display: none"></textarea>
|
||||
</div>
|
||||
<div class="col-12" id="container_evidence_description" style="display:none">
|
||||
<div id="target_evidence_desc"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<p>Automatically compute file information by selecting it below. The file will not be uploaded nor saved.</p>
|
||||
<input id="input_autofill" type="file">
|
||||
<button class="btn btn-sm" type="button" onclick="get_hash()" id="btn_rfile_proc">Process</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'modals/modal_attributes_tabs.html' %}
|
||||
</div>
|
||||
{% if rfile.id %}
|
||||
<button type="button" class="btn btn-outline-danger mt-5"
|
||||
onclick="delete_rfile('{{ rfile.id }}');">Delete</button>
|
||||
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
|
||||
onclick="update_rfile('{{ rfile.id }}');" id="submit_new_rfiles">Update</button>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-outline-success float-right" onclick="add_rfile();">Register</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,139 @@
|
||||
<div class="modal-header">
|
||||
<div class="col md-12">
|
||||
<div class="row">
|
||||
<div class="col align-self-center">
|
||||
<h4 class="modal-title mr-4">{% if task.id %} Task ID #{{ task.id }}{% else %} Add task {% endif %}</h4>
|
||||
<small><i class="text-muted">#{{ task.task_uuid }}</i></small>
|
||||
</div>
|
||||
{% include 'modals/modal_attributes_nav.html' %}
|
||||
<div class="col">
|
||||
<div class="row float-right">
|
||||
{% if task.id %}
|
||||
<div class="dropdown">
|
||||
<button class="btn bg-transparent pull-right" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
<span aria-hidden="true"><i class="fas fa-ellipsis-v"></i></span>
|
||||
</button>
|
||||
<div class="dropdown-menu pull-right" id="task_modal_quick_actions" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item" href="#" onclick='copy_object_link({{task.id}});return false;'><i class="fa fa-share mr-2"></i>Share</a>
|
||||
<a class="dropdown-item" href="#" onclick='copy_object_link_md("task", {{task.id}});return false;'><i class="fa-brands fa-markdown mr-2"></i>Markdown Link</a>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn bg-transparent btn-xs" onclick="comment_element({{ task.id }}, 'tasks')" title="Comments">
|
||||
<span class="btn-label">
|
||||
<i class="fa-solid fa-comments"></i><span class="notification" id="object_comments_number">{{ comments_map|length }}</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<button class="float-right btn bg-transparent" title="Minimize" onclick="modal_minimized('modal_add_task', '{% if task.id %} Task ID #{{ task.id }}{% else %} Add task {% endif %}');"> <i class='fa fa-minus'></i> </button>
|
||||
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true"><i class="fa fa-times"></i></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div role="tabpanel">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="details">
|
||||
<div class="container col-md-12">
|
||||
<form method="post" action="" id="form_new_task">
|
||||
<div class="col-md-12 col-lg-12 col-sm-12">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="form-group mt-3 row">
|
||||
<div class="col-6 col-xs-12">
|
||||
<label for="task_assignee_id" class="placeholder">Assigned to *</label>
|
||||
{{ form.task_assignees_id(class="selectpicker col-12", data_actions_box="true", data_dropup_auto="false") }}
|
||||
|
||||
</div>
|
||||
<div class="col-6 col-xs-12">
|
||||
<label for="task_status_id" class="placeholder">Status *</label>
|
||||
{{ form.task_status_id(class="selectpicker col-12") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="task_title" class="placeholder">{{ form.task_title.label.text }} *</label>
|
||||
{{ form.task_title(class='form-control col-md-12 col-sm-12', autocomplete="off") }}
|
||||
</div>
|
||||
<div class="form-group mt-3">
|
||||
<label for="task_description" class="placeholder">Description</label>
|
||||
<div class="md_description_field">
|
||||
<div class="form-group mt--2">
|
||||
<button type="button" class="float-right icon-note btn btn-circle btn-sm mt-2" onclick="edit_in_task_desc();">
|
||||
</button>
|
||||
<button type="button" style="display: none;" class="btn btn-dark btn-sm float-right mr-2 mt-2"
|
||||
onclick="preview_task_description();" id="task_preview_button"><i class="fa-solid fa-eye"></i></button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col mb--2 ml--2" id="task_edition_btn" style="display:none;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-left:0px;">
|
||||
<div class="col-12" id="container_task_desc_content">
|
||||
<div id="task_description" class="mr-2" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}">{% if task.task_description %}{{ task.task_description }}{% endif %}</div>
|
||||
<textarea id="task_desc_content" rows="10" cols="82" style="display: none"></textarea>
|
||||
</div>
|
||||
<div class="col-12" id="container_task_description" style="display:none">
|
||||
<div id="target_task_desc"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mt-3">
|
||||
<label for="task_tags">Task tags
|
||||
</label>
|
||||
<input type="text" id="task_tags"
|
||||
class="form-control col-md-12" {% if task.task_tags %} value="{{ task.task_tags }}" {% endif %}/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'modals/modal_attributes_tabs.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% if task.id %}
|
||||
<button type="button" class="btn btn-outline-danger mt-5"
|
||||
onclick="delete_task({{ task.id }});">Delete</button>
|
||||
|
||||
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right" id="submit_new_task"
|
||||
onclick="update_task({{ task.id }});">Update</button>
|
||||
|
||||
{% else %}
|
||||
|
||||
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
|
||||
id="submit_new_task">Save</button>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
var data = [];
|
||||
if (current_users_list.length === 0) {
|
||||
refresh_users(do_list_users, [{% for assignee in task.task_assignees %} {{ assignee.id }}, {% endfor %}]);
|
||||
} else {
|
||||
do_list_users(current_users_list, [{% for assignee in task.task_assignees %} {{ assignee.id }}, {% endfor %}]);
|
||||
}
|
||||
|
||||
$('form#form_new_task').validate();
|
||||
|
||||
$('#task_tags').amsifySuggestags({
|
||||
printValues: false,
|
||||
suggestions: [ {% for tag in tags %} '{{ tag }}', {% endfor %} ]
|
||||
});
|
||||
|
||||
$('#task_status_id').selectpicker({
|
||||
liveSearch: true,
|
||||
title: "Select task status"
|
||||
});
|
||||
|
||||
{% if task.task_status_id %}
|
||||
$('#task_status_id').selectpicker('val', '{{task.task_status_id}}');
|
||||
{% else %}
|
||||
$('#task_status_id').selectpicker('val', 'To do');
|
||||
{% endif %}
|
||||
$('[data-toggle="popover"]').popover();
|
||||
</script>
|
||||
|
@ -0,0 +1,33 @@
|
||||
|
||||
<div class="modal-content">
|
||||
<form method="post" action="" id="form_add_asset">
|
||||
<div class="modal-header">
|
||||
<h5>Add asset to graph</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="asset" class="mr-4">Add an existing asset to the case graph.<br>If you want to create a new asset, please go to the Asset tab.
|
||||
</label>
|
||||
|
||||
<select class="selectpicker form-control bg-outline-success dropdown-submenu" data-show-subtext="true" data-live-search="true" id="asset">
|
||||
{% for asset in assets_list %}
|
||||
<option value="{{ asset.IP }}">{{ asset.Name }} </option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-success float-right" id="submit_add_asset">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div><!-- /.modal-content -->
|
||||
<script>
|
||||
$('#asset').selectpicker({
|
||||
liveSearch: true,
|
||||
title: "None",
|
||||
style: "Bootstrap 4: 'btn-outline-primary'",
|
||||
|
||||
});
|
||||
</script>
|
@ -0,0 +1,54 @@
|
||||
<link rel="stylesheet" href="/static/assets/css/dropzone.css">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title mt-2 mr-4">Processing pipelines</h4>
|
||||
<button type="button" class="pull-right btn btn-white" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true"><i class="fa fa-times"></i></span></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container col-md-12">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p>Select processing pipeline and drop analysis files below. Press Process to start the processing. Do not close the page until the upload is finished.<br/>
|
||||
Supports up to 40 files and 10GB at once.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group">
|
||||
<label for="import_pipeline" class="placeholder">Processing pipeline</label>
|
||||
<i class="fas fa-question-circle mr-2" data-toggle="popover"
|
||||
title="Pipelines"
|
||||
data-content="Pipelines are the way files dropped below are processed. Each pipelines handles a different type of file."></i>
|
||||
|
||||
{{ form.pipeline(class="selectpicker pl--6 btn-outline-white", id="update_pipeline_selector") }}
|
||||
</div>
|
||||
<div class="form-group col-md-6 mb-2">
|
||||
{% for itm in pipeline_args %}
|
||||
{% for tm in itm[2] %}
|
||||
<div class="input-group mb-4 control-update-pipeline-args control-update-pipeline-{{itm[0]}}">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">{{tm[0]}} ({{tm[1]}})</span>
|
||||
</div>
|
||||
<input class="form-control update-{{itm[0]}}" id="{{tm[0]}}" name="{{tm[0]}}" type="text" value="" {{tm[1]}}>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="tab-content col-md-12">
|
||||
<div class="dropzone col-md-12" id="files_drop_1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span id="msg_submit" class="ml-4"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default float-left" data-dismiss="modal">Dismiss</button>
|
||||
|
||||
<button type="button" class="btn btn-outline-success float-right"
|
||||
id="submit_update_case" onclick="submit_update_casefn();">Process</button>
|
||||
</div>
|
||||
<script src="/static/assets/js/plugin/dropzone/dropzone.js"></script>
|
||||
<script src="/static/assets/js/iris/case.pipelines.js"></script>
|
@ -0,0 +1,52 @@
|
||||
|
||||
<div class="modal-header">
|
||||
<div style="display:none;" id="current_username">{{current_user.user}}</div>
|
||||
<div class="col md-12">
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<h4 class="modal-title mt-2 mr-4 text-truncate">Comments on <i>{{ title }}</i></h4>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="row float-right">
|
||||
<button class="btn bg-transparent" title="Minimize" onclick="modal_minimized('modal_comment', 'Comment {{ element_type }}');"> <i class='fa fa-minus'></i> </button>
|
||||
<button type="button" class="float-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true"><i class="fa fa-times"></i></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body comments-listing">
|
||||
<div id="comments_list">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="row">
|
||||
<div class="col mb--2 ml--2" id="comment_edition_btn">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 comment-content" id="container_comment_content">
|
||||
<div id="comment_message" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}"></div>
|
||||
<textarea id="comment_content" rows="2" style="display: none"></textarea>
|
||||
</div>
|
||||
<div class="col-12 comment-content" id="container_comment_preview" style="display:none;">
|
||||
<div id="target_comment_content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12" id="container_comment_submit">
|
||||
<button type="button" class="btn btn-primary btn-sm float-right ml-2" id="comment_submit" onclick="save_comment({{ element_id }}, '{{ element_type }}');"><i class="fa-regular fa-paper-plane"></i> Comment</button>
|
||||
<button type="button" class="btn btn-primary btn-sm float-right ml-2" id="comment_edition" style="display: none;" onclick="save_edit_comment({{ element_id }}, '{{ element_type }}');"><i class="fa-regular fa-paper-plane"></i> Save</button>
|
||||
<button type="button" class="btn btn-danger btn-sm float-right ml-2" id="cancel_edition" style="display: none;" onclick="cancel_edition();"><i class="fa-solid fa-xmark"></i> Cancel</button>
|
||||
<button type="button" class="btn btn-dark btn-sm float-right ml-2" onclick="preview_comment();" id="comment_preview_button"><i class="fa-solid fa-eye"></i> Preview</button>
|
||||
<button type="button" class="btn btn-light btn-sm float-left" onclick="load_comments({{ element_id }}, '{{ element_type }}', null, true);"><i class="fa-solid fa-refresh"></i> Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,33 @@
|
||||
<div class="modal-xl modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title mt-1 mr-4">Timeline filtering help</h4>
|
||||
<button type="button" class="float-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true"><i class="fa fa-times"></i></span></button>
|
||||
</div>
|
||||
<div class="modal-body mb-2">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p>The timeline can be filtered thanks to simple queries. The query schema is : <code>target_element:search_value AND target_element2:search_value2</code>.<br/>
|
||||
There is no <code>OR</code> condition and searching without target does not work.
|
||||
<p>The following target elements can be used to filter :</p>
|
||||
<ul>
|
||||
<li><code>asset</code>: Asset linked to the event</li>
|
||||
<li><code>ioc</code>: IOC linked to the event</li>
|
||||
<li><code>tag</code>: Tag within the event</li>
|
||||
<li><code>title</code>: Title of the event</li>
|
||||
<li><code>description</code>: Description of the event</li>
|
||||
<li><code>raw</code> : Raw event content</li>
|
||||
<li><code>category</code>: Category of the event</li>
|
||||
<li><code>source</code>: Source of the event</li>
|
||||
<li><code>startDate</code>: Start date to filter with</li>
|
||||
<li><code>endDate</code>: End date to filter with</li>
|
||||
</ul>
|
||||
The dates filters uses the same guessing as the date parser in events, so a lots of format are handled.<br/>
|
||||
Example of filter :
|
||||
<code>asset: DESKTOP-X5487 AND description: rdp connection to AND source: Windows Security</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,98 @@
|
||||
<div class="modal-header">
|
||||
<div class="col md-12">
|
||||
<div class="row">
|
||||
<div class="col align-self-center">
|
||||
<h4 class="modal-title mr-4">Note #{{ note.note_id }}</h4>
|
||||
<small><i class="text-muted">#{{ note.note_uuid }}</i></small>
|
||||
</div>
|
||||
{% include 'modals/modal_attributes_nav.html' %}
|
||||
<div class="col">
|
||||
<div class="row float-right">
|
||||
<div class="avatar-group-note mt-2 float-right" id="ppl_list_viewing">
|
||||
</div>
|
||||
<button class="btn bg-transparent pull-right" title="Toggle focus mode" id="focus_mode" onclick="toggle_focus_mode();return false;">
|
||||
<span aria-hidden="true"><i class="fas fas fa-coffee"></i></span>
|
||||
</button>
|
||||
<button type="button" class="btn bg-transparent btn-xs" onclick="comment_element({{ note.note_id }}, 'notes')" title="Comments">
|
||||
<span class="btn-label">
|
||||
<i class="fa-solid fa-comments"></i><span class="notification" id="object_comments_number">{{ comments_map|length }}</span>
|
||||
</span>
|
||||
</button>
|
||||
<div class="dropdown">
|
||||
<button class="btn bg-transparent pull-right" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
<span aria-hidden="true"><i class="fas fa-ellipsis-v"></i></span>
|
||||
</button>
|
||||
<div class="dropdown-menu pull-right" id="note_modal_quick_actions" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item" href="#" onclick='copy_object_link({{ note.note_id }});return false;'><i class="fa fa-share mr-2"></i>Share</a>
|
||||
<a class="dropdown-item" href="#" onclick='copy_object_link_md("note",{{ note.note_id }});return false;'><i class="fa-brands fa-markdown mr-2"></i>Markdown Link</a>
|
||||
</div>
|
||||
</div>
|
||||
<button class="float-right btn bg-transparent" title="Minimize" onclick="modal_minimized('modal_note_detail', '{{ note.title }}');"> <i class='fa fa-minus'></i> </button>
|
||||
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true"><i class="fa fa-times"></i></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div role="tabpanel">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="details">
|
||||
<form method="post" action="" id="form_note">
|
||||
<iris_notein style="display: none;">{{ note.note_id }}</iris_notein>
|
||||
|
||||
{{ note.hidden_tag() }}
|
||||
<div class="container col-md-12">
|
||||
<div class="form-group">
|
||||
<label>Note title *</label>
|
||||
{{ note.note_title(class='form-control input') }}
|
||||
</div>
|
||||
<div class="row mb-1 mt-3">
|
||||
<div class="col">
|
||||
<span class="badge badge-light" id="content_typing"></span>
|
||||
<span class="badge badge-light" id="content_last_saved_by"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 mt-3">
|
||||
<div class="col-10" id="notes_edition_btn">
|
||||
<div class="btn btn-sm btn-light mr-1 " title="CTRL-S" id="last_saved" onclick="save_note( this );"><i class="fa-solid fa-file-circle-check"></i></div>
|
||||
<div class="btn btn-sm btn-light mr-1 " title="CTRL-B" onclick="note_editor.insertSnippet('**${1:$SELECTION}**');note_editor.focus();"><i class="fa-solid fa-bold"></i></div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="CTRL-I" onclick="note_editor.insertSnippet('*${1:$SELECTION}*');note_editor.focus();"><i class="fa-solid fa-italic"></i></div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="CTRL-SHIFT-1" onclick="note_editor.insertSnippet('# ${1:$SELECTION}');note_editor.focus();">H1</div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="CTRL-SHIFT-2" onclick="note_editor.insertSnippet('## ${1:$SELECTION}');note_editor.focus();">H2</div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="CTRL-SHIFT-3" onclick="note_editor.insertSnippet('### ${1:$SELECTION}');note_editor.focus();">H3</div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="CTRL-SHIFT-4" onclick="note_editor.insertSnippet('#### ${1:$SELECTION}');note_editor.focus();">H4</div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="CTRL+\`" onclick="note_editor.insertSnippet('```${1:$SELECTION}```');note_editor.focus();"><i class="fa-solid fa-code"></i></div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="CTRL-K" onclick="note_editor.insertSnippet('[${1:$SELECTION}](url)');note_editor.focus();"><i class="fa-solid fa-link"></i></div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="Insert table" onclick="note_editor.insertSnippet('|\t|\t|\t|\n|--|--|--|\n|\t|\t|\t|\n|\t|\t|\t|');note_editor.focus();"><i class="fa-solid fa-table"></i></div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="Insert bullet list" onclick="note_editor.insertSnippet('\n- \n- \n- ');note_editor.focus();"><i class="fa-solid fa-list"></i></div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="Insert numbered list" onclick="note_editor.insertSnippet('\n1. a \n2. b \n3. c ');note_editor.focus();"><i class="fa-solid fa-list-ol"></i></div>
|
||||
<div class="btn btn-sm btn-light mr-1" title="Toggle editor expansion" onclick="toggle_max_editor();"><i class="fa-solid fa-maximize"></i></div>
|
||||
<div class="btn btn-sm btn-transparent mr-1" title="Help" onclick="get_md_helper_modal();"><i class="fa-solid fa-question-circle"></i></div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<button type="button" class="float-right icon-note btn btn-circle btn-sm" onclick="edit_innote();"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6" id="container_note_content">
|
||||
<div style="display: none" id="fetched_crc"></div>
|
||||
<div id="editor_detail" data-theme="{% if current_user.in_dark_mode %}dark{% else %}light{% endif %}">{{ note.content }}</div>
|
||||
<textarea id="note_content" rows="10" cols="82" style="display: none"></textarea>
|
||||
</div>
|
||||
<div class="col-md-6" id="ctrd_notesum">
|
||||
<div id="targetDiv"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% include 'modals/modal_attributes_tabs.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-danger mr-auto" onclick="delete_note(this, {{ ncid }});">Delete note</button>
|
||||
<button type="button" class="btn btn-default" onclick="save_note( this, {{ ncid }} );" id="btn_save_note">Save </button>
|
||||
</div>
|
@ -0,0 +1,24 @@
|
||||
|
||||
{% extends "layouts/default.html" %}
|
||||
|
||||
{% block title %} Case Error {% endblock title %}
|
||||
|
||||
{% block stylesheets %}
|
||||
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h2 class="mx-5">No case found for you !</h2><br/><br/>
|
||||
<h3 class="font-weight-light mx-5">The page you’re looking for is only available when a case is selected.</h3><br/>
|
||||
|
||||
<h3 class="font-weight-light mx-5">Please press the <b><i class="flaticon-repeat"></i></b> button on the top right to select one.</h3>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascripts %}
|
||||
|
||||
{% endblock javascripts %}
|
25
iris-web/source/app/blueprints/context/__init__.py
Normal file
25
iris-web/source/app/blueprints/context/__init__.py
Normal file
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# IRIS Source Code
|
||||
# Copyright (C) 2021 - Airbus CyberSecurity (SAS)
|
||||
# ir@cyberactionlab.net
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# IMPORTS ------------------------------------------------
|
||||
|
||||
# VARS ---------------------------------------------------
|
||||
|
||||
# CONTENT ------------------------------------------------
|
145
iris-web/source/app/blueprints/context/context.py
Normal file
145
iris-web/source/app/blueprints/context/context.py
Normal file
@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# IRIS Source Code
|
||||
# Copyright (C) 2021 - Airbus CyberSecurity (SAS)
|
||||
# ir@cyberactionlab.net
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from flask import Blueprint
|
||||
# IMPORTS ------------------------------------------------
|
||||
from flask import redirect
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
|
||||
from app import app
|
||||
from app import cache
|
||||
from app import db
|
||||
from app.datamgmt.context.context_db import ctx_get_user_cases
|
||||
from app.datamgmt.context.context_db import ctx_search_user_cases
|
||||
from app.models.authorization import Permissions
|
||||
from app.models.cases import Cases
|
||||
from app.models.models import Client
|
||||
from app.util import ac_api_requires
|
||||
from app.util import not_authenticated_redirection_url
|
||||
from app.util import response_success
|
||||
|
||||
# CONTENT ------------------------------------------------
|
||||
ctx_blueprint = Blueprint(
|
||||
'context',
|
||||
__name__,
|
||||
template_folder='templates'
|
||||
)
|
||||
|
||||
|
||||
@ctx_blueprint.route('/context/set', methods=['POST'])
|
||||
def set_ctx():
|
||||
"""
|
||||
Set the context elements of a user i.e the current case
|
||||
:return: Page
|
||||
"""
|
||||
if not current_user.is_authenticated:
|
||||
return redirect(not_authenticated_redirection_url())
|
||||
|
||||
ctx = request.form.get('ctx')
|
||||
ctx_h = request.form.get('ctx_h')
|
||||
|
||||
current_user.ctx_case = ctx
|
||||
current_user.ctx_human_case = ctx_h
|
||||
|
||||
db.session.commit()
|
||||
|
||||
update_user_case_ctx()
|
||||
|
||||
return response_success(msg="Saved")
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def iris_version():
|
||||
return dict(iris_version=app.config.get('IRIS_VERSION'),
|
||||
organisation_name=app.config.get('ORGANISATION_NAME'),
|
||||
std_permissions=Permissions,
|
||||
demo_domain=app.config.get('DEMO_DOMAIN', None))
|
||||
|
||||
|
||||
@app.context_processor
|
||||
@cache.cached(timeout=3600, key_prefix='iris_has_updates')
|
||||
def has_updates():
|
||||
|
||||
return dict(has_updates=False)
|
||||
|
||||
|
||||
@ctx_blueprint.route('/context/get-cases/<int:max_results>', methods=['GET'])
|
||||
@ac_api_requires(no_cid_required=True)
|
||||
def cases_context(max_results, caseid):
|
||||
# Get all investigations not closed
|
||||
datao = ctx_get_user_cases(current_user.id, max_results=max_results)
|
||||
|
||||
return response_success(data=datao)
|
||||
|
||||
|
||||
@ctx_blueprint.route('/context/search-cases', methods=['GET'])
|
||||
@ac_api_requires(no_cid_required=True)
|
||||
def cases_context_search(caseid):
|
||||
search = request.args.get('q')
|
||||
if not search:
|
||||
return response_success(data=[])
|
||||
|
||||
# Get all investigations not closed
|
||||
datao = ctx_search_user_cases(search, current_user.id, max_results=100)
|
||||
|
||||
return response_success(data=datao)
|
||||
|
||||
|
||||
def update_user_case_ctx():
|
||||
"""
|
||||
Retrieve a list of cases for the case selector
|
||||
:return:
|
||||
"""
|
||||
# Get all investigations not closed
|
||||
res = Cases.query.with_entities(
|
||||
Cases.name,
|
||||
Client.name,
|
||||
Cases.case_id,
|
||||
Cases.close_date) \
|
||||
.join(Cases.client) \
|
||||
.order_by(Cases.open_date) \
|
||||
.all()
|
||||
|
||||
data = [row for row in res]
|
||||
|
||||
if current_user and current_user.ctx_case:
|
||||
# If the current user have a current case,
|
||||
# Look for it in the fresh list. If not
|
||||
# exists then remove from the user context
|
||||
is_found = False
|
||||
for row in data:
|
||||
if row[2] == current_user.ctx_case:
|
||||
is_found = True
|
||||
break
|
||||
|
||||
if not is_found:
|
||||
# The case does not exist,
|
||||
# Removes it from the context
|
||||
current_user.ctx_case = None
|
||||
current_user.ctx_human_case = "Not set"
|
||||
db.session.commit()
|
||||
# current_user.save()
|
||||
|
||||
app.jinja_env.globals.update({
|
||||
'cases_context_selector': data
|
||||
})
|
||||
|
||||
return data
|
381
iris-web/source/app/blueprints/dashboard/dashboard_routes.py
Normal file
381
iris-web/source/app/blueprints/dashboard/dashboard_routes.py
Normal file
@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# IRIS Source Code
|
||||
# Copyright (C) 2021 - Airbus CyberSecurity (SAS)
|
||||
# ir@cyberactionlab.net
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import marshmallow
|
||||
# IMPORTS ------------------------------------------------
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from flask import Blueprint
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
from flask import request
|
||||
from flask import session
|
||||
from flask import url_for
|
||||
from flask_login import current_user
|
||||
from flask_login import logout_user
|
||||
from flask_wtf import FlaskForm
|
||||
from sqlalchemy import distinct
|
||||
|
||||
from app import app
|
||||
from app import db
|
||||
from app.datamgmt.dashboard.dashboard_db import get_global_task, list_user_cases, list_user_reviews
|
||||
from app.datamgmt.dashboard.dashboard_db import get_tasks_status
|
||||
from app.datamgmt.dashboard.dashboard_db import list_global_tasks
|
||||
from app.datamgmt.dashboard.dashboard_db import list_user_tasks
|
||||
from app.forms import CaseGlobalTaskForm
|
||||
from app.iris_engine.module_handler.module_handler import call_modules_hook
|
||||
from app.iris_engine.utils.tracker import track_activity
|
||||
from app.models.authorization import User
|
||||
from app.models.cases import Cases
|
||||
from app.models.models import CaseTasks
|
||||
from app.models.models import GlobalTasks
|
||||
from app.models.models import TaskStatus
|
||||
from app.models.models import UserActivity
|
||||
from app.schema.marshables import CaseTaskSchema, CaseSchema, CaseDetailsSchema
|
||||
from app.schema.marshables import GlobalTasksSchema
|
||||
from app.util import ac_api_requires
|
||||
from app.util import ac_requires
|
||||
from app.util import not_authenticated_redirection_url
|
||||
from app.util import response_error
|
||||
from app.util import response_success
|
||||
|
||||
# CONTENT ------------------------------------------------
|
||||
dashboard_blueprint = Blueprint(
|
||||
'index',
|
||||
__name__,
|
||||
template_folder='templates'
|
||||
)
|
||||
|
||||
|
||||
# Logout user
|
||||
@dashboard_blueprint.route('/logout')
|
||||
def logout():
|
||||
"""
|
||||
Logout function. Erase its session and redirect to index i.e login
|
||||
:return: Page
|
||||
"""
|
||||
if session['current_case']:
|
||||
current_user.ctx_case = session['current_case']['case_id']
|
||||
current_user.ctx_human_case = session['current_case']['case_name']
|
||||
db.session.commit()
|
||||
|
||||
track_activity("user '{}' has been logged-out".format(current_user.user), ctx_less=True, display_in_ui=False)
|
||||
logout_user()
|
||||
|
||||
return redirect(not_authenticated_redirection_url(request_url='/'))
|
||||
|
||||
|
||||
@dashboard_blueprint.route('/dashboard/case_charts', methods=['GET'])
|
||||
@ac_api_requires()
|
||||
def get_cases_charts(caseid):
|
||||
"""
|
||||
Get case charts
|
||||
:return: JSON
|
||||
"""
|
||||
|
||||
res = Cases.query.with_entities(
|
||||
Cases.open_date
|
||||
).filter(
|
||||
Cases.open_date > (datetime.utcnow() - timedelta(days=365))
|
||||
).order_by(
|
||||
Cases.open_date
|
||||
).all()
|
||||
retr = [[], []]
|
||||
rk = {}
|
||||
for case in res:
|
||||
month = "{}/{}/{}".format(case.open_date.day, case.open_date.month, case.open_date.year)
|
||||
|
||||
if month in rk:
|
||||
rk[month] += 1
|
||||
else:
|
||||
rk[month] = 1
|
||||
|
||||
retr = [list(rk.keys()), list(rk.values())]
|
||||
|
||||
return response_success("", retr)
|
||||
|
||||
|
||||
@dashboard_blueprint.route('/')
|
||||
def root():
|
||||
if app.config['DEMO_MODE_ENABLED'] == 'True':
|
||||
return redirect(url_for('demo-landing.demo_landing'))
|
||||
|
||||
return redirect(url_for('index.index'))
|
||||
|
||||
|
||||
@dashboard_blueprint.route('/dashboard')
|
||||
@ac_requires()
|
||||
def index(caseid, url_redir):
|
||||
"""
|
||||
Index page. Load the dashboard data, create the add customer form
|
||||
:return: Page
|
||||
"""
|
||||
if url_redir:
|
||||
return redirect(url_for('index.index', cid=caseid if caseid is not None else 1, redirect=True))
|
||||
|
||||
msg = None
|
||||
|
||||
# Retrieve the dashboard data from multiple sources.
|
||||
# Quite fast as it is only counts.
|
||||
user_open_case = Cases.query.filter(
|
||||
Cases.owner_id == current_user.id,
|
||||
Cases.close_date == None
|
||||
).count()
|
||||
|
||||
data = {
|
||||
"user_open_count": user_open_case,
|
||||
"cases_open_count": Cases.query.filter(Cases.close_date == None).count(),
|
||||
"cases_count": Cases.query.with_entities(distinct(Cases.case_id)).count(),
|
||||
}
|
||||
|
||||
# Create the customer form to be able to quickly add a customer
|
||||
form = FlaskForm()
|
||||
|
||||
return render_template('index.html', data=data, form=form, msg=msg)
|
||||
|
||||
|
||||
@dashboard_blueprint.route('/global/tasks/list', methods=['GET'])
|
||||
@ac_api_requires()
|
||||
def get_gtasks(caseid):
|
||||
|
||||
tasks_list = list_global_tasks()
|
||||
|
||||
if tasks_list:
|
||||
output = [c._asdict() for c in tasks_list]
|
||||
else:
|
||||
output = []
|
||||
|
||||
ret = {
|
||||
"tasks_status": get_tasks_status(),
|
||||
"tasks": output
|
||||
}
|
||||
|
||||
return response_success("", data=ret)
|
||||
|
||||
|
||||
@dashboard_blueprint.route('/user/cases/list', methods=['GET'])
|
||||
@ac_api_requires()
|
||||
def list_own_cases(caseid):
|
||||
|
||||
cases = list_user_cases(
|
||||
request.args.get('show_closed', 'false', type=str).lower() == 'true'
|
||||
)
|
||||
|
||||
return response_success("", data=CaseDetailsSchema(many=True).dump(cases))
|
||||
|
||||
|
||||
@dashboard_blueprint.route('/global/tasks/<int:cur_id>', methods=['GET'])
|
||||
@ac_api_requires()
|
||||
def view_gtask(cur_id, caseid):
|
||||
|
||||
task = get_global_task(task_id=cur_id)
|
||||
if not task:
|
||||
return response_error(f'Global task ID {cur_id} not found')
|
||||
|
||||
return response_success("", data=task._asdict())
|
||||
|
||||
|
||||
@dashboard_blueprint.route('/user/tasks/list', methods=['GET'])
|
||||
@ac_api_requires()
|
||||
def get_utasks(caseid):
|
||||
|
||||
ct = list_user_tasks()
|
||||
|
||||
if ct:
|
||||
output = [c._asdict() for c in ct]
|
||||
else:
|
||||
output = []
|
||||
|
||||
ret = {
|
||||
"tasks_status": get_tasks_status(),
|
||||
"tasks": output
|
||||
}
|
||||
|
||||
return response_success("", data=ret)
|
||||
|
||||
|
||||
@dashboard_blueprint.route('/user/reviews/list', methods=['GET'])
|
||||
@ac_api_requires()
|
||||
def get_reviews(caseid):
|
||||
|
||||
ct = list_user_reviews()
|
||||
|
||||
if ct:
|
||||
output = [c._asdict() for c in ct]
|
||||
else:
|
||||
output = []
|
||||
|
||||
|
||||
return response_success("", data=output)
|
||||
|
||||
|
||||
@dashboard_blueprint.route('/user/tasks/status/update', methods=['POST'])
|
||||
@ac_api_requires()
|
||||
def utask_statusupdate(caseid):
|
||||
jsdata = request.get_json()
|
||||
if not jsdata:
|
||||
return response_error("Invalid request")
|
||||
|
||||
jsdata = request.get_json()
|
||||
if not jsdata:
|
||||
return response_error("Invalid request")
|
||||
|
||||
case_id = jsdata.get('case_id') if jsdata.get('case_id') else caseid
|
||||
task_id = jsdata.get('task_id')
|
||||
task = CaseTasks.query.filter(CaseTasks.id == task_id, CaseTasks.task_case_id == case_id).first()
|
||||
if not task:
|
||||
return response_error(f"Invalid case task ID {task_id} for case {case_id}")
|
||||
|
||||
status_id = jsdata.get('task_status_id')
|
||||
status = TaskStatus.query.filter(TaskStatus.id == status_id).first()
|
||||
if not status:
|
||||
return response_error(f"Invalid task status ID {status_id}")
|
||||
|
||||
task.task_status_id = status_id
|
||||
try:
|
||||
|
||||
db.session.commit()
|
||||
|
||||
except Exception as e:
|
||||
return response_error(f"Unable to update task. Error {e}")
|
||||
|
||||
task_schema = CaseTaskSchema()
|
||||
return response_success("Updated", data=task_schema.dump(task))
|
||||
|
||||
|
||||
@dashboard_blueprint.route('/global/tasks/add/modal', methods=['GET'])
|
||||
@ac_api_requires()
|
||||
def add_gtask_modal(caseid):
|
||||
task = GlobalTasks()
|
||||
|
||||
form = CaseGlobalTaskForm()
|
||||
|
||||
form.task_assignee_id.choices = [(user.id, user.name) for user in User.query.filter(User.active == True).order_by(User.name).all()]
|
||||
form.task_status_id.choices = [(a.id, a.status_name) for a in get_tasks_status()]
|
||||
|
||||
return render_template("modal_add_global_task.html", form=form, task=task, uid=current_user.id, user_name=None)
|
||||
|
||||
|
||||
@dashboard_blueprint.route('/global/tasks/add', methods=['POST'])
|
||||
@ac_api_requires()
|
||||
def add_gtask(caseid):
|
||||
|
||||
try:
|
||||
|
||||
gtask_schema = GlobalTasksSchema()
|
||||
|
||||
request_data = call_modules_hook('on_preload_global_task_create', data=request.get_json(), caseid=caseid)
|
||||
|
||||
gtask = gtask_schema.load(request_data)
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages, status=400)
|
||||
|
||||
gtask.task_userid_update = current_user.id
|
||||
gtask.task_open_date = datetime.utcnow()
|
||||
gtask.task_last_update = datetime.utcnow()
|
||||
gtask.task_last_update = datetime.utcnow()
|
||||
|
||||
try:
|
||||
|
||||
db.session.add(gtask)
|
||||
db.session.commit()
|
||||
|
||||
except Exception as e:
|
||||
return response_error(msg="Data error", data=e.__str__(), status=400)
|
||||
|
||||
gtask = call_modules_hook('on_postload_global_task_create', data=gtask, caseid=caseid)
|
||||
track_activity("created new global task \'{}\'".format(gtask.task_title), caseid=caseid)
|
||||
|
||||
return response_success('Task added', data=gtask_schema.dump(gtask))
|
||||
|
||||
|
||||
@dashboard_blueprint.route('/global/tasks/update/<int:cur_id>/modal', methods=['GET'])
|
||||
@ac_api_requires()
|
||||
def edit_gtask_modal(cur_id, caseid):
|
||||
form = CaseGlobalTaskForm()
|
||||
task = GlobalTasks.query.filter(GlobalTasks.id == cur_id).first()
|
||||
form.task_assignee_id.choices = [(user.id, user.name) for user in
|
||||
User.query.filter(User.active == True).order_by(User.name).all()]
|
||||
form.task_status_id.choices = [(a.id, a.status_name) for a in get_tasks_status()]
|
||||
|
||||
# Render the task
|
||||
form.task_title.render_kw = {'value': task.task_title}
|
||||
form.task_description.data = task.task_description
|
||||
user_name, = User.query.with_entities(User.name).filter(User.id == task.task_userid_update).first()
|
||||
|
||||
return render_template("modal_add_global_task.html", form=form, task=task,
|
||||
uid=task.task_assignee_id, user_name=user_name)
|
||||
|
||||
|
||||
@dashboard_blueprint.route('/global/tasks/update/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_requires()
|
||||
def edit_gtask(cur_id, caseid):
|
||||
|
||||
form = CaseGlobalTaskForm()
|
||||
task = GlobalTasks.query.filter(GlobalTasks.id == cur_id).first()
|
||||
form.task_assignee_id.choices = [(user.id, user.name) for user in User.query.filter(User.active == True).order_by(User.name).all()]
|
||||
form.task_status_id.choices = [(a.id, a.status_name) for a in get_tasks_status()]
|
||||
|
||||
if not task:
|
||||
return response_error(msg="Data error", data="Invalid task ID", status=400)
|
||||
|
||||
try:
|
||||
gtask_schema = GlobalTasksSchema()
|
||||
|
||||
request_data = call_modules_hook('on_preload_global_task_update', data=request.get_json(),
|
||||
caseid=caseid)
|
||||
|
||||
gtask = gtask_schema.load(request_data, instance=task)
|
||||
gtask.task_userid_update = current_user.id
|
||||
gtask.task_last_update = datetime.utcnow()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
gtask = call_modules_hook('on_postload_global_task_update', data=gtask, caseid=caseid)
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages, status=400)
|
||||
|
||||
track_activity("updated global task {} (status {})".format(task.task_title, task.task_status_id), caseid=caseid)
|
||||
|
||||
return response_success('Task updated', data=gtask_schema.dump(gtask))
|
||||
|
||||
|
||||
@dashboard_blueprint.route('/global/tasks/delete/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_requires()
|
||||
def gtask_delete(cur_id, caseid):
|
||||
|
||||
call_modules_hook('on_preload_global_task_delete', data=cur_id, caseid=caseid)
|
||||
|
||||
if not cur_id:
|
||||
return response_error("Missing parameter")
|
||||
|
||||
data = GlobalTasks.query.filter(GlobalTasks.id == cur_id).first()
|
||||
if not data:
|
||||
return response_error("Invalid global task ID")
|
||||
|
||||
GlobalTasks.query.filter(GlobalTasks.id == cur_id).delete()
|
||||
db.session.commit()
|
||||
|
||||
call_modules_hook('on_postload_global_task_delete', data=request.get_json(), caseid=caseid)
|
||||
track_activity("deleted global task ID {}".format(cur_id), caseid=caseid)
|
||||
|
||||
return response_success("Task deleted")
|
397
iris-web/source/app/blueprints/dashboard/templates/index.html
Normal file
397
iris-web/source/app/blueprints/dashboard/templates/index.html
Normal file
@ -0,0 +1,397 @@
|
||||
|
||||
{% extends "layouts/default.html" %}
|
||||
|
||||
{% block title %} Dashboard {% endblock title %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<link rel="stylesheet" href="/static/assets/css/suggestags.css">
|
||||
<link href="/static/assets/css/dataTables.contextualActions.min.css" rel="stylesheet">
|
||||
<link href="/static/assets/css/dataTables.select.min.css" rel="stylesheet">
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel-header bg-primary-gradient mt--4">
|
||||
<div class="page-inner py-5">
|
||||
<div class="d-flex align-items-left align-items-md-center flex-column flex-md-row ">
|
||||
<div>
|
||||
<h2 class="text-white pb-2 fw-bold">Dashboard</h2>
|
||||
</div>
|
||||
<div class="ml-md-auto py-2 py-md-0">
|
||||
<a href="/manage/cases?cid={{session['current_case'].case_id}}" class="btn btn-white btn-border btn-round mr-2">
|
||||
<span class="btn-label">
|
||||
<i class="fa fa-plus"></i>
|
||||
</span>
|
||||
Add case
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container mt--2 mb--2">
|
||||
<canvas id="htmlLegendsChart" style="display: block; width: auto; height: 100px;" width="auto" height="100px" class="chartjs-render-monitor"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-inner mt--5">
|
||||
<div class="row row-card-no-pd">
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="card card-stats card-round">
|
||||
<div class="card-body ">
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<div class="icon-big text-center">
|
||||
<i class="flaticon-file-1 text-success"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-7 col-stats">
|
||||
<div class="numbers">
|
||||
<p class="card-category">Cases (open / all)</p>
|
||||
<h4 class="card-title">{{ data.cases_open_count }} / {{ data.cases_count }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="card card-stats card-round">
|
||||
<div class="card-body ">
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<div class="icon-big text-center">
|
||||
<i class="flaticon-suitcase text-warning"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-7 col-stats">
|
||||
<div class="numbers">
|
||||
<p class="card-category">Attributed open cases</p>
|
||||
<h4 class="card-title">{{ data.user_open_count }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="card card-stats card-round">
|
||||
<div class="card-body ">
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<div class="icon-big text-center">
|
||||
<i id='icon_user_task' class=""></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-7 col-stats">
|
||||
<div class="numbers">
|
||||
<p class="card-category">Attributed open tasks</p>
|
||||
<h4 class="card-title" id="user_attr_count"></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" id="rowPendingCasesReview" style="display: none;">
|
||||
<div class="col-md-12">
|
||||
<section class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">Attributed cases review
|
||||
<div class="text-faded float-right">
|
||||
<small id="ureviews_last_updated"></small>
|
||||
<button type="button" class="btn btn-xs btn-dark ml-2"
|
||||
onclick="update_ureviews_list();">Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive" id="ureviews_table_wrapper">
|
||||
<table class="table display table-striped table-hover" width="100%" cellspacing="0" id="ureview_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Case name</th>
|
||||
<th>Review Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th>Case name</th>
|
||||
<th>Review Status</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{{ form.hidden_tag() }}
|
||||
<section class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">Attributed open tasks
|
||||
<div class="text-faded float-right">
|
||||
<small id="utasks_last_updated"></small>
|
||||
<button type="button" class="btn btn-xs btn-dark ml-2"
|
||||
onclick="update_utasks_list();">Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive" id="utasks_table_wrapper">
|
||||
<table class="table display table-striped table-hover" width="100%" cellspacing="0" id="utasks_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Status</th>
|
||||
<th>Case</th>
|
||||
<th>Last update</th>
|
||||
<th>Tags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Status</th>
|
||||
<th>Case</th>
|
||||
<th>Last update</th>
|
||||
<th>Tags</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<section class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">Attributed open cases
|
||||
<div class="text-faded float-right">
|
||||
<small id="ucases_last_updated"></small>
|
||||
<button type="button" class="btn btn-xs btn-dark ml-2"
|
||||
onclick="update_ucases_list(true);">Show closed cases
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-dark ml-2"
|
||||
onclick="update_ucases_list();">Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive" id="ucases_table_wrapper">
|
||||
<table class="table display table-striped table-hover" width="100%" cellspacing="0" id="ucases_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Client</th>
|
||||
<th>Opening date</th>
|
||||
<th>Tags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Client</th>
|
||||
<th>Opening date</th>
|
||||
<th>Tags</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
||||
<section class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">Global tasks
|
||||
<div class="text-faded float-right">
|
||||
<small id="tasks_last_updated"></small>
|
||||
<button type="button" class="btn btn-xs btn-dark ml-2"
|
||||
onclick="add_gtask();">
|
||||
Add global task
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-dark ml-2"
|
||||
onclick="update_gtasks_list();">Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive" id="gtasks_table_wrapper">
|
||||
<table class="table display table-striped table-hover" width="100%" cellspacing="0" id="gtasks_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Status</th>
|
||||
<th>Assigned to</th>
|
||||
<th>Last update</th>
|
||||
<th>Tags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Status</th>
|
||||
<th>Assigned to</th>
|
||||
<th>Last update</th>
|
||||
<th>Tags</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal " tabindex="-1" role="dialog" id="modal_add_gtask" data-backdrop="true">
|
||||
<div class="modal-xl modal-dialog" role="document">
|
||||
<div class="modal-content" id="modal_add_gtask_content">
|
||||
|
||||
</div>
|
||||
<!-- /.modal-content -->
|
||||
</div>
|
||||
<!-- /.modal-dialog -->
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascripts %}
|
||||
<script src="/static/assets/js/plugin/tagsinput/suggesttag.js"></script>
|
||||
<script src="/static/assets/js/plugin/select/select2.js"></script>
|
||||
|
||||
<script src="/static/assets/js/plugin/datatables/dataTables.cellEdit.js"></script>
|
||||
<script src="/static/assets/js/plugin/datatables/dataTables.buttons.min.js"></script>
|
||||
<script src="/static/assets/js/plugin/datatables/dataTables.contextualActions.min.js"></script>
|
||||
<script src="/static/assets/js/plugin/datatables/dataTables.select.min.js"></script>
|
||||
<script src="/static/assets/js/plugin/datatables/buttons.html5.min.js"></script>
|
||||
<script src="/static/assets/js/plugin/datatables/buttons.print.min.js"></script>
|
||||
<script src="/static/assets/js/iris/dashboard.js"></script>
|
||||
<script src="/static/assets/js/core/charts.js"></script>
|
||||
<script>
|
||||
|
||||
htmlLegendsChart = document.getElementById('htmlLegendsChart').getContext('2d');
|
||||
|
||||
$.ajax({
|
||||
url: '/dashboard/case_charts' + case_param(),
|
||||
type: "GET",
|
||||
dataType: "JSON",
|
||||
success: function (data) {
|
||||
jsdata = data;
|
||||
if (jsdata.status == "success") {
|
||||
// Chart with HTML Legends
|
||||
var gradientStroke = htmlLegendsChart.createLinearGradient(500, 0, 100, 0);
|
||||
gradientStroke.addColorStop(0, '#177dff');
|
||||
gradientStroke.addColorStop(1, '#80b6f4');
|
||||
|
||||
var gradientFill = htmlLegendsChart.createLinearGradient(500, 0, 100, 0);
|
||||
gradientFill.addColorStop(0, "rgba(23, 125, 255, 0.7)");
|
||||
gradientFill.addColorStop(1, "rgba(128, 182, 244, 0.3)");
|
||||
|
||||
|
||||
var myHtmlLegendsChart = new Chart(htmlLegendsChart, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: jsdata.data[0],
|
||||
datasets: [ {
|
||||
label: "Open case",
|
||||
borderColor: gradientStroke,
|
||||
pointBackgroundColor: gradientStroke,
|
||||
pointRadius: 0,
|
||||
backgroundColor: gradientFill,
|
||||
legendColor: '#fff',
|
||||
fill: true,
|
||||
borderWidth: 1,
|
||||
data: jsdata.data[1]
|
||||
}]
|
||||
},
|
||||
options : {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltips: {
|
||||
bodySpacing: 4,
|
||||
mode:"nearest",
|
||||
intersect: 0,
|
||||
position:"nearest",
|
||||
xPadding:10,
|
||||
yPadding:10,
|
||||
caretPadding:10
|
||||
},
|
||||
layout:{
|
||||
padding:{left:15,right:15,top:15,bottom:15}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
display: false
|
||||
},
|
||||
gridLines: {
|
||||
drawTicks: false,
|
||||
display: false
|
||||
}
|
||||
}],
|
||||
xAxes: [{
|
||||
gridLines: {
|
||||
zeroLineColor: "transparent",
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
padding: 20,
|
||||
fontColor: "rgba(0,0,0,0.5)",
|
||||
fontStyle: "500",
|
||||
display: false
|
||||
}
|
||||
}]
|
||||
},
|
||||
legendCallback: function(chart) {
|
||||
var text = [];
|
||||
text.push('<ul class="' + chart.id + '-legend html-legend">');
|
||||
for (var i = 0; i < chart.data.datasets.length; i++) {
|
||||
text.push('<li><span style="background-color:' + chart.data.datasets[i].legendColor + '"></span>');
|
||||
if (chart.data.datasets[i].label) {
|
||||
text.push(chart.data.datasets[i].label);
|
||||
}
|
||||
text.push('</li>');
|
||||
}
|
||||
text.push('</ul>');
|
||||
return text.join('');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//var myLegendContainer = document.getElementById("myChartLegend");
|
||||
|
||||
// generate HTML legend
|
||||
//myLegendContainer.innerHTML = myHtmlLegendsChart.generateLegend();
|
||||
}
|
||||
},
|
||||
error: function (error) {
|
||||
notify_error(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock javascripts %}
|
@ -0,0 +1,97 @@
|
||||
<div class="modal-header">
|
||||
<div class="col md-12">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h4 class="modal-title mt-1 mr-4">{% if task.id %} Task ID #{{ task.id }}{% else %} Add global task {% endif %}
|
||||
{% if task.id %}
|
||||
<i class="fas fa-info-circle ml-3" data-toggle="popover"
|
||||
title="Task info"
|
||||
data-content="Last updated {{ task.task_last_update }} by {{ user_name }}."></i>
|
||||
{% endif %}
|
||||
</h4>
|
||||
<small><i class="text-muted">{% if task.task_uuid %}#{{ task.task_uuid }}{% endif %}</i></small>
|
||||
</div>
|
||||
<div class="col ">
|
||||
<div class="row float-right">
|
||||
<button type="button" class="float-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true"><i class="fa fa-times"></i></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="container col-md-12">
|
||||
|
||||
<form method="post" action="" id="form_new_gtask">
|
||||
<div class="col-md-12 col-lg-12 col-sm-12">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="form-group mt-3">
|
||||
<label for="task_assignee" class="placeholder">Assigned to</label>
|
||||
{{ form.task_assignee_id(class="selectpicker pl--6 col-5") }}
|
||||
|
||||
<label for="task_status" class="placeholder">Status</label>
|
||||
{{ form.task_status_id(class="selectpicker pl--6 col-5") }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="task_title" class="placeholder">{{ form.task_title.label.text }} *</label>
|
||||
{{ form.task_title(class='form-control col-md-12 col-sm-12', autocomplete="off") }}
|
||||
</div>
|
||||
<div class="form-group mt-3">
|
||||
<label for="task_description" class="placeholder">{{ form.task_description.label.text }}</label>
|
||||
{{ form.task_description(class='form-control col-md-12 col-sm-12 sizable-textarea', autocomplete="off") }}
|
||||
</div>
|
||||
<div class="form-group mt-3">
|
||||
<label for="task_tags">Task tags
|
||||
</label>
|
||||
<input type="text" id="task_tags"
|
||||
class="form-control col-md-12" {% if task.task_tags %} value="{{ task.task_tags }}" {% endif %}/>
|
||||
</div>
|
||||
</div>
|
||||
{% if task.id %}
|
||||
<button type="button" class="btn btn-outline-danger mt-5"
|
||||
onclick="delete_gtask({{ task.id }});">Delete</button>
|
||||
|
||||
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
|
||||
onclick="update_gtask({{ task.id }});">Update</button>
|
||||
|
||||
{% else %}
|
||||
|
||||
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right"
|
||||
id="submit_new_gtask">Save</button>
|
||||
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$('#task_tags').amsifySuggestags({
|
||||
printValues: false,
|
||||
suggestions: [ {% for tag in tags %} '{{ tag }}', {% endfor %} ]
|
||||
});
|
||||
$('#task_assignee_id').selectpicker({
|
||||
liveSearch: true,
|
||||
title: "None",
|
||||
style: "Bootstrap 4: 'btn-outline-primary'",
|
||||
});
|
||||
$('#task_status_id').selectpicker({
|
||||
liveSearch: true,
|
||||
title: "None",
|
||||
style: "Bootstrap 4: 'btn-outline-primary'",
|
||||
});
|
||||
{% if uid %}
|
||||
$('#task_assignee_id').selectpicker('val', '{{uid}}');
|
||||
{% endif %}
|
||||
|
||||
{% if task.task_status_id %}
|
||||
$('#task_status_id').selectpicker('val', '{{task.task_status_id}}');
|
||||
{% else %}
|
||||
$('#task_status_id').selectpicker('val', '1');
|
||||
{% endif %}
|
||||
$('[data-toggle="popover"]').popover();
|
||||
</script>
|
||||
|
455
iris-web/source/app/blueprints/datastore/datastore_routes.py
Normal file
455
iris-web/source/app/blueprints/datastore/datastore_routes.py
Normal file
@ -0,0 +1,455 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
#
|
||||
# IRIS Source Code
|
||||
# Copyright (C) 2022 - DFIR IRIS Team
|
||||
# contact@dfir-iris.org
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
import base64
|
||||
import datetime
|
||||
import json
|
||||
import marshmallow.exceptions
|
||||
import urllib.parse
|
||||
from flask import Blueprint
|
||||
from flask import render_template
|
||||
from flask import request
|
||||
from flask import send_file
|
||||
from flask import url_for
|
||||
from flask_login import current_user
|
||||
from pathlib import Path
|
||||
from werkzeug.utils import redirect
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.datamgmt.datastore.datastore_db import datastore_add_child_node
|
||||
from app.datamgmt.datastore.datastore_db import datastore_add_file_as_evidence
|
||||
from app.datamgmt.datastore.datastore_db import datastore_add_file_as_ioc
|
||||
from app.datamgmt.datastore.datastore_db import datastore_delete_file
|
||||
from app.datamgmt.datastore.datastore_db import datastore_delete_node
|
||||
from app.datamgmt.datastore.datastore_db import datastore_filter_tree
|
||||
from app.datamgmt.datastore.datastore_db import datastore_get_file
|
||||
from app.datamgmt.datastore.datastore_db import datastore_get_interactive_path_node
|
||||
from app.datamgmt.datastore.datastore_db import datastore_get_local_file_path
|
||||
from app.datamgmt.datastore.datastore_db import datastore_get_path_node
|
||||
from app.datamgmt.datastore.datastore_db import datastore_get_standard_path
|
||||
from app.datamgmt.datastore.datastore_db import datastore_rename_node
|
||||
from app.datamgmt.datastore.datastore_db import ds_list_tree
|
||||
from app.forms import ModalDSFileForm
|
||||
from app.iris_engine.utils.tracker import track_activity
|
||||
from app.models.authorization import CaseAccessLevel
|
||||
from app.schema.marshables import DSFileSchema, DSPathSchema
|
||||
from app.util import ac_api_case_requires
|
||||
from app.util import ac_case_requires
|
||||
from app.util import add_obj_history_entry
|
||||
from app.util import response_error
|
||||
from app.util import response_success
|
||||
|
||||
datastore_blueprint = Blueprint(
|
||||
'datastore',
|
||||
__name__,
|
||||
template_folder='templates'
|
||||
)
|
||||
|
||||
logger = app.logger
|
||||
|
||||
|
||||
@datastore_blueprint.route('/datastore/list/tree', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def datastore_list_tree(caseid):
|
||||
|
||||
data = ds_list_tree(caseid)
|
||||
|
||||
return response_success("", data=data)
|
||||
|
||||
|
||||
@datastore_blueprint.route('/datastore/list/filter', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def datastore_list_filter(caseid):
|
||||
|
||||
args = request.args.to_dict()
|
||||
query_filter = args.get('q')
|
||||
try:
|
||||
|
||||
filter_d = dict(json.loads(urllib.parse.unquote_plus(query_filter)))
|
||||
|
||||
except Exception as e:
|
||||
logger.error('Error parsing filter: {}'.format(query_filter))
|
||||
logger.error(e)
|
||||
return response_error('Invalid query')
|
||||
|
||||
data, log = datastore_filter_tree(filter_d, caseid)
|
||||
if data is None:
|
||||
logger.error('Error parsing filter: {}'.format(query_filter))
|
||||
logger.error(log)
|
||||
return response_error('Invalid query')
|
||||
|
||||
return response_success("", data=data)
|
||||
|
||||
|
||||
@datastore_blueprint.route('/datastore/file/add/<int:cur_id>/modal', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.full_access)
|
||||
def datastore_add_file_modal(cur_id: int, caseid: int, url_redir: bool):
|
||||
|
||||
if url_redir:
|
||||
return redirect(url_for('index.index', cid=caseid, redirect=True))
|
||||
|
||||
dsp = datastore_get_path_node(cur_id, caseid)
|
||||
if not dsp:
|
||||
return response_error('Invalid path node for this case')
|
||||
|
||||
form = ModalDSFileForm()
|
||||
|
||||
return render_template("modal_ds_file.html", form=form, file=None, dsp=dsp)
|
||||
|
||||
|
||||
@datastore_blueprint.route('/datastore/filter-help/modal', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def datastore_filter_help_modal(caseid, url_redir):
|
||||
if url_redir:
|
||||
return redirect(url_for('index.index', cid=caseid, redirect=True))
|
||||
|
||||
return render_template("modal_help_filter_ds.html")
|
||||
|
||||
|
||||
@datastore_blueprint.route('/datastore/file/update/<int:cur_id>/modal', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def datastore_update_file_modal(cur_id: int, caseid: int, url_redir: bool):
|
||||
|
||||
if url_redir:
|
||||
return redirect(url_for('index.index', cid=caseid, redirect=True))
|
||||
|
||||
file = datastore_get_file(cur_id, caseid)
|
||||
if not file:
|
||||
return response_error('Invalid file ID for this case')
|
||||
|
||||
dsp = datastore_get_path_node(file.file_parent_id, caseid)
|
||||
|
||||
form = ModalDSFileForm()
|
||||
form.file_is_ioc.data = file.file_is_ioc
|
||||
form.file_original_name.data = file.file_original_name
|
||||
form.file_password.data = file.file_password
|
||||
form.file_password.render_kw = {'disabled': 'disabled'}
|
||||
form.file_description.data = file.file_description
|
||||
form.file_is_evidence.data = file.file_is_evidence
|
||||
|
||||
return render_template("modal_ds_file.html", form=form, file=file, dsp=dsp)
|
||||
|
||||
|
||||
@datastore_blueprint.route('/datastore/file/info/<int:cur_id>/modal', methods=['GET'])
|
||||
@ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def datastore_info_file_modal(cur_id: int, caseid: int, url_redir: bool):
|
||||
|
||||
if url_redir:
|
||||
return redirect(url_for('index.index', cid=caseid, redirect=True))
|
||||
|
||||
file = datastore_get_file(cur_id, caseid)
|
||||
if not file:
|
||||
return response_error('Invalid file ID for this case')
|
||||
|
||||
dsp = datastore_get_path_node(file.file_parent_id, caseid)
|
||||
|
||||
return render_template("modal_ds_file_info.html", file=file, dsp=dsp)
|
||||
|
||||
|
||||
@datastore_blueprint.route('/datastore/file/info/<int:cur_id>', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def datastore_info_file(cur_id: int, caseid: int):
|
||||
|
||||
file = datastore_get_file(cur_id, caseid)
|
||||
if not file:
|
||||
return response_error('Invalid file ID for this case')
|
||||
|
||||
file_schema = DSFileSchema()
|
||||
file = file_schema.dump(file)
|
||||
del file['file_local_name']
|
||||
|
||||
return response_success("", data=file)
|
||||
|
||||
|
||||
@datastore_blueprint.route('/datastore/file/update/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def datastore_update_file(cur_id: int, caseid: int):
|
||||
|
||||
dsf = datastore_get_file(cur_id, caseid)
|
||||
if not dsf:
|
||||
return response_error('Invalid file ID for this case')
|
||||
|
||||
dsf_schema = DSFileSchema()
|
||||
try:
|
||||
|
||||
dsf_sc = dsf_schema.load(request.form, instance=dsf, partial=True)
|
||||
add_obj_history_entry(dsf_sc, 'updated')
|
||||
|
||||
dsf.file_is_ioc = request.form.get('file_is_ioc') is not None or request.form.get('file_is_ioc') is True
|
||||
dsf.file_is_evidence = request.form.get('file_is_evidence') is not None or request.form.get('file_is_evidence') is True
|
||||
|
||||
db.session.commit()
|
||||
|
||||
if request.files.get('file_content'):
|
||||
ds_location = datastore_get_standard_path(dsf_sc, caseid)
|
||||
dsf_sc.file_local_name, dsf_sc.file_size, dsf_sc.file_sha256 = dsf_schema.ds_store_file(
|
||||
request.files.get('file_content'),
|
||||
ds_location,
|
||||
dsf_sc.file_is_ioc,
|
||||
dsf_sc.file_password)
|
||||
|
||||
if dsf_sc.file_is_ioc and not dsf_sc.file_password:
|
||||
dsf_sc.file_password = 'infected'
|
||||
|
||||
db.session.commit()
|
||||
|
||||
msg_added_as = ''
|
||||
if dsf.file_is_ioc:
|
||||
datastore_add_file_as_ioc(dsf, caseid)
|
||||
msg_added_as += 'and added in IOC'
|
||||
|
||||
if dsf.file_is_evidence:
|
||||
datastore_add_file_as_evidence(dsf, caseid)
|
||||
msg_added_as += ' and evidence' if len(msg_added_as) > 0 else 'and added in evidence'
|
||||
|
||||
track_activity(f'File \"{dsf.file_original_name}\" updated in DS', caseid=caseid)
|
||||
return response_success('File updated in datastore', data=dsf_schema.dump(dsf_sc))
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages)
|
||||
|
||||
|
||||
@datastore_blueprint.route('/datastore/file/move/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def datastore_move_file(cur_id: int, caseid: int):
|
||||
|
||||
if not request.json:
|
||||
return response_error("Invalid data")
|
||||
|
||||
dsf = datastore_get_file(cur_id, caseid)
|
||||
if not dsf:
|
||||
return response_error('Invalid file ID for this case')
|
||||
|
||||
dsp = datastore_get_path_node(request.json.get('destination-node'), caseid)
|
||||
if not dsp:
|
||||
return response_error('Invalid destination node ID for this case')
|
||||
|
||||
dsf.file_parent_id = dsp.path_id
|
||||
db.session.commit()
|
||||
|
||||
track_activity(f'File \"{dsf.file_original_name}\" moved to \"{dsp.path_name}\" in DS', caseid=caseid)
|
||||
return response_success(f"File successfully moved to {dsp.path_name}")
|
||||
|
||||
|
||||
@datastore_blueprint.route('/datastore/folder/move/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def datastore_move_folder(cur_id: int, caseid: int):
|
||||
|
||||
if not request.json:
|
||||
return response_error("Invalid data")
|
||||
|
||||
dsp = datastore_get_path_node(cur_id, caseid)
|
||||
if not dsp:
|
||||
return response_error('Invalid file ID for this case')
|
||||
|
||||
dsp_dst = datastore_get_path_node(request.json.get('destination-node'), caseid)
|
||||
if not dsp_dst:
|
||||
return response_error('Invalid destination node ID for this case')
|
||||
|
||||
if dsp.path_id == dsp_dst.path_id:
|
||||
return response_error("If that's true, then I've made a mistake, and you should kill me now.")
|
||||
|
||||
dsp.path_parent_id = dsp_dst.path_id
|
||||
db.session.commit()
|
||||
|
||||
dsf_folder_schema = DSPathSchema()
|
||||
|
||||
msg = f"Folder \"{dsp.path_name}\" successfully moved to \"{dsp_dst.path_name}\""
|
||||
track_activity(msg, caseid=caseid)
|
||||
return response_success(msg, data=dsf_folder_schema.dump(dsp))
|
||||
|
||||
|
||||
@datastore_blueprint.route('/datastore/file/view/<int:cur_id>', methods=['GET'])
|
||||
@ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
|
||||
def datastore_view_file(cur_id: int, caseid: int):
|
||||
has_error, dsf = datastore_get_local_file_path(cur_id, caseid)
|
||||
if has_error:
|
||||
return response_error('Unable to get requested file ID', data=dsf)
|
||||
|
||||
if dsf.file_is_ioc or dsf.file_password:
|
||||
dsf.file_original_name += ".zip"
|
||||
|
||||
if not Path(dsf.file_local_name).is_file():
|
||||
return response_error(f'File {dsf.file_local_name} does not exists on the server. '
|
||||
f'Update or delete virtual entry')
|
||||
|
||||
resp = send_file(dsf.file_local_name, as_attachment=True,
|
||||
download_name=dsf.file_original_name)
|
||||
|
||||
track_activity(f"File \"{dsf.file_original_name}\" downloaded", caseid=caseid, display_in_ui=False)
|
||||
return resp
|
||||
|
||||
|
||||
@datastore_blueprint.route('/datastore/file/add/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def datastore_add_file(cur_id: int, caseid: int):
|
||||
|
||||
dsp = datastore_get_path_node(cur_id, caseid)
|
||||
if not dsp:
|
||||
return response_error('Invalid path node for this case')
|
||||
|
||||
dsf_schema = DSFileSchema()
|
||||
try:
|
||||
|
||||
dsf_sc = dsf_schema.load(request.form, partial=True)
|
||||
|
||||
dsf_sc.file_parent_id = dsp.path_id
|
||||
dsf_sc.added_by_user_id = current_user.id
|
||||
dsf_sc.file_date_added = datetime.datetime.now()
|
||||
dsf_sc.file_local_name = 'tmp_xc'
|
||||
dsf_sc.file_case_id = caseid
|
||||
add_obj_history_entry(dsf_sc, 'created')
|
||||
|
||||
if dsf_sc.file_is_ioc and not dsf_sc.file_password:
|
||||
dsf_sc.file_password = 'infected'
|
||||
|
||||
db.session.add(dsf_sc)
|
||||
db.session.commit()
|
||||
|
||||
ds_location = datastore_get_standard_path(dsf_sc, caseid)
|
||||
dsf_sc.file_local_name, dsf_sc.file_size, dsf_sc.file_sha256 = dsf_schema.ds_store_file(
|
||||
request.files.get('file_content'),
|
||||
ds_location,
|
||||
dsf_sc.file_is_ioc,
|
||||
dsf_sc.file_password)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
msg_added_as = ''
|
||||
if dsf_sc.file_is_ioc:
|
||||
datastore_add_file_as_ioc(dsf_sc, caseid)
|
||||
msg_added_as += 'and added in IOC'
|
||||
|
||||
if dsf_sc.file_is_evidence:
|
||||
datastore_add_file_as_evidence(dsf_sc, caseid)
|
||||
msg_added_as += ' and evidence' if len(msg_added_as) > 0 else 'and added in evidence'
|
||||
|
||||
track_activity(f"File \"{dsf_sc.file_original_name}\" added to DS", caseid=caseid)
|
||||
return response_success(f'File saved in datastore {msg_added_as}', data=dsf_schema.dump(dsf_sc))
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages)
|
||||
|
||||
|
||||
@datastore_blueprint.route('/datastore/file/add-interactive', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def datastore_add_interactive_file(caseid: int):
|
||||
|
||||
dsp = datastore_get_interactive_path_node(caseid)
|
||||
if not dsp:
|
||||
return response_error('Invalid path node for this case')
|
||||
|
||||
dsf_schema = DSFileSchema()
|
||||
try:
|
||||
js_data = request.json
|
||||
|
||||
try:
|
||||
file_content = base64.b64decode(js_data.get('file_content'))
|
||||
filename = js_data.get('file_original_name')
|
||||
except Exception as e:
|
||||
return response_error(msg=str(e))
|
||||
|
||||
dsf_sc, existed = dsf_schema.ds_store_file_b64(filename, file_content, dsp, caseid)
|
||||
|
||||
if not existed:
|
||||
msg = "File saved in datastore"
|
||||
|
||||
else:
|
||||
msg = "File already existing in datastore. Using it."
|
||||
|
||||
track_activity(f"File \"{dsf_sc.file_original_name}\" added to DS", caseid=caseid)
|
||||
return response_success(msg, data={"file_url": f"/datastore/file/view/{dsf_sc.file_id}"})
|
||||
|
||||
except marshmallow.exceptions.ValidationError as e:
|
||||
return response_error(msg="Data error", data=e.messages)
|
||||
|
||||
|
||||
@datastore_blueprint.route('/datastore/folder/add', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def datastore_add_folder(caseid: int):
|
||||
data = request.json
|
||||
if not data:
|
||||
return response_error('Invalid data')
|
||||
|
||||
parent_node = data.get('parent_node')
|
||||
folder_name = data.get('folder_name')
|
||||
|
||||
if not parent_node or not folder_name:
|
||||
return response_error('Invalid data')
|
||||
|
||||
has_error, logs, node = datastore_add_child_node(parent_node, folder_name, caseid)
|
||||
dsf_folder_schema = DSPathSchema()
|
||||
|
||||
if not has_error:
|
||||
track_activity(f"Folder \"{folder_name}\" added to DS", caseid=caseid)
|
||||
return response_success(msg=logs, data=dsf_folder_schema.dump(node))
|
||||
|
||||
return response_error(msg=logs)
|
||||
|
||||
|
||||
@datastore_blueprint.route('/datastore/folder/rename/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def datastore_rename_folder(cur_id: int, caseid: int):
|
||||
data = request.json
|
||||
if not data:
|
||||
return response_error('Invalid data')
|
||||
|
||||
parent_node = data.get('parent_node')
|
||||
folder_name = data.get('folder_name')
|
||||
|
||||
if not parent_node or not folder_name:
|
||||
return response_error('Invalid data')
|
||||
|
||||
if int(parent_node) != cur_id:
|
||||
return response_error('Invalid data')
|
||||
|
||||
has_error, logs, dsp_base = datastore_rename_node(parent_node, folder_name, caseid)
|
||||
dsf_folder_schema = DSPathSchema()
|
||||
|
||||
if has_error:
|
||||
return response_error(logs)
|
||||
|
||||
track_activity(f"Folder \"{parent_node}\" renamed to \"{folder_name}\" in DS", caseid=caseid)
|
||||
return response_success(logs, data=dsf_folder_schema.dump(dsp_base))
|
||||
|
||||
|
||||
@datastore_blueprint.route('/datastore/folder/delete/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def datastore_delete_folder_route(cur_id: int, caseid: int):
|
||||
|
||||
has_error, logs = datastore_delete_node(cur_id, caseid)
|
||||
if has_error:
|
||||
return response_error(logs)
|
||||
|
||||
track_activity(f"Folder \"{cur_id}\" deleted from DS", caseid=caseid)
|
||||
return response_success(logs)
|
||||
|
||||
|
||||
@datastore_blueprint.route('/datastore/file/delete/<int:cur_id>', methods=['POST'])
|
||||
@ac_api_case_requires(CaseAccessLevel.full_access)
|
||||
def datastore_delete_file_route(cur_id: int, caseid: int):
|
||||
|
||||
has_error, logs = datastore_delete_file(cur_id, caseid)
|
||||
if has_error:
|
||||
return response_error(logs)
|
||||
|
||||
track_activity(f"File \"{cur_id}\" deleted from DS", caseid=caseid)
|
||||
return response_success(logs)
|
@ -0,0 +1,119 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title mt-2 mr-4">Datastore File</h4>
|
||||
<div class="row text-right">
|
||||
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true"><i class="fa fa-times"></i></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container col-md-12">
|
||||
<form method="post" action="" id="form_new_ds_file">
|
||||
<div class="col-md-12 col-lg-12 col-sm-12">
|
||||
{{ form.hidden_tag() }}
|
||||
{% if file.file_id %}
|
||||
<div class="row ml-2">
|
||||
<p>The file is currently saved in virtual folder <code>{{ dsp.path_name }}</code>.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row ml-2">
|
||||
<p>The file will be saved in virtual folder <code>{{ dsp.path_name }}</code>.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-row ml-2">
|
||||
<div class="form-group col-12">
|
||||
<label for="input_upload_ds_file" class="form-label">Choose file to upload : </label>
|
||||
<input id="input_upload_ds_file" class="form-control" type="file">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row ml-2">
|
||||
<div class="form-group col-12">
|
||||
<label for="file_original_name" class="placeholder">Filename *</label>
|
||||
{{ form.file_original_name(class='form-control', autocomplete="off") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row ml-2">
|
||||
<div class="form-group col-12">
|
||||
<label for="file_description" class="placeholder">Description</label>
|
||||
{{ form.file_description(class='form-control col-md-12 col-sm-12 sizable-textarea', autocomplete="off") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row ml-2">
|
||||
<div class="form-group col-6 col-xs-12">
|
||||
<label for="file_password" class="placeholder">Password<i class="ml-1 mt-1 fa-regular text-dark fa-circle-question"
|
||||
title="Help" data-toggle="popover" data-html="true"
|
||||
data-trigger="hover" style="cursor: pointer;"
|
||||
data-content="If set, the file is locally encrypted with this password.<br/><b class='text-danger'>Passwords are stored in clear text server side. Do not put sensitive password here.</b><br/>Encrypted files cannot be used in notes.<br/>IOC are automatically encrypted with password <code>infected</code> unless specified otherwise here.">
|
||||
|
||||
</i></label>
|
||||
<div class="input-group mb-2 mr-sm-2">
|
||||
{{ form.file_password(class='form-control', autocomplete="off", type="password") }}
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text" id="toggle_file_password"><i class="fa-solid fa-eye"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-6 col-xs-12">
|
||||
<label for="file_tags">File tags</label>
|
||||
<input type="text" id="file_tags" name="file_tags"
|
||||
class="form-control col-md-12" {% if file.file_tags %} value="{{ file.file_tags }}" {% endif %}/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row ml-2">
|
||||
<div class="form-group col-6 col-xs-12">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
{{ form.file_is_ioc(class="form-check-input", type="checkbox") }}
|
||||
<span class="form-check-sign"> File is IOC <i class="ml-1 mt-1 fa-regular text-dark fa-circle-question"
|
||||
title="Help" data-toggle="popover" data-html="true"
|
||||
data-trigger="hover" style="cursor: pointer;"
|
||||
data-content="If set, the file is stored in a dedicated IOC folder on the server and is encrypted with password <code>infected</code> unless specified otherwise in the password field.<br/> The file is also added to the case IOC.">
|
||||
|
||||
</i></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-6 col-xs-12">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
{{ form.file_is_evidence(class="form-check-input", type="checkbox") }}
|
||||
<span class="form-check-sign"> File is Evidence <i class="ml-1 mt-1 fa-regular text-dark fa-circle-question"
|
||||
title="Help" data-toggle="popover" data-html="true"
|
||||
data-trigger="hover" style="cursor: pointer;"
|
||||
data-content="If set, the file is stored in a dedicated Evidence folder on the server and added to the case Evidences.">
|
||||
|
||||
</i></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if file.file_id %}
|
||||
<button type="button" class="btn btn-outline-danger ml-4 mt-5"
|
||||
onclick="delete_ds_file({{ file.file_id }});">Delete</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-outline-success ml-4 mt-5 float-right" onclick="save_ds_file({{dsp.path_id}}, {{ file.file_id }});return false;">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$('[data-toggle="popover"]').popover();
|
||||
$('#toggle_file_password').on('click', function (e) {
|
||||
const type = $('#file_password').attr('type') === 'password' ? 'text' : 'password';
|
||||
$('#file_password').attr('type', type);
|
||||
|
||||
$('#toggle_file_password > i').attr('class', type === 'password' ? 'fa-solid fa-eye' : 'fa-solid fa-eye-slash');
|
||||
});
|
||||
|
||||
$('#file_tags').amsifySuggestags({
|
||||
printValues: true,
|
||||
{% if file.file_tags %}
|
||||
suggestions: [ {% for tag in file.file_tags %} '{{ tag }}', {% endfor %} ],
|
||||
{% endif %}
|
||||
whiteList: false,
|
||||
selectOnHover: false,
|
||||
});
|
||||
$("#input_upload_ds_file").on("change", function(e) {
|
||||
var file = e.target.files[0].name;
|
||||
$('#file_original_name').val(file);
|
||||
});
|
||||
</script>
|
@ -0,0 +1,74 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title mt-2 mr-4">Datastore File {{ file.file_original_name }}</h4>
|
||||
<div class="row text-right">
|
||||
<button type="button" class="pull-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true"><i class="fa fa-times"></i></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container col-md-12">
|
||||
<div class="col-md-12 col-lg-12 col-sm-12">
|
||||
<h3>File information</h3>
|
||||
<dl class="row mt-2">
|
||||
<dt class="col-sm-3">Virtual location: </dt>
|
||||
<dd class="col-sm-8">{{ dsp.path_name }}</dd>
|
||||
|
||||
<dt class="col-sm-3">Original file name: </dt>
|
||||
<dd class="col-sm-8">{{ file.file_original_name }}</dd>
|
||||
|
||||
<dt class="col-sm-3">File description: </dt>
|
||||
<dd class="col-sm-8">{{ file.file_description }}</dd>
|
||||
|
||||
<dt class="col-sm-3">Storage UUID: </dt>
|
||||
<dd class="col-sm-8">dsf-{{ file.file_uuid }}</dd>
|
||||
|
||||
<dt class="col-sm-3">Storage ID: </dt>
|
||||
<dd class="col-sm-8">dsf-{{ file.file_id }}</dd>
|
||||
|
||||
<dt class="col-sm-3">Tags: </dt>
|
||||
<dd class="col-sm-8">{% for tag in file.file_tags.split(',') %} <div class="badge badge-light">{{ tag }}</div> {% endfor %}</dd>
|
||||
|
||||
<dt class="col-sm-3">SHA256: </dt>
|
||||
<dd class="col-sm-8">{{ file.file_sha256 }}</dd>
|
||||
|
||||
<dt class="col-sm-3">Size (bytes): </dt>
|
||||
<dd class="col-sm-8">{{ file.file_size }}</dd>
|
||||
|
||||
<dt class="col-sm-3">Is evidence: </dt>
|
||||
<dd class="col-sm-8">{{ file.file_is_evidence }}</dd>
|
||||
|
||||
<dt class="col-sm-3">Is IOC: </dt>
|
||||
<dd class="col-sm-8">{{ file.file_is_ioc }}</dd>
|
||||
|
||||
<dt class="col-sm-3">Is password protected: </dt>
|
||||
<dd class="col-sm-8">{% if file.file_password %} True {% else %} False {% endif %}</dd>
|
||||
|
||||
<dt class="col-sm-3">Password: </dt>
|
||||
<dd class="col-sm-8">
|
||||
<div class="row">
|
||||
<input class="form_control ml-3" style="border:none;" type="password" value="{{ file.file_password }}" id="ds_file_password" disabled>
|
||||
<div class="file_show_password" id="toggle_file_password"><i class="fa-solid fa-eye"></i></div>
|
||||
</div>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3 mt-4">Modification history: </dt>
|
||||
<dd class="mt-4">
|
||||
<ul>
|
||||
{% if file.modification_history %}
|
||||
{% for mod in file.modification_history %}
|
||||
<li>{{ mod|format_datetime('%Y-%m-%d %H:%M') }} - {{ file.modification_history[mod].action }} by {{ file.modification_history[mod].user }} </li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$('#toggle_file_password').on('click', function (e) {
|
||||
const type = $('#ds_file_password').attr('type') === 'password' ? 'text' : 'password';
|
||||
$('#ds_file_password').attr('type', type);
|
||||
$('#toggle_file_password > i').attr('class', type === 'password' ? 'fa-solid fa-eye' : 'fa-solid fa-eye-slash');
|
||||
});
|
||||
</script>
|
@ -0,0 +1,32 @@
|
||||
<div class="modal-xl modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title mt-1 mr-4">Datastore filtering help</h4>
|
||||
<button type="button" class="float-right btn bg-transparent" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true"><i class="fa fa-times"></i></span></button>
|
||||
</div>
|
||||
<div class="modal-body mb-2">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p>Files can be filtered thanks to simple queries. The query schema is : <code>target_element:search_value AND target_element2:search_value2</code>.<br/>
|
||||
There is no <code>OR</code> condition and searching without target does not work.
|
||||
<p>The following target elements can be used to filter :</p>
|
||||
<ul>
|
||||
<li><code>name</code>: Name of the file</li>
|
||||
<li><code>id</code>: ID of the file</li>
|
||||
<li><code>uuid</code>: UUID of the file</li>
|
||||
<li><code>storage_name</code>: Name of the file on the FS</li>
|
||||
<li><code>tag</code>: Tag of the file</li>
|
||||
<li><code>description</code>: Description of the file</li>
|
||||
<li><code>is_ioc</code> : Set to any value to filter files which are IOCs</li>
|
||||
<li><code>is_evidence</code>: Set to any value to filter files which are evidences</li>
|
||||
<li><code>has_password</code>: Set to any value to filter files which have passwords</li>
|
||||
<li><code>sha256</code>: SHA256 to filter files with</li>
|
||||
</ul>
|
||||
Example of filter :
|
||||
<code>name: .exe AND is_ioc: true</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
59
iris-web/source/app/blueprints/demo_landing/demo_landing.py
Normal file
59
iris-web/source/app/blueprints/demo_landing/demo_landing.py
Normal file
@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# IRIS Source Code
|
||||
# contact@dfir-iris.org
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# IMPORTS ------------------------------------------------
|
||||
|
||||
from flask import Blueprint
|
||||
from flask import render_template
|
||||
|
||||
from app import app
|
||||
from app.iris_engine.demo_builder import gen_demo_admins
|
||||
from app.iris_engine.demo_builder import gen_demo_users
|
||||
|
||||
demo_blueprint = Blueprint(
|
||||
'demo-landing',
|
||||
__name__,
|
||||
template_folder='templates'
|
||||
)
|
||||
|
||||
log = app.logger
|
||||
|
||||
if app.config.get('DEMO_MODE_ENABLED') == 'True':
|
||||
@demo_blueprint.route('/welcome', methods=['GET'])
|
||||
def demo_landing():
|
||||
iris_version = app.config.get('IRIS_VERSION')
|
||||
demo_domain = app.config.get('DEMO_DOMAIN')
|
||||
seed_user = app.config.get('DEMO_USERS_SEED')
|
||||
seed_adm = app.config.get('DEMO_ADM_SEED')
|
||||
adm_count = int(app.config.get('DEMO_ADM_COUNT', 4))
|
||||
users_count = int(app.config.get('DEMO_USERS_COUNT', 10))
|
||||
|
||||
demo_users = [
|
||||
{'username': username, 'password': pwd, 'role': 'Admin'} for _, username, pwd, _ in gen_demo_admins(adm_count, seed_adm)
|
||||
]
|
||||
demo_users += [
|
||||
{'username': username, 'password': pwd, 'role': 'User'} for _, username, pwd, _ in gen_demo_users(users_count, seed_user)
|
||||
]
|
||||
|
||||
return render_template(
|
||||
'demo-landing.html',
|
||||
iris_version=iris_version,
|
||||
demo_domain=demo_domain,
|
||||
demo_users=demo_users
|
||||
)
|
@ -0,0 +1,160 @@
|
||||
<html class="" lang="en"><head>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<title>IRIS Demonstration</title>
|
||||
|
||||
<meta name="robots" content="noindex">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Barlow:wght@100&display=swap">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">
|
||||
<link rel="stylesheet" href="/static/assets/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/static/assets/css/atlantis.css">
|
||||
<link rel="stylesheet" href="/static/assets/css/demo.css">
|
||||
<link rel="icon" href="/static/assets/img/logo.ico" type="image/x-icon"/>
|
||||
<script defer data-domain="v200.beta.dfir-iris.org" src="https://analytics.dfir-iris.org/js/plausible.js"></script>
|
||||
|
||||
</head>
|
||||
<body class="landing-demo">
|
||||
<div class="ml-1 row justify-content-center mr-1">
|
||||
<div class="col-xl-8">
|
||||
<div class="card mt-3">
|
||||
<div class="mt-4">
|
||||
<div class="col d-flex justify-content-center">
|
||||
<a href="/" class="logo ml-2 text-center">
|
||||
<img src="/static/assets/img/logo-full-blue.png" alt="navbar brand" width="300rem">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h4 class="ml-auto mr-auto"><span class="text-danger">shared</span> demonstration instance {{ iris_version }}</h4>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h5 class="text-muted ml-auto mr-auto"><i>Try out IRIS, find bugs and security vulnerabilities</i></h5><br/>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
</div>
|
||||
<div class="row mt-4">
|
||||
</div>
|
||||
<div class="row mt-2 mb-4">
|
||||
<div class="col-md-1 col-lg-2"></div>
|
||||
<div class="col-md-10 col-lg-8 ml-4">
|
||||
<h3 class=" ml-auto mr-auto">Hey there, please read the following carefully</h3><br/>
|
||||
<ul>
|
||||
<li><b>Do not upload any illegal or confidential materials</b></li>
|
||||
<li><b>Do not download and open files from other users blindly</b></li>
|
||||
<li><b>Respect a <a class="text-muted" target="_blank" rel="noopener noreferrer" href="https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html#responsible-or-coordinated-disclosure">responsible disclosure</a> of 30 days if you find a vulnerability</b></li>
|
||||
</ul>
|
||||
<b>Not sure what IRIS is about? You'll find more info on the <a target="_blank" rel="noopener" href="https://dfir-iris.org">main website</a></b>
|
||||
</div>
|
||||
<div class="col-md-1 col-lg-2"></div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-1 col-lg-2"></div>
|
||||
<div class="col-md-10 col-lg-8 ml-4 mr-3">
|
||||
<p class="">Accounts to access the instance are available at the bottom of the page. <br/>
|
||||
IRIS is not optimized to be used on phones. We recommend accessing it from a computer.<br/>
|
||||
If you notice anything suspicious or have any question, please <a href="mailto:contact@dfir-iris.org">contact us</a>. <br/>Note that the instance might be reset at any moment.</p>
|
||||
|
||||
<p><i>By accessing this instance you confirm you read, understand and agree with all the information on this page.</i></p>
|
||||
</div>
|
||||
<div class="col-md-1 col-lg-2"></div>
|
||||
</div>
|
||||
<div class="row mt-4 mb-4 mr-2">
|
||||
|
||||
<a class="btn btn-outline-success ml-auto mr-auto" target="_blank" rel="noopener" href="/login">
|
||||
Access IRIS
|
||||
</a>
|
||||
</div>
|
||||
<div class="row mt-4 mb-4 mr-2 justify-content-center">
|
||||
<div class="ml-mr-auto">
|
||||
<button class="btn btn-primary" type="button" data-toggle="collapse" data-target="#collapseSecRules" aria-expanded="false" aria-controls="collapseSecRules">
|
||||
Rules of engagement
|
||||
</button>
|
||||
<button class="btn btn-primary" type="button" data-toggle="collapse" data-target="#collapseLiability" aria-expanded="false" aria-controls="collapseLiability">
|
||||
Disclaimer
|
||||
</button>
|
||||
<button class="btn btn-primary" type="button" data-toggle="collapse" data-target="#collapseAccounts" aria-expanded="false" aria-controls="collapseAccounts">
|
||||
Accounts
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-4 mb-4 mr-2 justify-content-center">
|
||||
<div class="col ml-4">
|
||||
<div class="collapse" id="collapseLiability">
|
||||
<div class="card card-body">
|
||||
<h3 class="mt-2">Disclaimer</h3>
|
||||
DFIR-IRIS is a non-profit organization. It is not responsible for any damage caused by the use of this site and any material contained in it, or from any action or decision taken as a result of using this site.<br/>
|
||||
It is not responsible for the content of any external sites linked to this site.<br/> By using this site, you acknowledge that content posted on this site is public and DFIR-IRIS cannot guarantee the security of any information disclose on it; you make such disclosures at your own risk.
|
||||
|
||||
<h4 class="mt-2">Privacy</h4><br/>
|
||||
<p>This demonstration instance is shared and we cannot guarantee the privacy of data you might upload on it. We are not responsible for any data loss or data leak. </p>
|
||||
<p>To better understand the use of this instance, DFIR-IRIS uses a privacy-friendly cookie-less analytics. DFIR-IRIS does not collect any personal data. DFIR-IRIS does not use any third-party analytics and uses a self-hosted <a target="_blank" rel="noopener" href="https://plausible.io/">Plausible</a> instance.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse" id="collapseSecRules">
|
||||
<div class="card card-body">
|
||||
<h3 class="mt-2">Rules of engagement</h3>
|
||||
<p class=""><b>If you find a vulnerability</b>, <a href="mailto:contact@dfir-iris.org">contact us</a> before going public as it may impact systems already in production.<br/>
|
||||
In other words, please respect a responsible disclosure of 30 days. We will patch and then publish the vulnerability. Depending on the finding a CVE might be requested, and will have your name - except if you don't want to.<br/>
|
||||
You can report anything you find at <a href="mailto:contact@dfir-iris.org">contact@dfir-iris.org</a>.</p>
|
||||
<p class=""><b>The scope of the security tests</b> is limited to the Web Application IRIS hosted on <a class="" target="_blank" rel="noopener" href="{{ demo_domain }}">{{ demo_domain }}</a>.<br/>
|
||||
<span class="text-danger">Subdomains, SSH, scanning of the IP, BF, and other flavors are <b>out of scope.</b></span></p>
|
||||
We are mostly interested in the following:
|
||||
<ul>
|
||||
<li><b>authentication bypass</b>: achieve any action requiring an authentication without being authenticated. <span class="text-danger">Brute-force is not what we are looking for</span></li>
|
||||
<li><b>privilege escalations within the application</b>: from a standard user (<code>user_std_XX</code>) to administrative rights (<code>adm_XX</code>) on IRIS</li>
|
||||
<li><b>privilege escalations on the host server</b>: from a standard user (<code>user_std_XX</code>) to code execution on the server</li>
|
||||
<li><b>data leakage</b>: from a standard user (<code>user_std_XX</code>) read data of non-accessible cases (titled <code>Restricted Case XXX</code>)</li>
|
||||
</ul>
|
||||
<h3>Important Remarks</h3>
|
||||
<ul>
|
||||
<li>If you can, use a local instance of IRIS instead of this one. It only takes a few minutes to <a target="_blank" rel="noopener" href="https://docs.dfir-iris.org/getting_started/">get it on docker.</a></li>
|
||||
<li>The administrators account can publish stored XSS on the platform via <a target="_blank" rel="noopener" href="https://docs.dfir-iris.org/operations/custom_attributes/">Custom Attributes</a>. This is an operational requirement and not recognized as a vulnerability.</li>
|
||||
<li><b>Try not to be destructive.</b> If you manage to run code on the host server, do not try to go further.</li>
|
||||
</ul>
|
||||
<h3>Restrictions</h3>
|
||||
To keep this demo instance alive, there are some restrictions put in place.
|
||||
<ul>
|
||||
<li>The <code>administrator</code> account cannot be updated nor deleted.</li>
|
||||
<li>The accounts available on this page cannot be updated nor deleted.</li>
|
||||
<li>File upload in datastore is limited to 200KB per file.</li>
|
||||
</ul>
|
||||
<h3>Resources</h3>
|
||||
<p>You can read more about IRIS on the <a target="_blank" rel="noopener" href="https://docs.dfir-iris.org">official documentation website</a>.<br/>
|
||||
IRIS is an open source app, so you can directly access the code on <a target="_blank" rel="noopener" href="https://github.com">GitHub</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse" id="collapseAccounts">
|
||||
<div class="card card-body">
|
||||
<h3 class="mt-2">Accounts</h3>
|
||||
The following accounts are available on the instance. These users cannot be updated or deleted. However, new users and groups can be created.<br/>
|
||||
<b>If the passwords are not working, please double check spaces were not added while copying.</b>
|
||||
<table class="table table-striped table-hover responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Password</th>
|
||||
<th>Role</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in demo_users %}
|
||||
<tr>
|
||||
<td>{{ user.username }}</td>
|
||||
<td>{{ user.password }}</td>
|
||||
<td>{{ user.role }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="/static/assets/js/core/jquery.3.2.1.min.js"></script>
|
||||
<script src="/static/assets/js/core/bootstrap.min.js"></script>
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user