diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 9246dfbdc..24cf5683d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.4.5 +current_version = 2.4.6 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)(-(?P.*)-(?P\d+))? diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..241493f71 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +# IRIS Source Code +# Copyright (C) 2023 - DFIR-IRIS +# contact@dfir-iris.org +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +name: Continuous Integration +on: push + +jobs: + + tests: + name: API tests + runs-on: ubuntu-22.04 + steps: + - name: Check out iris + uses: actions/checkout@v4 + - name: Build dockers + run: | + # TODO using the environment file from tests to build here. + # I am a bit uneasy with this choice. + # For now this works, but if we come to have different .env files for different tests, it won't anymore. + # Maybe the .env should be split to differentiate the variables used during the build from the variables used at runtime, + # or maybe the docker building phase should also be part of the tests + # and we should build different dockers according to the scenarios? This sounds like an issue to me... + cp tests/data/basic.env .env + docker-compose build + - name: Run tests + working-directory: tests + run: | + python -m venv venv + source venv/bin/activate + pip install -r requirements.txt + PYTHONUNBUFFERED=true python -m unittest --verbose + diff --git a/CODESTYLE.md b/CODESTYLE.md index 9d341fa32..1cc5b25f7 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -2,6 +2,29 @@ If you wish to develop in DFIR-IRIS, please make sure to read the following tips. +## Workflow + +- development is done on branch `develop` + ``` + git switch develop + ``` +- safe and small modifications may be directly performed on branch `develop` +- modifications which either imply more work or are risky, must be performed on a branch of their own + ``` + git switch -c + git push --set-upstream origin + ``` +- when work on the branch is ready to be published, then a pull request (PR) is created from the github interface. + Do not forget to choose `develop` as the base branch (by default it is set to `master`, + more information [here](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request#changing-the-branch-range-and-destination-repository)). + + +### Commits +Try to follow the repository convention: + +- If it's not linked to an issue, use the format `[action] Commit message`, with `action` being a 3 letters action related to the commit, eg `ADD`for additions, `DEL` for deletions, `IMP` for improvements, etc. +- If it's linked to an issue, prepend with the issue ID, i.e `[#issue_id][action] Commit message` + ## License header New files should be prefixed by the following license header, where `${current_year}` is replaced by the current year @@ -33,12 +56,6 @@ New files should be prefixed by the following license header, where `${current_y #!/usr/bin/env python3 ``` -## Commits -Try to follow the repository convention : - -- If it's not linked to an issue, use the format `[action] Commit message`, with `action` being a 3 letters action related to the commit, eg `ADD`for additions, `DEL` for deletions, `IMP` for improvements, etc. -- If it's linked to an issue, prepend with the issue ID, i.e `[#issue_id][action] Commit message` - ## Code The code should be pretty easy to apprehend. It's not perfect but it will improve over time. Some documentation about development is available [here](https://docs.dfir-iris.org/development/). diff --git a/README.md b/README.md index b69a7ea4d..cd0d2fc28 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

Incident Response Investigation System
- Current Version v2.4.5 + Current Version v2.4.6
Online Demonstration

@@ -52,7 +52,7 @@ git clone https://github.com/dfir-iris/iris-web.git cd iris-web # Checkout to the last tagged version -git checkout v2.4.5 +git checkout v2.4.6 # Copy the environment file cp .env.model .env diff --git a/docker-compose.yml b/docker-compose.yml index eb90f3ccb..dd1a4a921 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,7 @@ services: rabbitmq: image: rabbitmq:3-management-alpine container_name: iriswebapp_rabbitmq + restart: always networks: - iris_backend @@ -27,7 +28,7 @@ services: build: context: docker/db container_name: iriswebapp_db - image: iriswebapp_db:v2.4.5 + image: iriswebapp_db:v2.4.6 restart: always # Used for debugging purposes, should be deleted for production ports: @@ -47,7 +48,7 @@ services: build: context: . dockerfile: docker/webApp/Dockerfile - image: iriswebapp_app:v2.4.5 + image: iriswebapp_app:v2.4.6 container_name: iriswebapp_app command: ['nohup', './iris-entrypoint.sh', 'iriswebapp'] volumes: @@ -85,8 +86,9 @@ services: build: context: . dockerfile: docker/webApp/Dockerfile - image: iriswebapp_app:v2.4.5 + image: iriswebapp_app:v2.4.6 container_name: iriswebapp_worker + restart: always command: ['./wait-for-iriswebapp.sh', 'app:8000', './iris-entrypoint.sh', 'iris-worker'] volumes: - ./certificates/rootCA/irisRootCACert.pem:/etc/irisRootCACert.pem:ro @@ -121,7 +123,7 @@ services: args: NGINX_CONF_GID: 1234 NGINX_CONF_FILE: nginx.conf - image: iriswebapp_nginx:v2.4.5 + image: iriswebapp_nginx:v2.4.6 container_name: iriswebapp_nginx environment: - IRIS_UPSTREAM_SERVER @@ -137,7 +139,7 @@ services: - "${INTERFACE_HTTPS_PORT:-443}:${INTERFACE_HTTPS_PORT:-443}" volumes: - "./certificates/web_certificates/:/www/certs/:ro" - restart: on-failure:5 + restart: always depends_on: - "app" diff --git a/docker/db/Dockerfile b/docker/db/Dockerfile index 26968c6b0..1449b99cb 100644 --- a/docker/db/Dockerfile +++ b/docker/db/Dockerfile @@ -17,6 +17,6 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -FROM postgres:12-alpine +FROM postgres:16-alpine COPY create_user.sh /docker-entrypoint-initdb.d/10-create_user.sh \ No newline at end of file diff --git a/source/app/__init__.py b/source/app/__init__.py index 2f0fb4606..cbc4e8233 100644 --- a/source/app/__init__.py +++ b/source/app/__init__.py @@ -90,6 +90,8 @@ def ac_current_user_has_manage_perms(): 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.jinja_options["autoescape"] = lambda _: True +app.jinja_env.autoescape = True app.config.from_object('app.configuration.Config') diff --git a/source/app/alembic/versions/9e4947a207a6_add_modification_history_to_case_objects.py b/source/app/alembic/versions/9e4947a207a6_add_modification_history_to_case_objects.py new file mode 100644 index 000000000..29eb71a07 --- /dev/null +++ b/source/app/alembic/versions/9e4947a207a6_add_modification_history_to_case_objects.py @@ -0,0 +1,40 @@ +"""Add modification history to case objects + +Revision ID: 9e4947a207a6 +Revises: 35c095f8be2b +Create Date: 2024-02-16 15:22:17.780516 + +""" +from alembic import op +import sqlalchemy as sa + +from app.alembic.alembic_utils import _table_has_column + +# revision identifiers, used by Alembic. +revision = '9e4947a207a6' +down_revision = '35c095f8be2b' +branch_labels = None +depends_on = None + + +def upgrade(): + tables = ['ioc', 'case_assets', 'case_received_file', 'case_tasks', 'notes', 'cases_events'] + for table in tables: + if not _table_has_column(table, 'modification_history'): + op.add_column(table, + sa.Column('modification_history', sa.JSON) + ) + t_ua = sa.Table( + table, + sa.MetaData(), + sa.Column('modification_history', sa.JSON) + ) + conn = op.get_bind() + conn.execute(t_ua.update().values( + modification_history={} + )) + pass + + +def downgrade(): + pass diff --git a/source/app/blueprints/alerts/alerts_routes.py b/source/app/blueprints/alerts/alerts_routes.py index c1a4bd59b..dc841709b 100644 --- a/source/app/blueprints/alerts/alerts_routes.py +++ b/source/app/blueprints/alerts/alerts_routes.py @@ -241,6 +241,9 @@ def alerts_get_route(caseid, alert_id) -> Response: if alert is None: return response_error('Alert not found') + if not user_has_client_access(current_user.id, alert.alert_customer_id): + return response_error('Alert not found') + alert_dump = alert_schema.dump(alert) # Get similar alerts @@ -271,6 +274,9 @@ def alerts_similarities_route(caseid, alert_id) -> Response: if alert is None: return response_error('Alert not found') + if not user_has_client_access(current_user.id, alert.alert_customer_id): + return response_error('Alert not found') + open_alerts = request.args.get('open-alerts', 'false').lower() == 'true' open_cases = request.args.get('open-cases', 'false').lower() == 'true' closed_cases = request.args.get('closed-cases', 'false').lower() == 'true' @@ -312,6 +318,9 @@ def alerts_update_route(alert_id, caseid) -> Response: if not alert: return response_error('Alert not found') + if not user_has_client_access(current_user.id, alert.alert_customer_id): + return response_error('User not entitled to update alerts for the client', status=403) + alert_schema = AlertSchema() do_resolution_hook = False @@ -339,10 +348,6 @@ def alerts_update_route(alert_id, caseid) -> Response: activity_data.append(f"\"{key}\"") - # Check if the user has access to the client - if not user_has_client_access(current_user.id, alert.alert_customer_id): - return response_error('User not entitled to update alerts for the client', status=403) - # Deserialize the JSON data into an Alert object updated_alert = alert_schema.load(data, instance=alert, partial=True) if data.get('alert_owner_id') is None and updated_alert.alert_owner_id is None: @@ -922,6 +927,9 @@ def alert_comment_modal(cur_id, caseid, url_redir): if not alert: return response_error('Invalid alert ID') + if not user_has_client_access(current_user.id, alert.alert_customer_id): + return response_error('User not entitled to update alerts for the client', status=403) + return render_template("modal_conversation.html", element_id=cur_id, element_type='alerts', title=f" alert #{alert.alert_id}") diff --git a/source/app/blueprints/case/case_notes_routes.py b/source/app/blueprints/case/case_notes_routes.py index 8e9816298..def16b0e6 100644 --- a/source/app/blueprints/case/case_notes_routes.py +++ b/source/app/blueprints/case/case_notes_routes.py @@ -48,7 +48,7 @@ from app.schema.marshables import CaseNoteDirectorySchema from app.schema.marshables import CaseNoteSchema from app.schema.marshables import CommentSchema -from app.util import ac_api_case_requires, ac_socket_requires, endpoint_deprecated +from app.util import ac_api_case_requires, ac_socket_requires, endpoint_deprecated, add_obj_history_entry from app.util import ac_case_requires from app.util import response_error from app.util import response_success @@ -168,6 +168,7 @@ def case_note_save(cur_id, caseid): note.update_date = datetime.utcnow() note.user_id = current_user.id + add_obj_history_entry(note, 'updated note', commit=True) note = call_modules_hook('on_postload_note_update', data=note, caseid=caseid) except marshmallow.exceptions.ValidationError as e: @@ -202,6 +203,8 @@ def case_note_add(caseid): db.session.add(new_note) db.session.commit() + add_obj_history_entry(new_note, 'created note', commit=True) + new_note = call_modules_hook('on_postload_note_create', data=new_note, caseid=caseid) if new_note: diff --git a/source/app/blueprints/case/case_timeline_routes.py b/source/app/blueprints/case/case_timeline_routes.py index 2bbcd6f97..b51647320 100644 --- a/source/app/blueprints/case/case_timeline_routes.py +++ b/source/app/blueprints/case/case_timeline_routes.py @@ -59,6 +59,7 @@ from app.datamgmt.states import update_timeline_state from app.forms import CaseEventForm from app.iris_engine.module_handler.module_handler import call_modules_hook +from app.iris_engine.utils.collab import collab_notify from app.iris_engine.utils.common import parse_bf_date_format from app.iris_engine.utils.tracker import track_activity from app.models import CompromiseStatus @@ -682,6 +683,8 @@ def case_delete_event(cur_id, caseid): call_modules_hook('on_postload_event_delete', data=cur_id, caseid=caseid) + collab_notify(caseid, 'events', 'deletion', cur_id) + track_activity(f"deleted event \"{event.event_title}\" in timeline", caseid) return response_success('Event ID {} deleted'.format(cur_id)) @@ -697,6 +700,8 @@ def event_flag(cur_id, caseid): event.event_is_flagged = not event.event_is_flagged db.session.commit() + collab_notify(caseid, 'events', 'flagged' if event.event_is_flagged else "un-flagged", cur_id) + return response_success("Event flagged" if event.event_is_flagged else "Event unflagged", data=event) @@ -804,7 +809,14 @@ def case_edit_event(cur_id, caseid): event = call_modules_hook('on_postload_event_update', data=event, caseid=caseid) track_activity(f"updated event \"{event.event_title}\"", caseid=caseid) - return response_success("Event updated", data=event_schema.dump(event)) + event_dump = event_schema.dump(event) + collab_notify(case_id=caseid, + object_type='events', + action_type='updated', + object_id=cur_id, + object_data=event_dump) + + return response_success("Event updated", data=event_dump) except marshmallow.exceptions.ValidationError as e: return response_error(msg="Data error", data=e.normalized_messages(), status=400) diff --git a/source/app/blueprints/case/templates/case_notes_v2.html b/source/app/blueprints/case/templates/case_notes_v2.html index 7426494df..253693137 100644 --- a/source/app/blueprints/case/templates/case_notes_v2.html +++ b/source/app/blueprints/case/templates/case_notes_v2.html @@ -59,40 +59,45 @@

Right-click on a directory

- + + + + + +
-
- - -
-
- - - - - -
+
+ + +
+
+ + + + + +
@@ -163,7 +168,26 @@
- + {% include 'includes/footer.html' %} diff --git a/source/app/blueprints/case/templates/case_timeline.html b/source/app/blueprints/case/templates/case_timeline.html index c14788584..94bd256b2 100644 --- a/source/app/blueprints/case/templates/case_timeline.html +++ b/source/app/blueprints/case/templates/case_timeline.html @@ -180,6 +180,7 @@

Upload events list (CSV format)
{% include 'includes/footer_case.html' %} + {% endblock javascripts %} \ No newline at end of file diff --git a/source/app/blueprints/datastore/datastore_routes.py b/source/app/blueprints/datastore/datastore_routes.py index 655572aec..90a49056d 100644 --- a/source/app/blueprints/datastore/datastore_routes.py +++ b/source/app/blueprints/datastore/datastore_routes.py @@ -112,6 +112,22 @@ def datastore_add_file_modal(cur_id: int, caseid: int, url_redir: bool): return render_template("modal_ds_file.html", form=form, file=None, dsp=dsp) +@datastore_blueprint.route('/datastore/file/add//multi-modal', methods=['GET']) +@ac_case_requires(CaseAccessLevel.full_access) +def datastore_add_multi_files_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_multi_files.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): diff --git a/source/app/blueprints/datastore/templates/modal_ds_multi_files.html b/source/app/blueprints/datastore/templates/modal_ds_multi_files.html new file mode 100644 index 000000000..434010956 --- /dev/null +++ b/source/app/blueprints/datastore/templates/modal_ds_multi_files.html @@ -0,0 +1,92 @@ + + + \ No newline at end of file diff --git a/source/app/blueprints/manage/manage_cases_routes.py b/source/app/blueprints/manage/manage_cases_routes.py index c1ac93226..e14dd9a1e 100644 --- a/source/app/blueprints/manage/manage_cases_routes.py +++ b/source/app/blueprints/manage/manage_cases_routes.py @@ -31,6 +31,7 @@ from flask_login import current_user from flask_wtf import FlaskForm from werkzeug import Response +from werkzeug.utils import secure_filename import app from app import db @@ -452,7 +453,9 @@ def update_case_info(cur_id, caseid): if not user_has_client_access(current_user.id, request_data.get('case_customer')): return response_error("Invalid customer ID. Permission denied.", status=403) - request_data['case_name'] = f"#{case_i.case_id} - {request_data.get('case_name').replace(f'#{case_i.case_id} - ', '')}" + if 'case_name' in request_data: + short_case_name = request_data.get('case_name').replace(f'#{case_i.case_id} - ', '') + request_data['case_name'] = f'#{case_i.case_id} - {short_case_name}' request_data['case_customer'] = case_i.client_id if not request_data.get('case_customer') else request_data.get('case_customer') request_data['reviewer_id'] = None if request_data.get('reviewer_id') == "" else request_data.get('reviewer_id') @@ -631,6 +634,7 @@ def manage_cases_uploadfiles(caseid): create=is_update ) + f.filename = secure_filename(f.filename) status = mod.pipeline_files_upload(fpath, f, case_customer, case_name, is_update) if status.is_success(): diff --git a/source/app/configuration.py b/source/app/configuration.py index 86e87e59c..a93e5d0df 100644 --- a/source/app/configuration.py +++ b/source/app/configuration.py @@ -263,7 +263,7 @@ class CeleryConfig: # --------- APP --------- class Config: # Handled by bumpversion - IRIS_VERSION = "v2.4.5" # DO NOT EDIT THIS LINE MANUALLY + IRIS_VERSION = "v2.4.6" # DO NOT EDIT THIS LINE MANUALLY if os.environ.get('IRIS_DEMO_VERSION') is not None and os.environ.get('IRIS_DEMO_VERSION') != 'None': IRIS_VERSION = os.environ.get('IRIS_DEMO_VERSION') diff --git a/source/app/datamgmt/case/case_db.py b/source/app/datamgmt/case/case_db.py index cdb2f5dfb..0174444cb 100644 --- a/source/app/datamgmt/case/case_db.py +++ b/source/app/datamgmt/case/case_db.py @@ -109,8 +109,11 @@ def get_case_report_template(): CaseTemplateReport.name, Languages.name, CaseTemplateReport.description - ).filter( + ).filter(and_( + Languages.id == CaseTemplateReport.report_type_id, ReportType.name == "Investigation" + )).join( + CaseTemplateReport.report_type ).all() return reports @@ -147,9 +150,10 @@ def get_activities_report_template(): CaseTemplateReport.name, Languages.name, CaseTemplateReport.description - ).filter( - ReportType.name == "Activities" - ).all() + ).filter(and_( + ReportType.name == "Activities", + Languages.id == CaseTemplateReport.language_id + )).all() return reports diff --git a/source/app/iris_engine/reporter/reporter.py b/source/app/iris_engine/reporter/reporter.py index 89454bf68..bcc791249 100644 --- a/source/app/iris_engine/reporter/reporter.py +++ b/source/app/iris_engine/reporter/reporter.py @@ -28,8 +28,10 @@ from datetime import datetime import jinja2 +from jinja2.sandbox import SandboxedEnvironment from app.datamgmt.reporter.report_db import export_case_json_for_report +from app.iris_engine.utils.common import IrisJinjaEnv from docx_generator.docx_generator import DocxGenerator from docx_generator.exceptions import rendering_error from flask_login import current_user @@ -100,7 +102,8 @@ def _get_activity_info(self): 'gen_user': current_user.name, 'case': {'name': case_info_in['case'].get('name'), 'open_date': case_info_in['case'].get('open_date'), - 'for_customer': case_info_in['case'].get('for_customer') + 'for_customer': case_info_in['case'].get('client').get('customer_name'), + 'client': case_info_in['case'].get('client') }, 'doc_id': doc_id } @@ -562,14 +565,10 @@ def generate_md_report(self, doc_type): output_file_path = os.path.join(self._tmp, name) try: - # Load the template - template_loader = jinja2.FileSystemLoader(searchpath="/") - template_env = jinja2.Environment(loader=template_loader, autoescape=True) - template_env.filters = app.jinja_env.filters - template = template_env.get_template(os.path.join( - app.config['TEMPLATES_PATH'], report.internal_reference)) - - # Render with a mapping between JSON (from db) and template tags + env = IrisJinjaEnv() + env.filters = app.jinja_env.filters + template = env.from_string( + open(os.path.join(app.config['TEMPLATES_PATH'], report.internal_reference)).read()) output_text = template.render(case_info) # Write the result in the output file diff --git a/source/app/iris_engine/utils/collab.py b/source/app/iris_engine/utils/collab.py new file mode 100644 index 000000000..f3334fb51 --- /dev/null +++ b/source/app/iris_engine/utils/collab.py @@ -0,0 +1,33 @@ +import json +from flask_socketio import join_room + +import app +from app.models.authorization import CaseAccessLevel +from app.util import ac_socket_requires + + +def collab_notify(case_id: int, + object_type: str, + action_type: str, + object_id, + object_data: json = None, + request_sid: int = None + ): + room = f"case-{case_id}" + app.socket_io.emit('case-obj-notif', + json.dumps({ + 'object_id': object_id, + 'action_type': action_type, + 'object_type': object_type, + 'object_data': object_data + }), + room=room, + to=room, + skip_sid=request_sid) + + +@app.socket_io.on('join-case-obj-notif') +@ac_socket_requires(CaseAccessLevel.full_access) +def socket_join_case_obj_notif(data): + room = data['channel'] + join_room(room=room) diff --git a/source/app/iris_engine/utils/common.py b/source/app/iris_engine/utils/common.py index 44cda66ae..31c2dbfbd 100644 --- a/source/app/iris_engine/utils/common.py +++ b/source/app/iris_engine/utils/common.py @@ -17,6 +17,7 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import os from datetime import datetime +from jinja2.sandbox import SandboxedEnvironment from app import app @@ -109,3 +110,24 @@ def parse_bf_date_format(input_str): pass return None + + +class IrisJinjaEnv(SandboxedEnvironment): + + def is_safe_attribute(self, obj, attr, value): + # Extend the list of blocked attributes with magic methods and other potential unsafe attributes + unsafe_attributes = [ + 'os', 'subprocess', 'eval', 'exec', 'open', 'input', '__import__', + '__class__', '__bases__', '__mro__', '__subclasses__', '__globals__' + ] + # Block access to all attributes starting and ending with double underscores + if attr in unsafe_attributes or attr.startswith('__') and attr.endswith('__'): + return False + return super().is_safe_attribute(obj, attr, value) + + def call(self, obj, *args, **kwargs): + # Block calling of functions if necessary + # For example, block if obj is a built-in function or method + if isinstance(obj, (type,)): + raise Exception("Calling of built-in types is not allowed.") + return super().call(obj, *args, **kwargs) \ No newline at end of file diff --git a/source/app/models/models.py b/source/app/models/models.py index 1ab9fe11e..09538b998 100644 --- a/source/app/models/models.py +++ b/source/app/models/models.py @@ -187,6 +187,8 @@ class CaseAssets(db.Model): analysis_status_id = Column(ForeignKey('analysis_status.id')) custom_attributes = Column(JSON) asset_enrichment = Column(JSONB) + modification_history = Column(JSON) + case = relationship('Cases') user = relationship('User') @@ -411,6 +413,7 @@ class Ioc(db.Model): ioc_tlp_id = Column(ForeignKey('tlp.tlp_id')) custom_attributes = Column(JSON) ioc_enrichment = Column(JSONB) + modification_history = Column(JSON) user = relationship('User') tlp = relationship('Tlp') @@ -532,6 +535,7 @@ class Notes(db.Model): note_case_id = Column(ForeignKey('cases.case_id')) custom_attributes = Column(JSON) directory_id = Column(ForeignKey('note_directory.id'), nullable=True) + modification_history = Column(JSON) user = relationship('User') case = relationship('Cases') @@ -606,6 +610,7 @@ class CaseReceivedFile(db.Model): type_id = Column(ForeignKey('evidence_type.id')) custom_attributes = Column(JSON) chain_of_custody = Column(JSON) + modification_history = Column(JSON) case = relationship('Cases') user = relationship('User') @@ -638,6 +643,7 @@ class CaseTasks(db.Model): task_status_id = Column(ForeignKey('task_status.id')) task_case_id = Column(ForeignKey('cases.case_id')) custom_attributes = Column(JSON) + modification_history = Column(JSON) case = relationship('Cases') user_open = relationship('User', foreign_keys=[task_userid_open]) diff --git a/source/app/static/assets/css/atlantis.css b/source/app/static/assets/css/atlantis.css index b0baa94f5..e6e7d3091 100644 --- a/source/app/static/assets/css/atlantis.css +++ b/source/app/static/assets/css/atlantis.css @@ -6429,6 +6429,9 @@ label.error { .dropdown-item { font-size: 13px; + width: inherit; + margin-left: 3px; + margin-right: 3px; } .navbar .navbar-nav .notification { @@ -8447,7 +8450,7 @@ body .badge-danger { background: #f4f4f4; margin-bottom: 10px; left: 0; - box-shadow: 2px 6px 15px 4px rgba(69, 65, 78, 0.29); + box-shadow: 1px 1px 20px 0px rgba(69, 65, 78, 0.18); } .timeline-t>.timeline-child>.timeline-odd-t { @@ -8455,17 +8458,17 @@ body .badge-danger { background: #f4f4f4; margin: 0 0 10px; left: 0; - box-shadow: 2px 6px 15px 4px rgba(69, 65, 78, 0.29); + box-shadow: 1px 1px 20px 0px rgba(69, 65, 78, 0.18); } .timeline-t>li>.timeline-panel-t { width: 50%; background: #ffffff; - border-radius: 1.5rem; - padding: 18px; - margin-bottom: 10px; + border-radius: 18px; + margin-bottom: 9px; position: relative; - box-shadow: 2px 6px 15px 0px rgba(69, 65, 78, 0.29); + box-shadow: 1px 1px 20px 0px rgba(69, 65, 78, 0.18); + padding: 12px 11px 10px 17px; } .timeline-t:before { @@ -15646,7 +15649,7 @@ div.dataTables_processing { z-index: 1; } box-shadow: 0 0 10px 5px #f96; } to { - box-shadow: 2px 6px 15px 0px rgba(69, 65, 78, 0.29);; + box-shadow: 1px 1px 20px 0px rgba(69, 65, 78, 0.18); } } diff --git a/source/app/static/assets/css/dataTables.contextualActions.min.css b/source/app/static/assets/css/dataTables.contextualActions.min.css index 98acc0d22..7319e48a8 100644 --- a/source/app/static/assets/css/dataTables.contextualActions.min.css +++ b/source/app/static/assets/css/dataTables.contextualActions.min.css @@ -1,2 +1,2 @@ -.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,0.15);border-radius:.25rem;font-family:inherit;box-shadow:0 0 7px rgba(0,0,0,0.18);line-height:1.5}.dropdown-menu .dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-menu .dropdown-item{display:block;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0} +.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,0.15);border-radius:6px;font-family:inherit;box-shadow:0 0 7px rgba(0,0,0,0.18);line-height:1.5}.dropdown-menu .dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-menu .dropdown-item{display:block;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0} .dropdown-menu .dropdown-item:hover{background-color:#e6e6e6}.dropdown-menu .dropdown-header{display:block;margin-top:0;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-menu .btn-group{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.dropdown-menu .btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;border-top-color:transparent;border-right-color:transparent;border-bottom-color:transparent;border-left-color:transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out} \ No newline at end of file diff --git a/source/app/static/assets/js/iris/alerts.js b/source/app/static/assets/js/iris/alerts.js index c41c95f71..44ba369c8 100644 --- a/source/app/static/assets/js/iris/alerts.js +++ b/source/app/static/assets/js/iris/alerts.js @@ -991,11 +991,11 @@ function renderAlert(alert, expanded=false, modulesOptionsAlertReq, ` : ''} ${alert.alert_source_event_time ? `
Source Event Time:
-
${alert.alert_source_event_time}
+
${formatTime(alert.alert_source_event_time)} UTC
` : ''} ${alert.alert_creation_time ? `
IRIS Creation Time:
-
${alert.alert_creation_time}
+
${formatTime(alert.alert_creation_time)} UTC
` : ''}
@@ -1186,8 +1186,8 @@ function renderAlert(alert, expanded=false, modulesOptionsAlertReq,
${alert_resolution === undefined ? "": alert_resolution} ${alert.status ? `${alert.status.status_name}` : ''} - - ${alert.alert_source_event_time} + + ${formatTime(alert.alert_source_event_time)} ${alert.severity.severity_name} diff --git a/source/app/static/assets/js/iris/case.common.js b/source/app/static/assets/js/iris/case.common.js index 2ab3d9427..fca96efe7 100644 --- a/source/app/static/assets/js/iris/case.common.js +++ b/source/app/static/assets/js/iris/case.common.js @@ -1,3 +1,5 @@ +let collab_case = null; + function buildShareLink(lookup_id) { current_path = location.protocol + '//' + location.host + location.pathname; current_path = current_path + case_param() + '&shared=' + lookup_id; @@ -27,4 +29,9 @@ $(document).ready(function(){ } }) }); + // Check if io is avalaible + if (typeof io !== 'undefined' && io !== undefined) { + collab_case = io.connect(); + collab_case.emit('join-case-obj-notif', { 'channel': 'case-' + get_caseid() }); + } }); \ No newline at end of file diff --git a/source/app/static/assets/js/iris/case.notes.js b/source/app/static/assets/js/iris/case.notes.js index 71a00dcd0..aae0b4ab3 100644 --- a/source/app/static/assets/js/iris/case.notes.js +++ b/source/app/static/assets/js/iris/case.notes.js @@ -12,6 +12,9 @@ let note_id = null; let last_ping = 0; let cid = null; let previousNoteTitle = null; +let timer = null; +let timeout = 5000; + const preventFormDefaultBehaviourOnSubmit = (event) => { event.preventDefault(); @@ -296,6 +299,15 @@ async function note_detail(id) { previousNoteTitle = data.data.note_title; $('#currentNoteIDLabel').text(`#${data.data.note_id} - ${data.data.note_uuid}`) .data('note_id', data.data.note_id); + let history_data = ''; + for (let ent in data.data.modification_history) { + let entry = data.data.modification_history[ent]; + let dateStr = formatTime(parseFloat(ent)) + let i_t = $('
  • '); + i_t.text(`${dateStr} - ${entry.user} ${entry.action}`); + history_data += i_t.prop('outerHTML'); + } + $('#modalHistoryList').empty().append(history_data); note_editor.on( "change", function( e ) { if( last_applied_change != e && note_editor.curOp && note_editor.curOp.command.name) { @@ -322,6 +334,19 @@ async function note_detail(id) { $('#last_saved').removeClass('btn-danger').addClass('btn-success'); $('#last_saved > i').attr('class', "fa-solid fa-file-circle-check"); + let ed_details = $('#editor_detail'); + ed_details.keyup(function(){ + if(timer) { + clearTimeout(timer); + } + timer = setTimeout(save_note, timeout); + }); + ed_details.off('paste'); + ed_details.on('paste', (event) => { + event.preventDefault(); + handle_ed_paste(event); + }); + setSharedLink(id); return true; @@ -881,6 +906,15 @@ function createDirectoryListItem(directory, directoryMap) { top: e.pageY }); + menu.append($('').addClass('dropdown-item').attr('href', '#').text('Copy link').on('click', function (e) { + e.preventDefault(); + copy_object_link(note.id); + })); + + menu.append($('').addClass('dropdown-item').attr('href', '#').text('Copy MD link').on('click', function (e) { + e.preventDefault(); + copy_object_link_md('notes',note.id); + })); menu.append($('').addClass('dropdown-item').attr('href', '#').text('Move').on('click', function (e) { e.preventDefault(); @@ -918,6 +952,7 @@ function note_interval_pinger() { } $(document).ready(function(){ + load_directories().then( function() { let shared_id = getSharedLink(); diff --git a/source/app/static/assets/js/iris/case.timeline.js b/source/app/static/assets/js/iris/case.timeline.js index 259ffa911..77c7136dc 100644 --- a/source/app/static/assets/js/iris/case.timeline.js +++ b/source/app/static/assets/js/iris/case.timeline.js @@ -509,6 +509,303 @@ function toggleSeeMore(element) { } } +function buildEvent(event_data, compact, comments_map, tree, tesk, tmb, idx, reap, converter) { + let evt = event_data; + let dta = evt.event_date.toString().split('T'); + let tags = ''; + let cats = ''; + let tmb_d = ''; + let style = ''; + let asset = ''; + + if (evt.event_id in comments_map) { + nb_comments = comments_map[evt.event_id].length; + } else { + nb_comments = ''; + } + + if(evt.category_name && evt.category_name != 'Unspecified') { + if (!compact) { + tags += `${sanitizeHTML(evt.category_name)}`; + } else { + if (evt.category_name != 'Unspecified') { + cats += `${sanitizeHTML(evt.category_name)}`; + } + } + } + + if (evt.iocs != null && evt.iocs.length > 0) { + for (let ioc in evt.iocs) { + let span_anchor = $(''); + span_anchor.addClass('badge badge-warning-event float-right ml-1 mt-2'); + span_anchor.attr('data-toggle', 'popover'); + span_anchor.attr('data-trigger', 'hover'); + span_anchor.attr('style', 'cursor: pointer;'); + span_anchor.attr('data-content', 'IOC - ' + evt.iocs[ioc].description); + span_anchor.attr('title', evt.iocs[ioc].name); + span_anchor.text(evt.iocs[ioc].name) + span_anchor.html('' + span_anchor.html()); + tags += span_anchor[0].outerHTML; + } + } + + if (evt.event_tags != null && evt.event_tags.length > 0) { + sp_tag = evt.event_tags.split(','); + for (tag_i in sp_tag) { + tags += get_tag_from_data(sp_tag[tag_i], 'badge badge-light ml-1 float-right mt-2'); + } + } + + let entry = ''; + let inverted = 'timeline'; + let timeline_style = tree ? '-t' : ''; + + /* Do we have a border color to set ? */ + if (tesk) { + style += "timeline-odd"+ timeline_style; + tesk = false; + } else { + style += "timeline-even" + timeline_style; + tesk = true; + } + + let style_s = ""; + if (evt.event_color != null) { + style_s = `style='border-left: 2px groove ${sanitizeHTML(evt.event_color)};'`; + } + + if (!tree) { + inverted += '-inverted'; + } else { + if (tesk) { + inverted += '-inverted'; + } + } + + + /* For every assets linked to the event, build a link tag */ + if (evt.assets != null) { + for (let ide in evt.assets) { + let cpn = evt.assets[ide]["ip"] + ' - ' + evt.assets[ide]["description"] + cpn = sanitizeHTML(cpn) + let span_anchor = $(''); + span_anchor.attr('data-toggle', 'popover'); + span_anchor.attr('data-trigger', 'hover'); + span_anchor.attr('style', 'cursor: pointer;'); + span_anchor.attr('data-content', cpn); + span_anchor.attr('title', evt.assets[ide]["name"]); + span_anchor.text(evt.assets[ide]["name"]); + + if (evt.assets[ide]["compromised"]) { + span_anchor.addClass('badge badge-warning-event float-right ml-1 mt-2'); + } else { + span_anchor.addClass('badge badge-light float-right ml-1 mt-2'); + } + + asset += span_anchor[0].outerHTML; + } + } + + let ori_date = ''; + if (evt.event_date_wtz != evt.event_date) { + ori_date += `` + } + + if(evt.event_in_summary) { + ori_date += `` + } + + if(evt.event_in_graph) { + ori_date += `` + } + + + let day = dta[0]; + // Transform the date to the user's system format. day is in the format YYYY-MM-DD. We want our date in the user's host format, without the minutes and seconds. + // First parse the date to a Date object + let date = new Date(day); + // Then use the toLocaleDateString method to get the date in the user's host format + day = date.toLocaleDateString(); + + let hour = dta[1].split('.')[0]; + + let mtop_day = ''; + + if (!tmb.includes(day) && evt.parent_event_id == null) { + tmb.push(day); + tmb_d = `
  • ${day}
  • `; + + idx += 1; + mtop_day = 'mt-4'; + } + + let title_parsed = match_replace_ioc(sanitizeHTML(evt.event_title), reap); + let raw_content = do_md_filter_xss(evt.event_content); // Raw markdown content + let formatted_content = converter.makeHtml(raw_content); // Convert markdown to HTML + + const wordLimit = 30; // Define your word limit + + if (!compact) { + let paragraphs = raw_content.split('\n\n'); + let short_content, long_content; + + if (paragraphs.join(' ').split(' ').length > wordLimit || paragraphs.length > 2) { + let temp_content = ''; + let i = 0; + let wordCount = 0; + + // Loop until the content length is more than wordLimit or paragraph count is more than 2 + while(wordCount <= wordLimit && i < 2 && i < paragraphs.length){ + let words = paragraphs[i].split(' '); + if (wordCount + words.length > wordLimit && wordCount != 0) { + break; + } + temp_content += paragraphs[i] + '\n\n'; + wordCount += words.length; + i++; + } + + short_content = converter.makeHtml(temp_content); // Convert markdown to HTML + short_content = match_replace_ioc(filterXSS(short_content), reap); + temp_content = paragraphs.slice(i).join('\n\n'); + long_content = converter.makeHtml(temp_content); // Convert markdown to HTML + long_content = match_replace_ioc(filterXSS(long_content), reap); + + formatted_content = short_content + `
    + ${long_content} +
    + `; + } else { + let content_parsed = converter.makeHtml(raw_content); // Convert markdown to HTML + content_parsed = filterXSS(content_parsed); + formatted_content = match_replace_ioc(content_parsed, reap); + } + } + + let shared_link = buildShareLink(evt.event_id); + + let flag = ''; + if (evt.event_is_flagged) { + flag = ``; + } else { + flag = ``; + } + + if (compact) { + entry = `
  • +
    +
    + + +
    +
    +
    +
    + ${formatted_content} +
    +
    + ${tags}${asset} +
    +
    +
    +
    +
  • ` + } else { + entry = `
  • +
    +
    + + +
    +
    + ${formatted_content} + +
    +
    +
    + ${formatTime(evt.event_date)}${ori_date} +
    + +
    + ${tags}${asset} +
    +
    +
    +
    +
    +
  • ` + } + return [entry, tmb_d]; +} + function build_timeline(data) { let compact = is_timeline_compact_view(); let tree = is_timeline_tree_view(); @@ -538,13 +835,13 @@ function build_timeline(data) { {value: 'AND ', score: 10, meta: 'AND operator'} ] - for (rid in data.data.assets) { + for (let rid in data.data.assets) { standard_filters.push( {value: data.data.assets[rid][0], score: 1, meta: data.data.assets[rid][1]} ); } - for (rid in data.data.categories) { + for (let rid in data.data.categories) { standard_filters.push( {value: data.data.categories[rid], score: 1, meta: "Event category"} ); @@ -586,299 +883,13 @@ function build_timeline(data) { let child_events = Object(); for (let index in data.data.tim) { - let evt = data.data.tim[index]; - let dta = evt.event_date.split('T'); - let tags = ''; - let cats = ''; - let tmb_d = ''; - let style = ''; - let asset = ''; - - if (evt.event_id in data.data.comments_map) { - nb_comments = data.data.comments_map[evt.event_id].length; - } else { - nb_comments = ''; - } - - if(evt.category_name && evt.category_name != 'Unspecified') { - if (!compact) { - tags += `${sanitizeHTML(evt.category_name)}`; - } else { - if (evt.category_name != 'Unspecified') { - cats += `${sanitizeHTML(evt.category_name)}`; - } - } - } - - if (evt.iocs != null && evt.iocs.length > 0) { - for (let ioc in evt.iocs) { - let span_anchor = $(''); - span_anchor.addClass('badge badge-warning-event float-right ml-1 mt-2'); - span_anchor.attr('data-toggle', 'popover'); - span_anchor.attr('data-trigger', 'hover'); - span_anchor.attr('style', 'cursor: pointer;'); - span_anchor.attr('data-content', 'IOC - ' + evt.iocs[ioc].description); - span_anchor.attr('title', evt.iocs[ioc].name); - span_anchor.text(evt.iocs[ioc].name) - span_anchor.html('' + span_anchor.html()); - tags += span_anchor[0].outerHTML; - } - } + let evt = data.data.tim[index]; + let eki = buildEvent(evt, compact, data.data.comments_map, tree, tesk, tmb, idx, reap, converter); + tesk = !tesk; - if (evt.event_tags != null && evt.event_tags.length > 0) { - sp_tag = evt.event_tags.split(','); - for (tag_i in sp_tag) { - tags += get_tag_from_data(sp_tag[tag_i], 'badge badge-light ml-1 float-right mt-2'); - } - } - - let entry = ''; - let inverted = 'timeline'; - let timeline_style = tree ? '-t' : ''; - - /* Do we have a border color to set ? */ - if (tesk) { - style += "timeline-odd"+ timeline_style; - tesk = false; - } else { - style += "timeline-even" + timeline_style; - tesk = true; - } - - let style_s = ""; - if (evt.event_color != null) { - style_s = `style='border-left: 2px groove ${sanitizeHTML(evt.event_color)};'`; - } - - if (!tree) { - inverted += '-inverted'; - } else { - if (tesk) { - inverted += '-inverted'; - } - } - - - /* For every assets linked to the event, build a link tag */ - if (evt.assets != null) { - for (ide in evt.assets) { - let cpn = evt.assets[ide]["ip"] + ' - ' + evt.assets[ide]["description"] - cpn = sanitizeHTML(cpn) - let span_anchor = $(''); - span_anchor.attr('data-toggle', 'popover'); - span_anchor.attr('data-trigger', 'hover'); - span_anchor.attr('style', 'cursor: pointer;'); - span_anchor.attr('data-content', cpn); - span_anchor.attr('title', evt.assets[ide]["name"]); - span_anchor.text(evt.assets[ide]["name"]); - - if (evt.assets[ide]["compromised"]) { - span_anchor.addClass('badge badge-warning-event float-right ml-1 mt-2'); - } else { - span_anchor.addClass('badge badge-light float-right ml-1 mt-2'); - } - - asset += span_anchor[0].outerHTML; - } - } - - let ori_date = ''; - if (evt.event_date_wtz != evt.event_date) { - ori_date += `` - } - - if(evt.event_in_summary) { - ori_date += `` - } - - if(evt.event_in_graph) { - ori_date += `` - } - - - let day = dta[0]; - - let hour = dta[1].split('.')[0]; - - let mtop_day = ''; - if (!tmb.includes(day)) { - tmb.push(day); - tmb_d = `
  • ${day}
  • `; - - idx += 1; - mtop_day = 'mt-4'; - } - - let title_parsed = match_replace_ioc(sanitizeHTML(evt.event_title), reap); - let raw_content = do_md_filter_xss(evt.event_content); // Raw markdown content - let formatted_content = converter.makeHtml(raw_content); // Convert markdown to HTML - - const wordLimit = 30; // Define your word limit - - if (!compact) { - let paragraphs = raw_content.split('\n\n'); - let short_content, long_content; - - if (paragraphs.join(' ').split(' ').length > wordLimit || paragraphs.length > 2) { - let temp_content = ''; - let i = 0; - let wordCount = 0; - - // Loop until the content length is more than wordLimit or paragraph count is more than 2 - while(wordCount <= wordLimit && i < 2 && i < paragraphs.length){ - let words = paragraphs[i].split(' '); - if (wordCount + words.length > wordLimit && wordCount != 0) { - break; - } - temp_content += paragraphs[i] + '\n\n'; - wordCount += words.length; - i++; - } - - short_content = converter.makeHtml(temp_content); // Convert markdown to HTML - short_content = match_replace_ioc(filterXSS(short_content), reap); - temp_content = paragraphs.slice(i).join('\n\n'); - long_content = converter.makeHtml(temp_content); // Convert markdown to HTML - long_content = match_replace_ioc(filterXSS(long_content), reap); - - formatted_content = short_content + `
    - ${long_content} -
    - `; - } else { - content_parsed = converter.makeHtml(raw_content); // Convert markdown to HTML - content_parsed = filterXSS(content_parsed); - formatted_content = match_replace_ioc(content_parsed, reap); - } - } - - - - let shared_link = buildShareLink(evt.event_id); - - let flag = ''; - if (evt.event_is_flagged) { - flag = ``; - } else { - flag = ``; - } - - if (compact) { - entry = `
  • -
    -
    - - -
    -
    -
    -
    - ${formatted_content} -
    -
    - ${tags}${asset} -
    -
    -
    -
    -
  • ` - } else { - entry = `
  • -
    -
    - - -
    -
    - ${formatted_content} - -
    -
    -
    - ${render_date(evt.event_date, true)}${ori_date} -
    - -
    - ${tags}${asset} -
    -
    -
    -
    -
    -
  • ` - } is_i = false; - - //entry = match_replace_ioc(entry, reap); - // display the time info as a small box + let entry = eki[0]; + let tmb_d = eki[1]; if (evt.parent_event_id != null) { if (!(evt.parent_event_id in child_events)) { @@ -1073,18 +1084,42 @@ function flag_event(event_id){ get_request_api('timeline/events/flag/'+event_id) .done(function(data) { if (notify_auto_api(data)) { - if (data.data.event_is_flagged == true) { - $('#event_'+event_id).find('.fa-flag').addClass('fas text-warning').removeClass('fa-regular'); - $('#event_210').find('.fa-flag').addClass('fas text-warning').removeClass('fa-regular'); - } else { - $('#event_'+event_id).find('.fa-flag').addClass('fa-regular').removeClass('fas text-warning'); - } + uiFlagEvent(event_id, data.data.event_is_flagged) } }); } +function uiFlagEvent(event_id, is_flagged) { + if (is_flagged === true) { + $('#event_'+event_id).find('.fa-flag').addClass('fas text-warning').removeClass('fa-regular'); + } else { + $('#event_'+event_id).find('.fa-flag').addClass('fa-regular').removeClass('fas text-warning'); + } +} + +function uiRemoveEvent(event_id) { + $('#event_'+event_id).remove(); +} + +function uiUpdateEvent(event_id, event_data) { + let last_event_id = 0; + for (let index in current_timeline){ + let list_date = new Date(current_timeline[index].event_date); + let evt_date = new Date(event_data.event_date); + + if (list_date < evt_date) { + last_event_id = current_timeline[index].event_id; + } + } + if (last_event_id !== 0) { + let updated_event = $(`#event_${event_id}`).html(); + $(`#event_${event_id}`).remove(); + $(`#event_${last_event_id}`).after(updated_event); + } +} + function time_converter(){ - date_val = $('#event_date_convert_input').val(); + let date_val = $('#event_date_convert_input').val(); var data_sent = Object(); data_sent['date_value'] = date_val; @@ -1306,6 +1341,22 @@ function upload_csv_events() { return false; } +function handleCollabNotifications(collab_data) { + if (collab_data.action_type === "flagged") { + uiFlagEvent(collab_data.object_id, true); + } + else if (collab_data.action_type === "un-flagged") { + uiFlagEvent(collab_data.object_id, false); + } + else if (collab_data.action_type === "deletion") { + uiRemoveEvent(collab_data.object_id); + } + // else if (collab_data.action_type === 'updated') { + // uiUpdateEvent(collab_data.object_id, + // collab_data.object_data) + // } +} + function generate_events_sample_csv(){ csv_data = "event_date,event_tz,event_title,event_category,event_content,event_raw,event_source,event_assets,event_iocs,event_tags\n" csv_data += '"2023-03-26T03:00:30.000","+00:00","An event","Unspecified","Event description","raw","source","","","defender|malicious"\n' @@ -1352,5 +1403,12 @@ $(document).ready(function(){ setInterval(function() { check_update('timeline/state'); }, 3000); + collab_case.on('case-obj-notif', function(data) { + let js_data = JSON.parse(data); + if (js_data.object_type === 'events') { + handleCollabNotifications(js_data); + } + }); + }); diff --git a/source/app/static/assets/js/iris/common.js b/source/app/static/assets/js/iris/common.js index 8599403e9..51ac83a74 100644 --- a/source/app/static/assets/js/iris/common.js +++ b/source/app/static/assets/js/iris/common.js @@ -16,6 +16,7 @@ $.fn.serializeObject = function() { var jdata_menu_options = []; +let current_cid = null; function clear_api_error() { $(".invalid-feedback").hide(); @@ -437,10 +438,13 @@ function updateURLParameter(url, param, paramVal) { } function get_caseid() { - queryString = window.location.search; - urlParams = new URLSearchParams(queryString); + if (current_cid === null) { + let queryString = window.location.search; + let urlParams = new URLSearchParams(queryString); - return urlParams.get('cid') + current_cid = urlParams.get('cid') + } + return current_cid } function is_redirect() { @@ -993,7 +997,7 @@ function get_avatar_initials(name, small, onClickFunction, xsmall) { snum = initial[0][0].charCodeAt(0); } - const initials = initial.map(i => i[0].toUpperCase()).join(''); + const initials = initial.map(i => i[0] ? i[0].toUpperCase(): '').join(''); const avatarColor = get_avatar_color(snum); const avatarHTMLin = `${initials}` @@ -1262,7 +1266,17 @@ function get_custom_attributes_fields() { } function update_time() { - $('#current_date').text((new Date()).toLocaleString().slice(0, 17)); + $('#current_date').text((new Date()).toLocaleString()); +} + +function formatTime(in_, format) { + if (typeof(in_) === typeof(1)){ + let date = new Date(Math.floor(in_) * 1000); + return date.toLocaleString(undefined, format); + } else if (typeof(in_) === typeof('')) { + let date = new Date(in_); + return date.toLocaleString(undefined, format); + } } function download_file(filename, contentType, data) { diff --git a/source/app/static/assets/js/iris/datastore.js b/source/app/static/assets/js/iris/datastore.js index 39b659d7b..e98ee056c 100644 --- a/source/app/static/assets/js/iris/datastore.js +++ b/source/app/static/assets/js/iris/datastore.js @@ -70,6 +70,7 @@ function build_ds_tree(data, tree_node) {