Repository: dfir-iris/iris-web
Pull request #776: Api v2 delete event
Author: login: c8y3
Created: 2025-04-11T12:47:06Z | Merged: 2025-04-15T06:42:11Z

Implement endpoint `DELETE /api/v2/cases/{case_identifier}/events/{identifier}` to delete an evidence.

* tests
* deprecated `POST /case/timeline/events/delete/{event_id}`
* fix: `DELETE /api/v2/cases/{case_identifier}/evidences/{identifier}` should not delete evidence, when user has no permission to access case
* deprecated create and update events preload hooks
* removed the adherence of the event business layer with `marshmallow` and the schema (moved up in the API)
* created class `Events` so as to avoid to duplicate and recreate the `EventSchema` several times in the code (and at runtime)
* removed endpoint `GET /manage/server/check-updates/modal`
* removed endpoint `GET /manage/server/make-update`
* fixed all remaining occurrences and activate ruff rule [RET506](https://docs.astral.sh/ruff/rules/superfluous-else-raise/)
* moved some methods from `app.util` closer to their use in namespace `app.schema.utils`

Note: this pull request goes hand in hand with the documentation [PR#56](https://github.com/dfir-iris/iris-doc-src/pull/56) in iris-doc-src.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Bug Fixes**
  - Improved deletion processes with enhanced access controls and clearer error responses for event and evidence removal.
  
- **Refactor**
  - Streamlined event management and removed outdated server update interfaces for a more reliable experience.
  
- **Tests**
  - Expanded test coverage to ensure consistent behavior during deletion operations and proper access validations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/pyproject.toml b/pyproject.toml
index 6d8f98c97..d86ff9c0f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,5 +1,5 @@
 [tool.ruff.lint]
 preview = true
-select = ["E225", "E23", "E24", "E3", "E4", "E7", "E9", "F", "UP032", "W29"]
+select = ["E225", "E23", "E24", "E3", "E4", "E7", "E9", "F", "RET506", "UP032", "W29"]
 ignore = ["E402", "E711", "E712", "E721", "E722"]
 
diff --git a/source/app/blueprints/pages/manage/manage_srv_settings_routes.py b/source/app/blueprints/pages/manage/manage_srv_settings_routes.py
index 925c2df87..5cc0d66d7 100644
--- a/source/app/blueprints/pages/manage/manage_srv_settings_routes.py
+++ b/source/app/blueprints/pages/manage/manage_srv_settings_routes.py
@@ -25,7 +25,6 @@
 from app import app
 from app.datamgmt.manage.manage_srv_settings_db import get_alembic_revision
 from app.datamgmt.manage.manage_srv_settings_db import get_srv_settings
-from app.iris_engine.updater.updater import is_updates_available
 from app.models.authorization import Permissions
 from app.blueprints.access_controls import ac_requires
 
@@ -36,28 +35,6 @@
 )
 
 
-@manage_srv_settings_blueprint.route('/manage/server/make-update', methods=['GET'])
-@ac_requires(Permissions.server_administrator, no_cid_required=True)
-def manage_update(caseid, url_redir):
-    if url_redir:
-        return redirect(url_for('manage_srv_settings_blueprint.manage_settings', cid=caseid))
-
-    # Return default page of case management
-    return render_template('manage_make_update.html')
-
-
-@manage_srv_settings_blueprint.route('/manage/server/check-updates/modal', methods=['GET'])
-@ac_requires(Permissions.server_administrator, no_cid_required=True)
-def manage_check_updates_modal(caseid, url_redir):
-    if url_redir:
-        return redirect(url_for('manage_srv_settings_blueprint.manage_settings', cid=caseid))
-
-    has_updates, updates_content, _ = is_updates_available()
-
-    # Return default page of case management
-    return render_template('modal_server_updates.html', has_updates=has_updates, updates_content=updates_content)
-
-
 @manage_srv_settings_blueprint.route('/manage/settings', methods=['GET'])
 @ac_requires(Permissions.server_administrator, no_cid_required=True)
 def manage_settings(caseid, url_redir):
diff --git a/source/app/blueprints/pages/manage/templates/manage_make_update.html b/source/app/blueprints/pages/manage/templates/manage_make_update.html
deleted file mode 100644
index 6f3a95d4d..000000000
--- a/source/app/blueprints/pages/manage/templates/manage_make_update.html
+++ /dev/null
@@ -1,120 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-
-	<meta http-equiv="X-UA-Compatible" content="IE=edge" />
-	<title>IRIS - Server updates</title>
-	<meta content='width=device-width, initial-scale=1.0, shrink-to-fit=no' name='viewport' />
-	<link rel="icon" href="/static/assets/img/logo.ico" type="image/x-icon"/>
-
-	<!-- Fonts and icons -->
-	<script src="/static/assets/js/plugin/webfont/webfont.min.js"></script>
-	<script>
-		WebFont.load({
-			custom: {"families":["Lato:300,400,700,900", "Flaticon", "Font Awesome 5 Solid", "Font Awesome 5 Regular", "Font Awesome 5 Brands", "simple-line-icons"],
-			urls: ['/static/assets/css/fonts.css']},
-			active: function() {
-				sessionStorage.fonts = true;
-			}
-		});
-	</script>
-
-	<!-- CSS Files -->
-	<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/bootstrap-select.min.css">
-
-</head>
-<body class="bg-dark-gradient p-4" style="height: 100vh;">
-    <div class="container bg-white shadow-lg p-3 rounded mb-4">
-			<div class="mt-2 row">
-				<div class="col d-flex justify-content-center">
-					<a href="/" class="logo ml-2 text-center">
-						<img src="/static/assets/img/logo-alone-2-black.png" alt="navbar brand" class='image-update navbar-brand' width="50">
-					</a>
-				</div>
-			</div>
-			<div class="mt-2 row">
-				<div class="col d-flex justify-content-center">
-					<h1 class="text-dark text-center">Server Updates</h1>
-				</div>
-			</div>
-			<div class="row ">
-				<div class="col d-flex justify-content-center">
-					<i class="text-dark">Some great improvements are hopefully on their way, hang tight</i>
-				</div>
-			</div>
-			<div class="row mt-4">
-				<div class="col d-flex justify-content-center">
-					<h4 class="text-danger text-center">Please do not leave or refresh the page once updates have started</h4>
-				</div>
-			</div>
-			<div class="row mt-4 update_start_txt">
-				<div class="col pl-4 justify-content-center">
-					<p class="text-center">Ensure users are disconnected. Any unsaved work might be lost.</p>
-				</div>
-			</div>
-			<div class="row mt-4 justify-content-center update_start_txt">
-				<div class="col-6">
-					<p class="text-center">The following steps might occur during updates, depending on the updates scope : </p>
-					<ul>
-						<li>Fetch of updates information from github.com</li>
-						<li>Download of updates assets from github</li>
-						<li>Signatures verification</li>
-						<li>Compatibility verifications</li>
-						<li>Back up of the current server code to backup storage defined in <code>BACKUP_PATH</code></li>
-						<li>Server stop and update of application</li>
-						<li>Server restarts</li>
-						<li>Version check</li>
-					</ul>
-				</div>
-			</div>
-			<div class="row mt-4">
-				<div class="col d-flex justify-content-center ">
-					<a href='#' onclick="start_updates();" class="btn btn-outline-success text-center" id="update_start_btn">Start updates</a>
-				</div>
-			</div>
-			<div id="container-updates" class="row shadow-lg ml-4 mr-4 rounded"  style="display:none;">
-				<div class="col justify-content-left text-dark pl-4 pb-4 pt-4 pr-4 mh-100">
-					<div class="col">
-						<div id="updates_log">
-						</div>
-						<div id="updates_log_end"></div>
-					</div>
-				</div>
-			</div>
-			<div class="row text-dark">
-				<div class="col d-flex justify-content-center">
-					<div id="offline_time" style="display:none;"></div>
-				</div>
-			</div>
-			<div class="row mt-4">
-				<div class="col d-flex justify-content-center">
-					<div id="tag_bottom" style="display:none;" class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
-					<a href='/manage/settings' class="btn btn-dark ml-mr-auto" style="display:none;" id="update_return_button">Go back to server</a>
-				</div>
-			</div>
-	</div>
-
-	<!--   Core JS Files   -->
-
-	<script src="/static/assets/js/core/jquery.3.2.1.min.js"></script>
-	<script src="/static/assets/js/core/popper.min.js"></script>
-	<script src="/static/assets/js/core/bootstrap.min.js"></script>
-
-	<!-- jQuery UI -->
-	<script src="/static/assets/js/plugin/jquery-ui-1.12.1.custom/jquery-ui.min.js"></script>
-	<script src="/static/assets/js/plugin/jquery-ui-touch-punch/jquery.ui.touch-punch.min.js"></script>
-
-	<!-- jQuery Scrollbar -->
-	<script src="/static/assets/js/plugin/jquery-scrollbar/jquery.scrollbar.min.js"></script>
-
-	<!-- Atlantis JS -->
-	<script src="/static/assets/js/atlantis.min.js"></script>
-	{% if current_user.in_dark_mode %}<script src="/static/assets/js/dark-mode.js"></script>{% endif %}
-
-    <script src="/static/assets/js/core/socket.io.js"></script>
-    <script src="/static/assets/js/iris/updates.handler.js"></script>
-
-</body>
-</html>
diff --git a/source/app/blueprints/pages/manage/templates/modal_server_updates.html b/source/app/blueprints/pages/manage/templates/modal_server_updates.html
deleted file mode 100644
index 33cc39536..000000000
--- a/source/app/blueprints/pages/manage/templates/modal_server_updates.html
+++ /dev/null
@@ -1,21 +0,0 @@
-<div class="modal-header">
-    <h4 class="modal-title mt-2 mr-4">Server Updates</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="col-md-12" id="updates_content_md">
-{{ updates_content }}
-          </div>
-    </div>
-</div>
-<div class="modal-footer">
-
-    <button type="button" class="btn btn-default" data-dismiss="modal">Dismiss</button>
-
-    {% if has_updates %}
-        <a type="button" class="btn btn-success" href="/manage/server/make-update?cid={{session['current_case'].case_id}}">Update server</a>
-    {% endif %}
-
-</div>
\ No newline at end of file
diff --git a/source/app/blueprints/rest/case/case_timeline_routes.py b/source/app/blueprints/rest/case/case_timeline_routes.py
index af08122e2..a09862d28 100644
--- a/source/app/blueprints/rest/case/case_timeline_routes.py
+++ b/source/app/blueprints/rest/case/case_timeline_routes.py
@@ -21,7 +21,7 @@
 import urllib.parse
 from datetime import datetime
 
-import marshmallow
+from marshmallow.exceptions import ValidationError
 from flask import Blueprint
 from flask import request
 from flask_login import current_user
@@ -35,7 +35,6 @@
 from app.datamgmt.case.case_events_db import add_comment_to_event
 from app.datamgmt.case.case_events_db import get_category_by_name
 from app.datamgmt.case.case_events_db import get_default_category
-from app.datamgmt.case.case_events_db import delete_event
 from app.datamgmt.case.case_events_db import delete_event_comment
 from app.datamgmt.case.case_events_db import get_case_event
 from app.datamgmt.case.case_events_db import get_case_event_comment
@@ -75,6 +74,8 @@
 from app.business.errors import BusinessProcessingError
 from app.business.events import events_create
 from app.business.events import events_update
+from app.business.events import events_delete
+from app.iris_engine.module_handler.module_handler import call_deprecated_on_preload_modules_hook
 
 case_timeline_rest_blueprint = Blueprint('case_timeline_rest', __name__)
 
@@ -156,7 +157,7 @@ def case_comment_add(cur_id, caseid):
         track_activity(f"event \"{event.event_title}\" commented", caseid=caseid)
         return response_success("Event commented", data=comment_schema.dump(comment))
 
-    except marshmallow.exceptions.ValidationError as e:
+    except ValidationError as e:
         return response_error(msg="Data error", data=e.normalized_messages())
 
 
@@ -625,6 +626,7 @@ def case_filter_timeline(caseid):
 
 
 @case_timeline_rest_blueprint.route('/case/timeline/events/delete/<int:cur_id>', methods=['POST'])
+@endpoint_deprecated('DELETE', '/api/v2/cases/{case_identifier}/events/{identifier}')
 @ac_requires_case_identifier(CaseAccessLevel.full_access)
 @ac_api_requires()
 def case_delete_event(cur_id, caseid):
@@ -634,13 +636,7 @@ def case_delete_event(cur_id, caseid):
     if not event:
         return response_error('Not a valid event ID for this case')
 
-    delete_event(event=event, caseid=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)
+    events_delete(event)
 
     return response_success(f'Event ID {cur_id} deleted')
 
@@ -694,8 +690,16 @@ def case_edit_event(cur_id, caseid):
         if not event:
             return response_error("Invalid event ID for this case")
 
-        request_json = request.get_json()
-        event = events_update(event, request_json)
+        request_data = call_deprecated_on_preload_modules_hook('event_update', request.get_json(), caseid)
+
+        request_data['event_id'] = event.event_id
+        event = _load(request_data, instance=event)
+        event_category_id = request_data.get('event_category_id')
+        event_assets = request_data.get('event_assets')
+        event_iocs = request_data.get('event_iocs')
+        event_sync_iocs_assets = request_data.get('event_sync_iocs_assets')
+
+        event = events_update(event, event_category_id, event_assets, event_iocs, event_sync_iocs_assets)
 
         event_schema = EventSchema()
         event_dump = event_schema.dump(event)
@@ -707,6 +711,8 @@ def case_edit_event(cur_id, caseid):
 
         return response_success("Event updated", data=event_dump)
 
+    except ValidationError as e:
+        return response_error(e.get_message(), data=e.get_data())
     except BusinessProcessingError as e:
         return response_error(e.get_message(), data=e.get_data())
 
@@ -716,9 +722,17 @@ def case_edit_event(cur_id, caseid):
 @ac_requires_case_identifier(CaseAccessLevel.full_access)
 @ac_api_requires()
 def case_add_event(caseid):
-    request_json = request.get_json()
     try:
-        event = events_create(caseid, request_json)
+        request_data = call_deprecated_on_preload_modules_hook('event_create', request.get_json(), caseid)
+
+        event = _load(request_data)
+
+        event_category_id = request_data.get('event_category_id')
+        event_assets = request_data.get('event_assets')
+        event_iocs = request_data.get('event_iocs')
+        sync_iocs_assets = request_data.get('event_sync_iocs_assets', False)
+
+        event = events_create(caseid, event, event_category_id, event_assets, event_iocs, sync_iocs_assets)
         event_schema = EventSchema()
         event_dump = event_schema.dump(event)
         collab_notify(case_id=caseid,
@@ -729,6 +743,8 @@ def case_add_event(caseid):
 
         return response_success('Event added', data=event_schema.dump(event))
 
+    except ValidationError as e:
+        return response_error('Data error', data=e.normalized_messages())
     except BusinessProcessingError as e:
         return response_error(e.get_message(), data=e.get_data())
 
@@ -789,7 +805,7 @@ def case_duplicate_event(cur_id, caseid):
         track_activity(f"added event \"{event.event_title}\"", caseid=caseid)
         return response_success("Event duplicated", data=event_schema.dump(event))
 
-    except marshmallow.exceptions.ValidationError as e:
+    except ValidationError as e:
         return response_error(msg="Data error", data=e.normalized_messages())
 
 
@@ -974,7 +990,7 @@ def case_events_upload_csv(caseid):
 
             track_activity(f"added event {event.event_id}", caseid=caseid)
 
-    except marshmallow.exceptions.ValidationError as e:
+    except ValidationError as e:
         return response_error(msg="Data error", data=e.normalized_messages())
 
     except Exception as e:
@@ -988,3 +1004,11 @@ def case_events_upload_csv(caseid):
     app.logger.info("======================== END_CSV_IMPORT ==========================================")
 
     return response_success(msg="Events added (CSV File)")
+
+
+def _load(request_data, **kwargs):
+    schema = EventSchema()
+    event = schema.load(request_data, **kwargs)
+    event.event_date, event.event_date_wtz = schema.validate_date(request_data.get(u'event_date'),
+                                                                  request_data.get(u'event_tz'))
+    return event
diff --git a/source/app/blueprints/rest/v2/case_objects/events.py b/source/app/blueprints/rest/v2/case_objects/events.py
index c89c0a077..471214c6a 100644
--- a/source/app/blueprints/rest/v2/case_objects/events.py
+++ b/source/app/blueprints/rest/v2/case_objects/events.py
@@ -18,16 +18,19 @@
 
 from flask import Blueprint
 from flask import request
+from marshmallow.exceptions import ValidationError
 
 from app.blueprints.access_controls import ac_api_requires
 from app.blueprints.rest.endpoints import response_api_created
 from app.blueprints.rest.endpoints import response_api_success
+from app.blueprints.rest.endpoints import response_api_deleted
 from app.blueprints.rest.endpoints import response_api_error
 from app.blueprints.rest.endpoints import response_api_not_found
 from app.blueprints.access_controls import ac_api_return_access_denied
 from app.business.events import events_create
 from app.business.events import events_get
 from app.business.events import events_update
+from app.business.events import events_delete
 from app.models.cases import CasesEvent
 from app.schema.marshables import EventSchema
 from app.business.errors import BusinessProcessingError
@@ -36,77 +39,142 @@
 from app.iris_engine.access_control.utils import ac_fast_check_current_user_has_case_access
 from app.iris_engine.utils.collab import notify
 from app.models.authorization import CaseAccessLevel
+from app.iris_engine.module_handler.module_handler import call_deprecated_on_preload_modules_hook
+
+
+class Events:
+
+    def __init__(self):
+        self._schema = EventSchema()
+
+    @staticmethod
+    def _check_event_and_case_identifier_match(event: CasesEvent, case_identifier):
+        if event.case_id != case_identifier:
+            raise BusinessProcessingError(f'Event {event.event_id} does not belong to case {case_identifier}')
 
+    def _load(self, request_data, **kwargs):
+        event = self._schema.load(request_data, **kwargs)
+        event.event_date, event.event_date_wtz = self._schema.validate_date(request_data.get(u'event_date'),
+                                                                            request_data.get(u'event_tz'))
+        return event
 
+    def create(self, case_identifier):
+        if not cases_exists(case_identifier):
+            return response_api_not_found()
+        if not ac_fast_check_current_user_has_case_access(case_identifier, [CaseAccessLevel.full_access]):
+            return ac_api_return_access_denied(caseid=case_identifier)
+
+        try:
+            request_data = call_deprecated_on_preload_modules_hook('event_create', request.get_json(), case_identifier)
+
+            event = self._load(request_data)
+
+            event_category_id = request_data.get('event_category_id')
+            event_assets = request_data.get('event_assets')
+            event_iocs = request_data.get('event_iocs')
+            sync_iocs_assets = request_data.get('event_sync_iocs_assets', False)
+
+            event = events_create(case_identifier, event, event_category_id, event_assets, event_iocs, sync_iocs_assets)
+            result = self._schema.dump(event)
+            notify(case_identifier, 'events', 'updated', event.event_id, object_data=result)
+
+            return response_api_created(result)
+
+        except ValidationError as e:
+            return response_api_error('Data error', data=e.normalized_messages())
+
+    def get(self, case_identifier, identifier):
+        if not cases_exists(case_identifier):
+            return response_api_not_found()
+
+        try:
+            event = events_get(identifier)
+            self._check_event_and_case_identifier_match(event, case_identifier)
+            if not ac_fast_check_current_user_has_case_access(event.case_id, [CaseAccessLevel.read_only, CaseAccessLevel.full_access]):
+                return ac_api_return_access_denied(caseid=event.case_id)
+
+            result = self._schema.dump(event)
+            result['event_category_id'] = event.category[0].id if event.category else None
+            return response_api_success(result)
+        except ObjectNotFoundError:
+            return response_api_not_found()
+        except BusinessProcessingError as e:
+            return response_api_error(e.get_message(), data=e.get_data())
+
+    def update(self, case_identifier, identifier):
+        if not cases_exists(case_identifier):
+            return response_api_not_found()
+
+        try:
+            event = events_get(identifier)
+            if not ac_fast_check_current_user_has_case_access(event.case_id, [CaseAccessLevel.full_access]):
+                return ac_api_return_access_denied(caseid=event.case_id)
+            self._check_event_and_case_identifier_match(event, case_identifier)
+
+            request_data = call_deprecated_on_preload_modules_hook('event_update', request.get_json(), case_identifier)
+
+            request_data['event_id'] = event.event_id
+            event = self._load(request_data, instance=event)
+            event_category_id = request_data.get('event_category_id')
+            event_assets = request_data.get('event_assets')
+            event_iocs = request_data.get('event_iocs')
+            event_sync_iocs_assets = request_data.get('event_sync_iocs_assets')
+
+            event = events_update(event, event_category_id, event_assets, event_iocs, event_sync_iocs_assets)
+
+            result = self._schema.dump(event)
+            notify(case_identifier, 'events', 'updated', identifier, object_data=result)
+
+            return response_api_success(result)
+        except ObjectNotFoundError:
+            return response_api_not_found()
+        except ValidationError as e:
+            return response_api_error('Data error', data=e.normalized_messages())
+        except BusinessProcessingError as e:
+            return response_api_error('Data error', data=e.get_data())
+
+    def delete(self, case_identifier, identifier):
+        if not cases_exists(case_identifier):
+            return response_api_not_found()
+
+        try:
+            event = events_get(identifier)
+            if not ac_fast_check_current_user_has_case_access(event.case_id, [CaseAccessLevel.full_access]):
+                return ac_api_return_access_denied(caseid=event.case_id)
+            self._check_event_and_case_identifier_match(event, case_identifier)
+
+            events_delete(event)
+
+            return response_api_deleted()
+        except ObjectNotFoundError:
+            return response_api_not_found()
+        except BusinessProcessingError as e:
+            return response_api_error(e.get_message(), data=e.get_data())
+
+
+events = Events()
 case_events_blueprint = Blueprint('case_events_rest_v2', __name__, url_prefix='/<int:case_identifier>/events')
 
 
 @case_events_blueprint.post('')
 @ac_api_requires()
 def create_event(case_identifier):
-    if not cases_exists(case_identifier):
-        return response_api_not_found()
-    if not ac_fast_check_current_user_has_case_access(case_identifier, [CaseAccessLevel.full_access]):
-        return ac_api_return_access_denied(caseid=case_identifier)
-
-    try:
-        event = events_create(case_identifier, request.get_json())
-        schema = EventSchema()
-        result = schema.dump(event)
-        notify(case_identifier, 'events', 'updated', event.event_id, object_data=result)
-
-        return response_api_created(result)
-    except BusinessProcessingError as e:
-        return response_api_error(e.get_message(), data=e.get_data())
+    return events.create(case_identifier)
 
 
 @case_events_blueprint.get('/<int:identifier>')
 @ac_api_requires()
 def get_event(case_identifier, identifier):
-    if not cases_exists(case_identifier):
-        return response_api_not_found()
-
-    try:
-        event = events_get(identifier)
-        _check_event_and_case_identifier_match(event, case_identifier)
-        if not ac_fast_check_current_user_has_case_access(event.case_id, [CaseAccessLevel.read_only, CaseAccessLevel.full_access]):
-            return ac_api_return_access_denied(caseid=event.case_id)
-
-        schema = EventSchema()
-        result = schema.dump(event)
-        result['event_category_id'] = event.category[0].id if event.category else None
-        return response_api_success(result)
-    except ObjectNotFoundError:
-        return response_api_not_found()
-    except BusinessProcessingError as e:
-        return response_api_error(e.get_message(), data=e.get_data())
+    return events.get(case_identifier, identifier)
 
 
 @case_events_blueprint.put('/<int:identifier>')
 @ac_api_requires()
 def update_event(case_identifier, identifier):
-    if not cases_exists(case_identifier):
-        return response_api_not_found()
-
-    try:
-        event = events_get(identifier)
-        if not ac_fast_check_current_user_has_case_access(event.case_id, [CaseAccessLevel.full_access]):
-            return ac_api_return_access_denied(caseid=event.case_id)
-        _check_event_and_case_identifier_match(event, case_identifier)
-
-        event = events_update(event, request.get_json())
+    return events.update(case_identifier, identifier)
 
-        schema = EventSchema()
-        result = schema.dump(event)
-        notify(case_identifier, 'events', 'updated', identifier, object_data=result)
 
-        return response_api_success(result)
-    except ObjectNotFoundError:
-        return response_api_not_found()
-    except BusinessProcessingError as e:
-        return response_api_error(e.get_message(), data=e.get_data())
-
-
-def _check_event_and_case_identifier_match(event: CasesEvent, case_identifier):
-    if event.case_id != case_identifier:
-        raise BusinessProcessingError(f'Event {event.event_id} does not belong to case {case_identifier}')
+@case_events_blueprint.delete('/<int:identifier>')
+@ac_api_requires()
+def delete_event(case_identifier, identifier):
+    return events.delete(case_identifier, identifier)
diff --git a/source/app/blueprints/rest/v2/case_objects/evidences.py b/source/app/blueprints/rest/v2/case_objects/evidences.py
index 19620df9b..0d209dbcb 100644
--- a/source/app/blueprints/rest/v2/case_objects/evidences.py
+++ b/source/app/blueprints/rest/v2/case_objects/evidences.py
@@ -134,11 +134,12 @@ def delete_evidence(case_identifier, identifier):
 
     try:
         evidence = evidences_get(identifier)
-        evidences_delete(evidence)
         _check_evidence_and_case_identifier_match(evidence, case_identifier)
         if not ac_fast_check_current_user_has_case_access(evidence.case_id, [CaseAccessLevel.full_access]):
             return ac_api_return_access_denied(caseid=evidence.case_id)
 
+        evidences_delete(evidence)
+
         return response_api_deleted()
     except ObjectNotFoundError:
         return response_api_not_found()
diff --git a/source/app/business/events.py b/source/app/business/events.py
index 84700fbd2..8f3337a37 100644
--- a/source/app/business/events.py
+++ b/source/app/business/events.py
@@ -18,40 +18,25 @@
 
 from datetime import datetime
 
-import marshmallow
 from flask_login import current_user
-from marshmallow.exceptions import ValidationError
 
 from app import db
 from app.models.cases import CasesEvent
 from app.business.errors import ObjectNotFoundError
-from app.iris_engine.module_handler.module_handler import call_modules_hook
-from app.schema.marshables import EventSchema
 from app.util import add_obj_history_entry
 from app.datamgmt.states import update_timeline_state
 from app.datamgmt.case.case_events_db import save_event_category
 from app.datamgmt.case.case_events_db import update_event_assets
 from app.business.errors import BusinessProcessingError
 from app.datamgmt.case.case_events_db import update_event_iocs
-from app.iris_engine.utils.tracker import track_activity
 from app.datamgmt.case.case_events_db import get_case_event
+from app.datamgmt.case.case_events_db import delete_event
+from app.iris_engine.utils.tracker import track_activity
+from app.iris_engine.utils.collab import collab_notify
+from app.iris_engine.module_handler.module_handler import call_modules_hook
 
 
-def _load(request_data, **kwargs):
-    try:
-        schema = EventSchema()
-        event = schema.load(request_data, **kwargs)
-        event.event_date, event.event_date_wtz = schema.validate_date(request_data.get(u'event_date'),
-                                                                      request_data.get(u'event_tz'))
-        return event
-    except ValidationError as e:
-        raise BusinessProcessingError('Data error', data=e.normalized_messages())
-
-
-def events_create(case_identifier, request_json) -> CasesEvent:
-    request_data = call_modules_hook('on_preload_event_create', data=request_json, caseid=case_identifier)
-
-    event = _load(request_data)
+def events_create(case_identifier, event: CasesEvent, event_category_id, event_assets, event_iocs, sync_iocs_assets) -> CasesEvent:
 
     event.case_id = case_identifier
     event.event_added = datetime.utcnow()
@@ -63,25 +48,23 @@ def events_create(case_identifier, request_json) -> CasesEvent:
     update_timeline_state(caseid=case_identifier)
     db.session.commit()
 
-    save_event_category(event.event_id, request_data.get('event_category_id'))
+    save_event_category(event.event_id, event_category_id)
 
-    setattr(event, 'event_category_id', request_data.get('event_category_id'))
-    sync_iocs_assets = request_data.get('event_sync_iocs_assets', False)
+    setattr(event, 'event_category_id', event_category_id)
 
-    success, log = update_event_assets(event.event_id, case_identifier, request_data.get('event_assets'),
-                                       request_data.get('event_iocs'), sync_iocs_assets)
+    success, log = update_event_assets(event.event_id, case_identifier, event_assets, event_iocs, sync_iocs_assets)
     if not success:
         raise BusinessProcessingError('Error while saving linked assets', data=log)
 
     success, log = update_event_iocs(event_id=event.event_id,
                                      caseid=case_identifier,
-                                     iocs_list=request_data.get('event_iocs'))
+                                     iocs_list=event_iocs)
     if not success:
         raise BusinessProcessingError('Error while saving linked iocs', data=log)
 
-    setattr(event, 'event_category_id', request_data.get('event_category_id'))
+    setattr(event, 'event_category_id', event_category_id)
 
-    event = call_modules_hook('on_postload_event_create', data=event, caseid=case_identifier)
+    event = call_modules_hook('on_postload_event_create', event, caseid=case_identifier)
 
     track_activity(f'added event "{event.event_title}"', caseid=case_identifier)
     return event
@@ -94,36 +77,35 @@ def events_get(identifier) -> CasesEvent:
     return event
 
 
-def events_update(event: CasesEvent, request_json: dict) -> CasesEvent:
-    try:
-        request_data = call_modules_hook('on_preload_event_update', data=request_json, caseid=event.case_id)
+def events_update(event: CasesEvent, event_category_id, event_assets, event_iocs, event_sync_iocs_assets) -> CasesEvent:
+    add_obj_history_entry(event, 'updated')
 
-        request_data['event_id'] = event.event_id
-        event = _load(request_data, instance=event)
+    update_timeline_state(caseid=event.case_id)
+    db.session.commit()
 
-        add_obj_history_entry(event, 'updated')
+    save_event_category(event.event_id, event_category_id)
 
-        update_timeline_state(caseid=event.case_id)
-        db.session.commit()
+    setattr(event, 'event_category_id', event_category_id)
 
-        save_event_category(event.event_id, request_data.get('event_category_id'))
+    success, log = update_event_assets(event.event_id, event.case_id, event_assets, event_iocs, event_sync_iocs_assets)
+    if not success:
+        raise BusinessProcessingError('Error while saving linked assets', data=log)
 
-        setattr(event, 'event_category_id', request_data.get('event_category_id'))
+    success, log = update_event_iocs(event_id=event.event_id,
+                                     caseid=event.case_id,
+                                     iocs_list=event_iocs)
+    if not success:
+        raise BusinessProcessingError('Error while saving linked iocs', data=log)
+
+    event = call_modules_hook('on_postload_event_update', event, caseid=event.case_id)
 
-        success, log = update_event_assets(event.event_id, event.case_id, request_data.get('event_assets'),
-                                           request_data.get('event_iocs'), request_data.get('event_sync_iocs_assets'))
-        if not success:
-            raise BusinessProcessingError('Error while saving linked assets', data=log)
+    track_activity(f"updated event \"{event.event_title}\"", caseid=event.case_id)
+    return event
 
-        success, log = update_event_iocs(event_id=event.event_id,
-                                         caseid=event.case_id,
-                                         iocs_list=request_data.get('event_iocs'))
-        if not success:
-            raise BusinessProcessingError('Error while saving linked iocs', data=log)
 
-        event = call_modules_hook('on_postload_event_update', data=event, caseid=event.case_id)
+def events_delete(event: CasesEvent):
+    delete_event(event)
 
-        track_activity(f"updated event \"{event.event_title}\"", caseid=event.case_id)
-        return event
-    except marshmallow.exceptions.ValidationError as e:
-        raise BusinessProcessingError('Data error', data=e.normalized_messages())
+    call_modules_hook('on_postload_event_delete', event.event_id, caseid=event.case_id)
+    collab_notify(event.case_id, 'events', 'deletion', event.event_id)
+    track_activity(f'deleted event "{event.event_title}" in timeline', event.case_id)
diff --git a/source/app/datamgmt/case/case_events_db.py b/source/app/datamgmt/case/case_events_db.py
index 7728fc52c..145068698 100644
--- a/source/app/datamgmt/case/case_events_db.py
+++ b/source/app/datamgmt/case/case_events_db.py
@@ -351,17 +351,18 @@ def get_case_iocs_for_tm(caseid):
     return iocs
 
 
-def delete_event(event, caseid):
+def delete_event(event):
+    case_identifier = event.case_id
     delete_event_category(event.event_id)
 
     CaseEventsAssets.query.filter(
         CaseEventsAssets.event_id == event.event_id,
-        CaseEventsAssets.case_id == caseid
+        CaseEventsAssets.case_id == case_identifier
     ).delete()
 
     CaseEventsIoc.query.filter(
         CaseEventsIoc.event_id == event.event_id,
-        CaseEventsIoc.case_id == caseid
+        CaseEventsIoc.case_id == case_identifier
     ).delete()
 
     com_ids = EventComments.query.with_entities(
@@ -378,7 +379,7 @@ def delete_event(event, caseid):
     db.session.commit()
 
     db.session.delete(event)
-    update_timeline_state(caseid=caseid)
+    update_timeline_state(caseid=case_identifier)
 
     db.session.commit()
 
diff --git a/source/app/iris_engine/module_handler/module_handler.py b/source/app/iris_engine/module_handler/module_handler.py
index e61a5cf05..065e5e2cf 100644
--- a/source/app/iris_engine/module_handler/module_handler.py
+++ b/source/app/iris_engine/module_handler/module_handler.py
@@ -28,6 +28,7 @@
 from sqlalchemy import and_
 
 from app import app
+from app.logger import logger
 from app import celery
 from app import db
 from app.datamgmt.iris_engine.modules_db import get_module_config_from_hname
@@ -41,8 +42,6 @@
 from app.util import hmac_verify
 from iris_interface import IrisInterfaceStatus as IStatus
 
-log = app.logger
-
 
 def check_module_compatibility(module_version):
     return True
@@ -94,7 +93,7 @@ def check_module_health(module_instance):
 
     def dup_logs(message):
         logs.append(message)
-        log.info(message)
+        logger.info(message)
 
     if not module_instance:
         return False, ['Error - cannot instantiate the module. Check server logs']
@@ -111,7 +110,7 @@ def dup_logs(message):
         if not (version.parse(app.config.get('MODULES_INTERFACE_MIN_VERSION'))
                 <= version.parse(mod_interface_version)
                 <= version.parse(app.config.get('MODULES_INTERFACE_MAX_VERSION'))):
-            log.critical('Module interface no compatible with server. Expected '
+            logger.critical('Module interface no compatible with server. Expected '
                          f"{app.config.get('MODULES_INTERFACE_MIN_VERSION')} <= module "
                          f"<= {app.config.get('MODULES_INTERFACE_MAX_VERSION')}")
             logs.append("Module interface no compatible with server. Expected "
@@ -124,14 +123,14 @@ def dup_logs(message):
 
         module_type = module_instance.get_module_type()
         if module_type not in ["module_pipeline", "module_processor"]:
-            log.critical(f"Unrecognised module type. Expected module_pipeline or module_processor, got {module_type}")
+            logger.critical(f"Unrecognised module type. Expected module_pipeline or module_processor, got {module_type}")
             logs.append(f"Unrecognised module type. Expected module_pipeline or module_processor, got {module_type}")
             return False, logs
 
         dup_logs(f'Module type: {module_instance.get_module_type()}')
 
         if not module_instance.is_providing_pipeline() and module_type == 'pipeline':
-            log.critical("Module of type pipeline has no pipelines")
+            logger.critical("Module of type pipeline has no pipelines")
             logs.append("Error - Module of type pipeline has not pipelines")
             return False, logs
 
@@ -149,8 +148,8 @@ def dup_logs(message):
         return module_instance.is_ready(), logs
 
     except Exception as e:
-        log.exception("Error while checking module health")
-        log.error(e.__str__())
+        logger.exception("Error while checking module health")
+        logger.error(e.__str__())
         logs.append(e.__str__())
         return False, logs
 
@@ -168,7 +167,7 @@ def instantiate_module_from_name(module_name):
             return None
     except Exception as e:
         msg = f"Could not import root module {module_name}: {e}"
-        log.error(msg)
+        logger.error(msg)
         return None, msg
     # The whole concept is based on the fact that the root module provides an __iris_module_interface
     # variable pointing to the interface class with which Iris can talk to
@@ -176,7 +175,7 @@ def instantiate_module_from_name(module_name):
         mod_interface = importlib.import_module(f'{module_name}.{mod_root_interface.__iris_module_interface}')
     except Exception as e:
         msg = f'Could not import module {module_name}: {e}'
-        log.error(msg)
+        logger.error(msg)
         return None, msg
 
     if not mod_interface:
@@ -187,7 +186,7 @@ def instantiate_module_from_name(module_name):
         cl_interface = getattr(mod_interface, mod_root_interface.__iris_module_interface)
     except Exception as e:
         msg = f"Could not get handle on the interface class of module {module_name}: {e}"
-        log.error(msg)
+        logger.error(msg)
         return None, msg
 
     if not cl_interface:
@@ -198,7 +197,7 @@ def instantiate_module_from_name(module_name):
         mod_inst = cl_interface()
     except Exception as e:
         msg = f'Could not instantiate the class for module {module_name}: {e}'
-        log.error(msg)
+        logger.error(msg)
         return None, msg
 
     return mod_inst, 'Success'
@@ -253,25 +252,25 @@ def register_module(module_name):
     """
 
     if not module_name:
-        log.error("Provided module has no names")
+        logger.error("Provided module has no names")
         return None, "Module has no names"
 
     try:
 
         mod_inst, _ = instantiate_module_from_name(module_name=module_name)
         if not mod_inst:
-            log.error("Module could not be instantiated")
+            logger.error("Module could not be instantiated")
             return None, "Module could not be instantiated"
 
         if iris_module_exists(module_name=module_name):
-            log.warning("Module already exists in Iris")
+            logger.warning("Module already exists in Iris")
             return None, "Module already exists in Iris"
 
         # Auto parse the configuration and fill with default
-        log.info('Parsing configuration')
+        logger.info('Parsing configuration')
         mod_config = preset_init_mod_config(mod_inst.get_init_configuration())
 
-        log.info('Adding module')
+        logger.info('Adding module')
         module = iris_module_add(module_name=module_name,
                                   module_human_name=mod_inst.get_module_name(),
                                   module_description=mod_inst.get_module_description(),
@@ -303,13 +302,13 @@ def iris_update_hooks(module_name, module_id):
     """
 
     if not module_name:
-        log.error("Provided module has no names")
+        logger.error("Provided module has no names")
         return False, ["Module has no names"]
 
     try:
         mod_inst, _ = instantiate_module_from_name(module_name=module_name)
         if not mod_inst:
-            log.error("Module could not be instantiated")
+            logger.error("Module could not be instantiated")
             return False, ["Module could not be instantiated"]
 
         if mod_inst.get_module_type() == 'module_processor':
@@ -398,7 +397,7 @@ def deregister_from_hook(module_id: int, iris_hook_name: str):
     :param iris_hook_name: hook_name to deregister from
     :return: IrisInterfaceStatus object
     """
-    log.info(f'Deregistering module #{module_id} from {iris_hook_name}')
+    logger.info(f'Deregistering module #{module_id} from {iris_hook_name}')
     hooks = IrisModuleHook.query.filter(
         IrisModuleHook.module_id == module_id,
         IrisHook.hook_name == iris_hook_name,
@@ -406,7 +405,7 @@ def deregister_from_hook(module_id: int, iris_hook_name: str):
     ).all()
     if hooks:
         for hook in hooks:
-            log.info(f'Deregistered module #{module_id} from {iris_hook_name}')
+            logger.info(f'Deregistered module #{module_id} from {iris_hook_name}')
             db.session.delete(hook)
 
     return True, ['Hook deregistered']
@@ -431,13 +430,13 @@ def task_hook_wrapper(self, module_name, hook_name, hook_ui_name, data, init_use
         signature, pdata = data.encode("utf-8").split(b" ")
         is_verified = hmac_verify(signature, pdata)
         if is_verified is False:
-            log.warning("data argument has not been correctly serialised")
+            logger.warning("data argument has not been correctly serialised")
             raise Exception('Unable to instantiate target module. Data has not been correctly serialised')
 
         deser_data = loads(base64.b64decode(pdata))
 
     except Exception as e:
-        log.exception(e)
+        logger.exception(e)
         raise Exception(e)
 
     try:
@@ -465,10 +464,10 @@ def task_hook_wrapper(self, module_name, hook_name, hook_ui_name, data, init_use
             _obj.append(_obj_a)
 
     except Exception as e:
-        log.exception(e)
+        logger.exception(e)
         raise Exception(e)
 
-    log.info(f'Calling module {module_name} for hook {hook_name}')
+    logger.info(f'Calling module {module_name} for hook {hook_name}')
 
     try:
         mod_inst, _ = instantiate_module_from_name(module_name=module_name)
@@ -484,8 +483,8 @@ def task_hook_wrapper(self, module_name, hook_name, hook_ui_name, data, init_use
 
     except Exception as e:
         msg = f"Failed to run hook {hook_name} with module {module_name}. Error {str(e)}"
-        log.critical(msg)
-        log.exception(e)
+        logger.critical(msg)
+        logger.exception(e)
         task_status = IStatus.I2Error(message=msg, logs=[traceback.format_exc()], user=init_user, caseid=caseid)
 
     return task_status
@@ -505,7 +504,7 @@ def call_modules_hook(hook_name: str, data: any, caseid: int = None, hook_ui_nam
     """
     hook = IrisHook.query.filter(IrisHook.hook_name == hook_name).first()
     if not hook:
-        log.critical(f'Hook name {hook_name} not found')
+        logger.critical(f'Hook name {hook_name} not found')
         raise Exception(f'Hook name {hook_name} not found')
 
     if hook_ui_name:
@@ -536,7 +535,7 @@ def call_modules_hook(hook_name: str, data: any, caseid: int = None, hook_ui_nam
 
     for module in modules:
         if module.run_asynchronously and "on_preload_" not in hook_name:
-            log.info(f'Calling module {module.module_name} asynchronously for hook {hook_name} :: {hook_ui_name}')
+            logger.info(f'Calling module {module.module_name} asynchronously for hook {hook_name} :: {hook_ui_name}')
             # We cannot directly pass the sqlalchemy in data, as it needs to be serializable
             # So pass a dumped instance and then rebuild on the task side
             ser_data = base64.b64encode(dumps(data))
@@ -547,7 +546,7 @@ def call_modules_hook(hook_name: str, data: any, caseid: int = None, hook_ui_nam
 
         else:
             # Direct call. Should be fast
-            log.info(f'Calling module {module.module_name} for hook {hook_name}')
+            logger.info(f'Calling module {module.module_name} for hook {hook_name}')
 
             try:
                 was_list = True
@@ -563,14 +562,14 @@ def call_modules_hook(hook_name: str, data: any, caseid: int = None, hook_ui_nam
                 status = mod_inst.hooks_handler(hook_name, module.manual_hook_ui_name, data=data_list)
 
             except Exception as e:
-                log.critical(f"Failed to run hook {hook_name} with module {module.module_name}. Error {str(e)}")
+                logger.critical(f"Failed to run hook {hook_name} with module {module.module_name}. Error {str(e)}")
                 continue
 
             if status.is_success():
                 data_result = status.get_data()
                 if not was_list:
                     if not isinstance(data_result, list):
-                        log.critical(f"Error getting data result from hook {hook_name}: "
+                        logger.critical(f"Error getting data result from hook {hook_name}: "
                                      f"A list is expected, instead got a {type(data_result)}")
                         continue
                     else:
@@ -582,6 +581,12 @@ def call_modules_hook(hook_name: str, data: any, caseid: int = None, hook_ui_nam
     return data
 
 
+def call_deprecated_on_preload_modules_hook(hook_name: str, data: any, case_identifier) -> any:
+    hook_name = f'on_preload_{hook_name}'
+    logger.warning(f'DEPRECATION WARNING: Hook {hook_name} has been deprecated and will be removed in a future version')
+    return call_modules_hook(hook_name, data, caseid=case_identifier)
+
+
 def list_available_pipelines():
     """
     Return a list of available pipelines by requesting the DB
diff --git a/source/app/schema/marshables.py b/source/app/schema/marshables.py
index 1fe86d848..e84470bfc 100644
--- a/source/app/schema/marshables.py
+++ b/source/app/schema/marshables.py
@@ -93,10 +93,10 @@
 from app.models.authorization import User
 from app.models.cases import CaseState
 from app.models.cases import CaseProtagonist
-from app.util import file_sha256sum
-from app.util import str_to_bool
-from app.util import assert_type_mml
-from app.util import stream_sha256sum
+from app.schema.utils import assert_type_mml
+from app.schema.utils import file_sha256sum
+from app.schema.utils import stream_sha256sum
+from app.schema.utils import str_to_bool
 
 ALLOWED_EXTENSIONS = {'png', 'svg'}
 POSTGRES_INT_MAX = 2147483647
diff --git a/source/app/schema/utils.py b/source/app/schema/utils.py
new file mode 100644
index 000000000..a69aa070b
--- /dev/null
+++ b/source/app/schema/utils.py
@@ -0,0 +1,93 @@
+#  IRIS Source Code
+#  Copyright (C) 2025 - 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.
+
+import hashlib
+from pathlib import Path
+from marshmallow import ValidationError
+
+from app.logger import logger
+
+
+def assert_type_mml(input_var: any, field_name: str, type: type, allow_none: bool = False,
+                    max_len: int = None, max_val: int = None, min_val: int = None):
+    if input_var is None:
+        if allow_none is False:
+            raise ValidationError('Invalid data - non null expected',
+                                  field_name=field_name if field_name else 'type')
+        return True
+
+    if isinstance(input_var, type):
+        if max_len:
+            if len(input_var) > max_len:
+                raise ValidationError('Invalid data - max length exceeded',
+                                      field_name=field_name if field_name else 'type')
+
+        if max_val:
+            if input_var > max_val:
+                raise ValidationError('Invalid data - max value exceeded',
+                                                  field_name=field_name if field_name else 'type')
+
+        if min_val:
+            if input_var < min_val:
+                raise ValidationError('Invalid data - min value exceeded',
+                                                  field_name=field_name if field_name else 'type')
+
+        return True
+
+    try:
+
+        if isinstance(type(input_var), type):
+            return True
+
+    except Exception as e:
+        logger.error(e)
+        print(e)
+
+    raise ValidationError('Invalid data type', field_name=field_name if field_name else 'type')
+
+
+def str_to_bool(value):
+    if value is None:
+        return False
+
+    if isinstance(value, bool):
+        return value
+
+    if isinstance(value, int):
+        return bool(value)
+
+    return value.lower() in ['true', '1', 'yes', 'y', 't']
+
+
+def stream_sha256sum(stream):
+
+    return hashlib.sha256(stream).hexdigest().upper()
+
+
+def file_sha256sum(file_path):
+
+    if not Path(file_path).is_file():
+        return None
+
+    sha256_hash = hashlib.sha256()
+    with open(file_path, "rb") as f:
+        # Read and update hash string value in blocks of 4K
+        for byte_block in iter(lambda: f.read(4096), b""):
+            sha256_hash.update(byte_block)
+
+        return sha256_hash.hexdigest().upper()
diff --git a/source/app/util.py b/source/app/util.py
index dd8367a42..bf08d97f3 100644
--- a/source/app/util.py
+++ b/source/app/util.py
@@ -21,17 +21,12 @@
 # TODO should probably dispatch the methods provided in this file in the different namespaces
 import base64
 import datetime
-import hashlib
-import logging as log
-import marshmallow
 import shutil
 import weakref
 from cryptography.exceptions import InvalidSignature
 from cryptography.hazmat.primitives import hashes
 from cryptography.hazmat.primitives import hmac
 from flask_login import current_user
-from pathlib import Path
-from pyunpack import Archive
 from sqlalchemy.orm.attributes import flag_modified
 from flask import current_app
 
@@ -51,24 +46,6 @@ def _do_cleanup(self, wr):
         shutil.rmtree(filepath, ignore_errors=True)
 
 
-def decompress_7z(filename: Path, output_dir):
-    """
-    Decompress a 7z file in specified output directory
-    :param filename: Filename to decompress
-    :param output_dir: Target output dir
-    :return: True if uncompress
-    """
-    try:
-        a = Archive(filename=filename)
-        a.extractall(directory=output_dir, auto_create_dir=True)
-
-    except Exception as e:
-        log.warning(e)
-        return False
-
-    return True
-
-
 def add_obj_history_entry(obj, action, commit=False):
     if hasattr(obj, 'modification_history'):
 
@@ -98,25 +75,6 @@ def add_obj_history_entry(obj, action, commit=False):
     return obj
 
 
-def file_sha256sum(file_path):
-
-    if not Path(file_path).is_file():
-        return None
-
-    sha256_hash = hashlib.sha256()
-    with open(file_path, "rb") as f:
-        # Read and update hash string value in blocks of 4K
-        for byte_block in iter(lambda: f.read(4096), b""):
-            sha256_hash.update(byte_block)
-
-        return sha256_hash.hexdigest().upper()
-
-
-def stream_sha256sum(stream):
-
-    return hashlib.sha256(stream).hexdigest().upper()
-
-
 def hmac_sign(data):
     key = bytes(current_app.config.get("SECRET_KEY"), "utf-8")
     h = hmac.HMAC(key, hashes.SHA256())
@@ -137,56 +95,3 @@ def hmac_verify(signature_enc, data):
         return True
     except InvalidSignature:
         return False
-
-
-def str_to_bool(value):
-    if value is None:
-        return False
-
-    if isinstance(value, bool):
-        return value
-
-    if isinstance(value, int):
-        return bool(value)
-
-    return value.lower() in ['true', '1', 'yes', 'y', 't']
-
-
-def assert_type_mml(input_var: any, field_name: str, type: type, allow_none: bool = False,
-                    max_len: int = None, max_val: int = None, min_val: int = None):
-    if input_var is None:
-        if allow_none is False:
-            raise marshmallow.ValidationError('Invalid data - non null expected',
-                                              field_name=field_name if field_name else 'type')
-        else:
-            return True
-
-    if isinstance(input_var, type):
-        if max_len:
-            if len(input_var) > max_len:
-                raise marshmallow.ValidationError('Invalid data - max length exceeded',
-                                                  field_name=field_name if field_name else 'type')
-
-        if max_val:
-            if input_var > max_val:
-                raise marshmallow.ValidationError('Invalid data - max value exceeded',
-                                                  field_name=field_name if field_name else 'type')
-
-        if min_val:
-            if input_var < min_val:
-                raise marshmallow.ValidationError('Invalid data - min value exceeded',
-                                                  field_name=field_name if field_name else 'type')
-
-        return True
-
-    try:
-
-        if isinstance(type(input_var), type):
-            return True
-
-    except Exception as e:
-        log.error(e)
-        print(e)
-
-    raise marshmallow.ValidationError('Invalid data type',
-                                      field_name=field_name if field_name else 'type')
diff --git a/tests/tests_rest_events.py b/tests/tests_rest_events.py
index c049b37b8..90a6cc7ca 100644
--- a/tests/tests_rest_events.py
+++ b/tests/tests_rest_events.py
@@ -359,3 +359,62 @@ def test_update_event_should_set_event_parent_id_when_provided(self):
                 'parent_event_id': parent_event_identifier}
         response = self._subject.update(f'/api/v2/cases/{case_identifier}/events/{identifier}', body).json()
         self.assertEqual(parent_event_identifier, response['parent_event_id'])
+
+    def test_delete_event_should_return_204(self):
+        case_identifier = self._subject.create_dummy_case()
+        body = {'event_title': 'title', 'event_category_id': 1,
+                'event_date': '2025-03-26T00:00:00.000', 'event_tz': '+00:00',
+                'event_assets': [], 'event_iocs': []}
+        response = self._subject.create(f'/api/v2/cases/{case_identifier}/events', body).json()
+        identifier = response['event_id']
+        response = self._subject.delete(f'/api/v2/cases/{case_identifier}/events/{identifier}')
+        self.assertEqual(204, response.status_code)
+
+    def test_get_event_should_return_404_after_it_has_been_deleted(self):
+        case_identifier = self._subject.create_dummy_case()
+        body = {'event_title': 'title', 'event_category_id': 1,
+                'event_date': '2025-03-26T00:00:00.000', 'event_tz': '+00:00',
+                'event_assets': [], 'event_iocs': []}
+        response = self._subject.create(f'/api/v2/cases/{case_identifier}/events', body).json()
+        identifier = response['event_id']
+        self._subject.delete(f'/api/v2/cases/{case_identifier}/events/{identifier}')
+        response = self._subject.get(f'/api/v2/cases/{case_identifier}/events/{identifier}')
+        self.assertEqual(404, response.status_code)
+
+    def test_delete_event_should_return_404_when_case_does_not_exist(self):
+        case_identifier = self._subject.create_dummy_case()
+        body = {'event_title': 'title', 'event_category_id': 1,
+                'event_date': '2025-03-26T00:00:00.000', 'event_tz': '+00:00',
+                'event_assets': [], 'event_iocs': []}
+        response = self._subject.create(f'/api/v2/cases/{case_identifier}/events', body).json()
+        identifier = response['event_id']
+        response = self._subject.delete(f'/api/v2/cases/{_IDENTIFIER_FOR_NONEXISTENT_OBJECT}/events/{identifier}')
+        self.assertEqual(404, response.status_code)
+
+    def test_delete_event_should_return_404_when_the_event_does_not_exist(self):
+        case_identifier = self._subject.create_dummy_case()
+        response = self._subject.delete(f'/api/v2/cases/{case_identifier}/events/{_IDENTIFIER_FOR_NONEXISTENT_OBJECT}')
+        self.assertEqual(404, response.status_code)
+
+    def test_delete_event_should_return_403_when_user_has_no_permission_to_access_case(self):
+        case_identifier = self._subject.create_dummy_case()
+        body = {'event_title': 'title', 'event_category_id': 1,
+                'event_date': '2025-03-26T00:00:00.000', 'event_tz': '+00:00',
+                'event_assets': [], 'event_iocs': []}
+        response = self._subject.create(f'/api/v2/cases/{case_identifier}/events', body).json()
+        identifier = response['event_id']
+
+        user = self._subject.create_dummy_user()
+        response = user.delete(f'/api/v2/cases/{case_identifier}/events/{identifier}')
+        self.assertEqual(403, response.status_code)
+
+    def test_delete_event_should_return_400_when_case_identifier_does_not_match_event_case(self):
+        case_identifier = self._subject.create_dummy_case()
+        body = {'event_title': 'title', 'event_category_id': 1,
+                'event_date': '2025-03-26T00:00:00.000', 'event_tz': '+00:00',
+                'event_assets': [], 'event_iocs': []}
+        response = self._subject.create(f'/api/v2/cases/{case_identifier}/events', body).json()
+        identifier = response['event_id']
+        case_identifier2 = self._subject.create_dummy_case()
+        response = self._subject.delete(f'/api/v2/cases/{case_identifier2}/events/{identifier}')
+        self.assertEqual(400, response.status_code)
diff --git a/tests/tests_rest_evidences.py b/tests/tests_rest_evidences.py
index a4ee16134..f99ff7ef0 100644
--- a/tests/tests_rest_evidences.py
+++ b/tests/tests_rest_evidences.py
@@ -300,6 +300,17 @@ def test_delete_evidence_should_return_403_when_user_has_no_permission_to_access
         response = user.delete(f'/api/v2/cases/{case_identifier}/evidences/{identifier}')
         self.assertEqual(403, response.status_code)
 
+    def test_delete_evidence_should_not_remove_evidence_when_user_has_no_permission_to_access_case(self):
+        case_identifier = self._subject.create_dummy_case()
+        body = {'filename': 'filename'}
+        response = self._subject.create(f'/api/v2/cases/{case_identifier}/evidences', body).json()
+        identifier = response['id']
+
+        user = self._subject.create_dummy_user()
+        user.delete(f'/api/v2/cases/{case_identifier}/evidences/{identifier}')
+        response = self._subject.get(f'/api/v2/cases/{case_identifier}/evidences/{identifier}')
+        self.assertEqual(200, response.status_code)
+
     def test_delete_evidence_should_return_404_when_case_does_not_exist(self):
         case_identifier = self._subject.create_dummy_case()
         body = {'filename': 'filename'}
diff --git a/ui/src/pages/updates.handler.js b/ui/src/pages/updates.handler.js
deleted file mode 100644
index c341cab75..000000000
--- a/ui/src/pages/updates.handler.js
+++ /dev/null
@@ -1,174 +0,0 @@
-channel = 'iris_update_status';
-
-function log_error(message) {
-    add_update_log(message, true);
-}
-
-function log_msg(message) {
-    add_update_log(message, false)
-}
-
-function add_update_log(message, is_error) {
-    html_wrap = `<h4><i class="mt-2 fas fa-check text-success"></i>  `
-    if (is_error) {
-        html_wrap = `<h4><i class="mt-2 fas fa-times text-danger"></i> `
-    }
-    $("#updates_log").append(html_wrap + message + '</h4><br/>')
-    $('html, body').animate({
-        scrollTop: $("#updates_log_end").offset().top
-    }, 50);
-}
-
-function get_caseid() {
-    queryString = window.location.search;
-    urlParams = new URLSearchParams(queryString);
-
-    return urlParams.get('cid')
-}
-
-function case_param() {
-    var params = {
-        cid: get_caseid
-    }
-    return '?'+ $.param(params);
-}
-
-function initiate_update() {
-    $.ajax({
-        url: `/manage/server/start-update${case_param()}`,
-        type: 'GET',
-        dataType: 'json',
-        beforeSend : function () {
-            log_msg('Update order sent. Expecting feedback anytime soon');
-        },
-        success: function (data) {},
-        error: function (data) {
-            log_error('Unexpected error starting update');
-        }
-    });
-}
-
-var intervalId = null;
-var ios = io('/server-updates');
-var update_socket = null;
-var current_version = null;
-var updated_version = null;
-var steps = 0;
-var no_resp_time = 0;
-
-function check_server_version() {
-    $.ajax({
-        url: `/api/versions${case_param()}`,
-        type: 'GET',
-        dataType: 'json',
-        timeout: 1000,
-        success: function (data) {
-            server_version = data.data.iris_current;
-            if (server_version == current_version) {
-                add_update_log('Something was wrong - server is still in the same version', true);
-                add_update_log('Please check server logs', true);
-                clearInterval(intervalId);
-                $('#tag_bottom').hide();
-                $('#update_return_button').show();
-            } else {
-                add_update_log('Successfully updated from ' + current_version + ' to ' + server_version, false);
-                add_update_log('You can now leave this page', false);
-                clearInterval(intervalId);
-                $('#tag_bottom').hide();
-                $('#update_return_button').show();
-            }
-        },
-        error: function (error) {
-            log_error('Something\'s wrong, server is not answering');
-            log_error('Please check server logs')
-            clearInterval(intervalId);
-            $('#tag_bottom').hide();
-            $('#update_return_button').show();
-        }
-    });
-}
-
-function ping_check_server_online() {
-
-    $.ajax({
-        url: `/api/ping${case_param()}`,
-        type: "GET",
-        dataType: "json",
-        timeout: 1000,
-        success: function (data) {
-            $("#offline_time").hide();
-            log_msg('Server is back online');
-            clearInterval(intervalId);
-            check_server_version();
-        },
-        error: function (error) {
-            no_resp_time += 1;
-            if (no_resp_time > 29) {
-                log_error('Something\'s wrong, server is not answering');
-                log_error('Please check server logs')
-                clearInterval(intervalId);
-                $('#tag_bottom').hide();
-                $('#update_return_button').show();
-            }
-            $("#offline_time").html('<h4 id="offline_time"><i class="fas fa-clock"></i> Attempt '+ no_resp_time +' / 30</h4><br/>');
-            $("#offline_time").show();
-        }
-    });
-}
-
-function start_updates(){
-    $('#update_start_btn').hide();
-    $('.update_start_txt').hide();
-    $('#container-updates').show();
-    update_socket.emit('update_get_current_version', { 'channel': channel });
-    update_socket.emit('update_ping', { 'channel': channel });
-//    index = 0;
-//    while(index < 20) {
-//        add_update_log('ping');
-//        index += 1;
-//    }
-}
-
-
-$(document).ready(function(){
-
-    update_socket = ios.connect();
-
-    update_socket.on('update_status', function(data) {
-        add_update_log(data.message, data.is_error)
-    }.bind() );
-
-    update_socket.on('update_ping', function(data) {
-        log_msg('Server connection verified');
-        log_msg('Starting update');
-        initiate_update();
-    }.bind() );
-
-    update_socket.on('server_has_updated', function(data) {
-        log_msg('Server reported updates applied. Checking . . .');
-        check_server_version();
-    }.bind() );
-
-    update_socket.on('disconnect', function () {
-        add_update_log('Server is offline, waiting for connection', false);
-        intervalId = window.setInterval(function(){
-            ping_check_server_online();
-        }, 1000);
-    });
-
-    update_socket.on('update_current_version', function (data) {
-        add_update_log('Server reported version ' + data.version , false);
-        if (current_version == null) {
-            current_version = data.version;
-        }
-    });
-
-    update_socket.on('update_has_fail', function () {
-        $('#tag_bottom').hide();
-        $('#update_return_button').show();
-    });
-
-    update_socket.emit('join-update', { 'channel': channel });
-
-
-});
\ No newline at end of file


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>source/app/schema/utils.py (1)</summary><blockquote>

`34-49`: **Consider combining nested `if` checks.**  
You can simplify the nested checks for `max_len`, `max_val`, and `min_val` into a single condition per block. For example:

```diff
- if max_len:
-     if len(input_var) > max_len:
+ if max_len and len(input_var) > max_len:
    raise ValidationError('Invalid data - max length exceeded',
                          field_name=field_name if field_name else 'type')
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

35-36: Use a single `if` statement instead of nested `if` statements

Combine `if` statements using `and`

(SIM102)

---

40-41: Use a single `if` statement instead of nested `if` statements

(SIM102)

---

45-46: Use a single `if` statement instead of nested `if` statements

(SIM102)

</details>

</details>

</blockquote></details>
<details>
<summary>source/app/business/events.py (3)</summary><blockquote>

`53-53`: **Use direct assignment instead of setattr.**  
```diff
- setattr(event, 'event_category_id', event_category_id)
+ event.event_category_id = event_category_id
```
This avoids the overhead of `setattr` for a single known field.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

53-53: Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.

Replace `setattr` with assignment

(B010)

</details>

</details>

---

`65-65`: **Use direct assignment instead of setattr.**  
```diff
- setattr(event, 'event_category_id', event_category_id)
+ event.event_category_id = event_category_id
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

65-65: Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.

Replace `setattr` with assignment

(B010)

</details>

</details>

---

`88-88`: **Direct assignment is more conventional than setattr.**  
```diff
- setattr(event, 'event_category_id', event_category_id)
+ event.event_category_id = event_category_id
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

88-88: Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.

Replace `setattr` with assignment

(B010)

</details>

</details>

</blockquote></details>
<details>
<summary>source/app/blueprints/rest/v2/case_objects/events.py (1)</summary><blockquote>

`55-59`: **Consider moving date validation inside the schema.**
Although calling `validate_date` externally works, placing date validation within the Marshmallow schema can further encapsulate data validation processes and reduce coupling.  

```diff
 def _load(self, request_data, **kwargs):
-    event = self._schema.load(request_data, **kwargs)
-    event.event_date, event.event_date_wtz = self._schema.validate_date(request_data.get(u'event_date'),
-                                                                        request_data.get(u'event_tz'))
+    event = self._schema.load(request_data, **kwargs)  # The schema handles all date validation
     return event
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1e6373ca5d11b957083f1f7ca83d7e3af0540624 and 11384c32493eb4b62c24f7174ddeb8b137487949.

</details>

<details>
<summary>📒 Files selected for processing (16)</summary>

* `pyproject.toml` (1 hunks)
* `source/app/blueprints/pages/manage/manage_srv_settings_routes.py` (0 hunks)
* `source/app/blueprints/pages/manage/templates/manage_make_update.html` (0 hunks)
* `source/app/blueprints/pages/manage/templates/modal_server_updates.html` (0 hunks)
* `source/app/blueprints/rest/case/case_timeline_routes.py` (12 hunks)
* `source/app/blueprints/rest/v2/case_objects/events.py` (2 hunks)
* `source/app/blueprints/rest/v2/case_objects/evidences.py` (1 hunks)
* `source/app/business/events.py` (3 hunks)
* `source/app/datamgmt/case/case_events_db.py` (2 hunks)
* `source/app/iris_engine/module_handler/module_handler.py` (19 hunks)
* `source/app/schema/marshables.py` (1 hunks)
* `source/app/schema/utils.py` (1 hunks)
* `source/app/util.py` (0 hunks)
* `tests/tests_rest_events.py` (1 hunks)
* `tests/tests_rest_evidences.py` (1 hunks)
* `ui/src/pages/updates.handler.js` (0 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (5)</summary>

* source/app/blueprints/pages/manage/templates/manage_make_update.html
* source/app/blueprints/pages/manage/templates/modal_server_updates.html
* source/app/util.py
* source/app/blueprints/pages/manage/manage_srv_settings_routes.py
* ui/src/pages/updates.handler.js

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (7)</summary>

<details>
<summary>source/app/blueprints/rest/v2/case_objects/evidences.py (1)</summary><blockquote>

<details>
<summary>source/app/business/evidences.py (1)</summary>

* `evidences_delete` (86-90)

</details>

</blockquote></details>
<details>
<summary>source/app/datamgmt/case/case_events_db.py (3)</summary><blockquote>

<details>
<summary>source/app/blueprints/rest/v2/case_objects/events.py (2)</summary>

* `delete_event` (179-180)
* `delete` (136-152)

</details>
<details>
<summary>source/app/models/models.py (2)</summary>

* `CaseEventsAssets` (280-290)
* `CaseEventsIoc` (293-303)

</details>
<details>
<summary>source/app/datamgmt/states.py (1)</summary>

* `update_timeline_state` (86-87)

</details>

</blockquote></details>
<details>
<summary>source/app/iris_engine/module_handler/module_handler.py (1)</summary><blockquote>

<details>
<summary>source/app/models/models.py (2)</summary>

* `IrisModuleHook` (877-891)
* `IrisHook` (869-874)

</details>

</blockquote></details>
<details>
<summary>tests/tests_rest_evidences.py (2)</summary><blockquote>

<details>
<summary>tests/iris.py (2)</summary>

* `create_dummy_case` (72-80)
* `create_dummy_user` (69-70)

</details>
<details>
<summary>source/app/blueprints/rest/v2/case_objects/events.py (3)</summary>

* `create` (61-84)
* `delete` (136-152)
* `get` (86-102)

</details>

</blockquote></details>
<details>
<summary>source/app/business/events.py (3)</summary><blockquote>

<details>
<summary>source/app/datamgmt/case/case_events_db.py (1)</summary>

* `delete_event` (354-384)

</details>
<details>
<summary>source/app/iris_engine/utils/tracker.py (1)</summary>

* `track_activity` (30-66)

</details>
<details>
<summary>source/app/iris_engine/utils/collab.py (1)</summary>

* `collab_notify` (6-15)

</details>

</blockquote></details>
<details>
<summary>source/app/blueprints/rest/case/case_timeline_routes.py (5)</summary><blockquote>

<details>
<summary>source/app/business/events.py (1)</summary>

* `events_delete` (106-111)

</details>
<details>
<summary>source/app/iris_engine/module_handler/module_handler.py (1)</summary>

* `call_deprecated_on_preload_modules_hook` (584-587)

</details>
<details>
<summary>source/app/blueprints/rest/endpoints.py (1)</summary>

* `endpoint_deprecated` (76-86)

</details>
<details>
<summary>source/app/blueprints/responses.py (1)</summary>

* `response_error` (48-54)

</details>
<details>
<summary>source/app/schema/marshables.py (2)</summary>

* `EventSchema` (1132-1284)
* `validate_date` (1159-1185)

</details>

</blockquote></details>
<details>
<summary>source/app/blueprints/rest/v2/case_objects/events.py (5)</summary><blockquote>

<details>
<summary>source/app/business/events.py (1)</summary>

* `events_delete` (106-111)

</details>
<details>
<summary>source/app/schema/marshables.py (2)</summary>

* `EventSchema` (1132-1284)
* `validate_date` (1159-1185)

</details>
<details>
<summary>source/app/business/cases.py (1)</summary>

* `cases_exists` (71-72)

</details>
<details>
<summary>source/app/iris_engine/access_control/utils.py (1)</summary>

* `ac_fast_check_current_user_has_case_access` (320-321)

</details>
<details>
<summary>source/app/models/authorization.py (1)</summary>

* `CaseAccessLevel` (38-45)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>source/app/iris_engine/module_handler/module_handler.py</summary>

468-468: Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling

(B904)

</details>
<details>
<summary>source/app/schema/utils.py</summary>

35-36: Use a single `if` statement instead of nested `if` statements

Combine `if` statements using `and`

(SIM102)

---

40-41: Use a single `if` statement instead of nested `if` statements

(SIM102)

---

45-46: Use a single `if` statement instead of nested `if` statements

(SIM102)

</details>
<details>
<summary>source/app/business/events.py</summary>

53-53: Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.

Replace `setattr` with assignment

(B010)

---

65-65: Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.

Replace `setattr` with assignment

(B010)

---

88-88: Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.

Replace `setattr` with assignment

(B010)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (51)</summary><blockquote>

<details>
<summary>source/app/blueprints/rest/v2/case_objects/evidences.py (1)</summary>

`141-142`: **Proper security check implementation.**

The evidences_delete call is now correctly positioned after the access control checks, which ensures that evidence will only be deleted when the user has appropriate access to the case. This prevents unauthorized deletion of evidence and aligns with the principle of complete authorization before performing sensitive operations.

</details>
<details>
<summary>tests/tests_rest_evidences.py (1)</summary>

`303-313`: **Good test case for unauthorized evidence deletion.**

This test appropriately verifies that evidence remains intact when a user without permissions attempts to delete it. It complements the fix in the `delete_evidence` function, ensuring that unauthorized deletion attempts are properly blocked while still returning an appropriate response code.

</details>
<details>
<summary>source/app/datamgmt/case/case_events_db.py (4)</summary>

`354-356`: **Good refactoring of parameter passing.**

The function now extracts the case ID from the event object rather than requiring it as a separate parameter. This makes the interface more consistent and less prone to errors where mismatched case IDs might be provided.

---

`358-361`: **Updated query to use case_identifier from event.**

The filtering condition now correctly uses the case_identifier variable extracted from the event object, maintaining consistency with the function signature change.

---

`363-366`: **Updated query to use case_identifier from event.**

Similar to the previous segment, this filtering condition is now correctly using the case_identifier variable from the event.

---

`382-382`: **Updated parameter passing to match refactored function signature.**

The call to update_timeline_state now correctly passes the case_identifier extracted from the event, completing the refactoring of the function parameter signature.

</details>
<details>
<summary>source/app/iris_engine/module_handler/module_handler.py (2)</summary>

`31-31`: **Standardized logger import.**

Replacing the local logger variable with a direct import from app.logger improves code consistency and maintainability.

---

`584-588`: **Added helper function for deprecated hooks.**

This new function properly handles deprecated hooks by adding a warning log while maintaining backward compatibility. This is a good approach for phasing out deprecated functionality.

</details>
<details>
<summary>source/app/schema/marshables.py (1)</summary>

`96-99`: **Reorganized imports look good.**  
These imports from `app.schema.utils` are consistent with the new modular structure. No concerns here.

</details>
<details>
<summary>source/app/blueprints/rest/case/case_timeline_routes.py (15)</summary>

`24-24`: **Improved explicit import for ValidationError.**  
This direct import from `marshmallow.exceptions` is clear and standard.

---

`77-78`: **Added imports for new event-deletion logic.**  
Bringing in `events_delete` and `call_deprecated_on_preload_modules_hook` aligns with the refactoring.

---

`160-160`: **Proper exception handling.**  
Catching `ValidationError` here is a clean approach for reporting data issues.

---

`629-629`: **Great use of deprecation decorator.**  
Using `@endpoint_deprecated` properly guides clients to the new DELETE API route.

---

`639-639`: **Refined event deletion call.**  
The switch to `events_delete` centralizes the deletion logic in the business layer.

---

`693-694`: **Preload hook invoked before update.**  
No issues found. The call to a deprecated hook is explicitly signaled.

---

`695-701`: **Extracted event fields and invoked _load.**  
Assigning `event_id` and then calling `_load` keeps the logic consistent. Looks good.

---

`702-702`: **Updated event via the new events_update function.**  
Centralizing update logic in the business layer fosters maintainability.

---

`714-715`: **Better error feedback for Marshmallow ValidationError.**  
Using `e.get_message()` and `e.get_data()` clarifies the issue for the client.

---

`726-727`: **Preload hook invoked before create.**  
Matches the pattern used in update. No concerns.

---

`728-728`: **_load used to parse the event data.**  
Good approach for consistent data loading.

---

`730-734`: **Pulling relevant fields from request_data.**  
Straightforward retrieval of event parameters.

---

`735-735`: **Creating event via events_create in the business layer.**  
This is consistent with the new architecture.

---

`809-809`: **Catching ValidationError for duplication scenario.**  
Still consistent with the other flows; no issues.

---

`1009-1015`: **Helper `_load` function fosters code reuse.**  
This centralized approach to schema validation ensures consistency across create and update flows.

</details>
<details>
<summary>source/app/business/events.py (11)</summary>

`33-34`: **Importing the new delete function.**  
Centralizing deletion logic through `delete_event` is a good dependency injection.

---

`39-39`: **Enhanced events_create signature.**  
This explicit approach clarifies which parameters are handled, improving readability.

---

`51-51`: **Saving event category upon creation.**  
Makes sense to keep category logic at the business layer.

---

`67-67`: **Post-creation module hook.**  
No issues spotted. The event is passed to external hooks for further processing.

---

`80-80`: **Refined signature for events_update.**  
Similarly clarifies what parameters are used for updating.

---

`81-84`: **Committed changes quickly after partial updates.**  
No problem, though consider batching if performance is a concern.

---

`86-86`: **Category saving for an update.**  
Ensures category is maintained or changed properly.

---

`90-99`: **Updating event assets & IOCs.**  
Catching and raising `BusinessProcessingError` for failures is clear.

---

`100-100`: **Post-update module hook.**  
Consistent approach to external integration.

---

`102-103`: **Track changes with an activity entry.**  
Fine approach for logging updated events.

---

`106-112`: **New events_delete function centralizes removal logic.**  
Covers deletion steps, calls module hook, notifies collaborators, and tracks activity.

</details>
<details>
<summary>source/app/blueprints/rest/v2/case_objects/events.py (9)</summary>

`21-21`: **New imports look good and consistent.**
No issues found with the introduced imports for `ValidationError`, `response_api_deleted`, `events_delete`, and `call_deprecated_on_preload_modules_hook`.  



Also applies to: 26-26, 33-33, 42-42

---

`45-49`: **Encapsulating event logic in `Events` class is a solid design choice.**
The introduction of this class centralizes event logic, improving maintainability and readability.

---

`51-53`: **Error handling for mismatched case identifiers is correct.**
Raising `BusinessProcessingError` here is consistent with the rest of the codebase, and the 400 error is handled properly in higher-level exception handling.

---

`61-86`: **Overall `create` method implementation is robust.**
- Properly checks case existence and access.
- Uses hooks for data pre-processing.  
- Raises `ValidationError` with a clear message.

No further concerns here.

---

`87-103`: **`get` method gracefully handles missing or restricted events.**
The checks for case existence, user access, and event mismatch are consistent and thorough.

---

`104-135`: **`update` method logic is well-structured.**
- Good use of `try` blocks for business logic.  
- The updates follow the same pattern as creation, ensuring consistency.

---

`136-153`: **`delete` method is properly secured and straightforward.**
- Checks existence and access before calling `events_delete`.  
- Returns a 204-equivalent response (`response_api_deleted()`).

---

`155-156`: **Instantiation and blueprint creation are appropriate.**
No issues with the blueprint registration or the `events = Events()` assignment.

---

`161-162`: **Route definitions align with the new class-based logic.**
The mapping of HTTP verbs to class methods is cleanly handled.  



Also applies to: 168-169, 172-175, 177-179, 180-181

</details>
<details>
<summary>tests/tests_rest_events.py (7)</summary>

`362-362`: **Good addition of new test cases for deletion scenarios.**
This improves overall coverage and ensures correct API behavior for event deletion.

---

`363-372`: **`test_delete_event_should_return_204`**  
Correctly validates that a successful deletion returns a 204 status code.

---

`373-383`: **`test_get_event_should_return_404_after_it_has_been_deleted`**  
Adequately confirms that a deleted event cannot be retrieved.

---

`384-393`: **`test_delete_event_should_return_404_when_case_does_not_exist`**  
Ensures proper handling of invalid case identifiers.

---

`394-398`: **`test_delete_event_should_return_404_when_the_event_does_not_exist`**  
Verifies that deleting a non-existent event responds with 404, as expected.

---

`399-410`: **`test_delete_event_should_return_403_when_user_has_no_permission_to_access_case`**  
Checks that the API enforces access control, which is crucial for data security.

---

`411-420`: **`test_delete_event_should_return_400_when_case_identifier_does_not_match_event_case`**  
Confirms correct handling when the case identifier is mismatched, aligning with `_check_event_and_case_identifier_match()`.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-14T07:18:00Z

Review comments:
diff_hunk: @@ -1,5 +1,5 @@
 [tool.ruff.lint]
 preview = true
-select = ["E225", "E23", "E24", "E3", "E4", "E7", "E9", "F", "UP032", "W29"]
+select = ["E225", "E23", "E24", "E3", "E4", "E7", "E9", "F", "RET506", "UP032", "W29"]
path: pyproject.toml
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Linting Rule Configuration: Verify Replacement Intent**

The commit summary mentions that `"RET506"` should replace the previous `"UP032"` rule. However, both `"RET506"` and `"UP032"` are still present in the `select` list. Please confirm if `"UP032"` should be removed to fully implement the intended change.

```diff
-select = ["E225", "E23", "E24", "E3", "E4", "E7", "E9", "F", "RET506", "UP032", "W29"]
+select = ["E225", "E23", "E24", "E3", "E4", "E7", "E9", "F", "RET506", "W29"]
```

---

Below is a shell script to verify that `"UP032"` is still present in the `pyproject.toml` file:



Based on the expected output from the above script, it confirms that `"UP032"` remains in the configuration. The commit summary indicated that `"RET506"` should replace `"UP032"`, so having both in the list is not in line with the intended change.

**Action Required: Remove "UP032" from lint rules**

- **File:** `pyproject.toml` (Line 3)
- **Current State:**
  ```
  select = ["E225", "E23", "E24", "E3", "E4", "E7", "E9", "F", "RET506", "UP032", "W29"]
  ```
- **Intended State (as per commit summary):**
  ```diff
  -select = ["E225", "E23", "E24", "E3", "E4", "E7", "E9", "F", "RET506", "UP032", "W29"]
  +select = ["E225", "E23", "E24", "E3", "E4", "E7", "E9", "F", "RET506", "W29"]
  ```

Please update the file accordingly to fully implement the intended linting rule replacement.

---

🏁 Script executed:

```shell

#!/bin/bash
# Search for UP032 in pyproject.toml
rg '"UP032"' pyproject.toml
```

Length of output: 238

---



</details>

**Update Linting Rule Configuration in pyproject.toml**

The current configuration still includes both `"RET506"` and `"UP032"`, but the commit summary specifies that `"RET506"` should fully replace `"UP032"`. Our verification confirms that `"UP032"` is present in the file.

- **File:** `pyproject.toml` (Line 3)
- **Current State:**
  ```
  select = ["E225", "E23", "E24", "E3", "E4", "E7", "E9", "F", "RET506", "UP032", "W29"]
  ```
- **Proposed Change:**
  ```diff
  -select = ["E225", "E23", "E24", "E3", "E4", "E7", "E9", "F", "RET506", "UP032", "W29"]
  +select = ["E225", "E23", "E24", "E3", "E4", "E7", "E9", "F", "RET506", "W29"]
  ```

Please update the configuration to remove `"UP032"` and ensure that the linting configuration reflects the intended replacement.



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
select = ["E225", "E23", "E24", "E3", "E4", "E7", "E9", "F", "RET506", "W29"]
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-14T07:17:59Z
author_association: NONE
diff_hunk: @@ -465,10 +464,10 @@ def task_hook_wrapper(self, module_name, hook_name, hook_ui_name, data, init_use
             _obj.append(_obj_a)
 
     except Exception as e:
-        log.exception(e)
+        logger.exception(e)
         raise Exception(e)
path: source/app/iris_engine/module_handler/module_handler.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Use `raise from` for better exception chaining.**

When re-raising exceptions from an except block, it's better to use `raise ... from e` to preserve the exception context.

```diff
- logger.exception(e)
- raise Exception(e)
+ logger.exception(e)
+ raise Exception(e) from e
```

This approach preserves the exception traceback, making debugging easier.


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

468-468: Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling

(B904)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-14T07:17:59Z
author_association: NONE
diff_hunk: @@ -0,0 +1,93 @@
+#  IRIS Source Code
+#  Copyright (C) 2025 - 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.
+
+import hashlib
+from pathlib import Path
+from marshmallow import ValidationError
+
+from app.logger import logger
+
+
+def assert_type_mml(input_var: any, field_name: str, type: type, allow_none: bool = False,
+                    max_len: int = None, max_val: int = None, min_val: int = None):
+    if input_var is None:
+        if allow_none is False:
+            raise ValidationError('Invalid data - non null expected',
+                                  field_name=field_name if field_name else 'type')
+        return True
+
+    if isinstance(input_var, type):
+        if max_len:
+            if len(input_var) > max_len:
+                raise ValidationError('Invalid data - max length exceeded',
+                                      field_name=field_name if field_name else 'type')
+
+        if max_val:
+            if input_var > max_val:
+                raise ValidationError('Invalid data - max value exceeded',
+                                                  field_name=field_name if field_name else 'type')
+
+        if min_val:
+            if input_var < min_val:
+                raise ValidationError('Invalid data - min value exceeded',
+                                                  field_name=field_name if field_name else 'type')
+
+        return True
+
+    try:
+
+        if isinstance(type(input_var), type):
+            return True
+
path: source/app/schema/utils.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Suspicious type check always returning True.**  
`if isinstance(type(input_var), type):` will virtually always be `True`, which makes the final `return True` unconditional. This may be a logical bug, as it bypasses validation if the earlier checks don’t match. Consider verifying the intended type check, for example:
```python
if not isinstance(input_var, the_expected_type):
    raise ValidationError(...)
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-14T07:17:59Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-11T12:47:13Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request updates several components of the application. The linter configuration in `pyproject.toml` has been modified. Two server management routes and their associated templates were removed. The error handling and data loading logic in case timeline routes have been refactored, and event-related functionality has been encapsulated into a new `Events` class. Additionally, the business logic functions for events have been updated for a parameterized design, and utility functions have been reorganized by moving them to a new schema utilities module while removing legacy implementations. New tests have also been added for event and evidence deletion scenarios.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `pyproject.toml` | Updated linting configuration: replaced `"UP032"` with `"RET506"` in the `select` list. |
| `src/app/.../manage_srv_settings_routes.py`<br>`src/app/.../manage_make_update.html`<br>`src/app/.../modal_server_updates.html` | Removed two server management routes and deleted the associated update templates. |
| `src/app/.../case_timeline_routes.py` | Refactored error handling; added helper method `_load` to centralize event data loading and validation. |
| `src/app/.../events.py` | Introduced the `Events` class with methods for creating, retrieving, updating, and deleting case events; moved identifier match check into the class scope. |
| `src/app/.../evidences.py` | Adjusted control flow in `delete_evidence` to perform access checks before deletion. |
| `src/app/business/events.py` | Updated the signatures of `events_create` and `events_update` to work with parameterized inputs; added new `events_delete` function. |
| `src/app/.../case_events_db.py` | Modified `delete_event` to remove the external caseid parameter and use the event’s own `case_id` attribute. |
| `src/app/.../module_handler.py` | Replaced local logger variable with an imported `logger`; added a new function `call_deprecated_on_preload_modules_hook` for logging deprecation warnings. |
| `src/app/schema/marshables.py` | Updated import paths from `app.util` to `app/schema/utils` for several utility functions. |
| `src/app/schema/utils.py` | New file added with functions: `assert_type_mml`, `str_to_bool`, `stream_sha256sum`, and `file_sha256sum`. |
| `src/app/util.py` | Removed several utility functions (`decompress_7z`, `file_sha256sum`, `stream_sha256sum`, `str_to_bool`, `assert_type_mml`) and associated imports. |
| `tests/tests_rest_events.py` | Added multiple new test methods covering event deletion scenarios with various response expectations (204, 404, 403, 400). |
| `tests/tests_rest_evidences.py` | Added test to ensure evidence is not removed when the user lacks proper access rights. |
| `ui/src/pages/updates.handler.js` | Removed update management functions, variables, and document ready logic associated with server updates. |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
    participant U as User
    participant API as REST Endpoint
    participant E as Events Class
    participant B as Business Logic
    participant DB as Database

    U->>API: Request deletion of event
    API->>E: Call delete(event, case_identifier)
    E->>B: Validate event, permissions, and case match
    B->>DB: Execute deletion query
    DB-->>B: Confirm deletion
    B-->>E: Return success status
    E-->>API: Return 204 No Content response
    API-->>U: Send response (204)
```

## Poem
> I’m a bunny, hopping with glee,  
> Changes abound, as plain as can be.  
> Routes removed, and events now neat,  
> New utils and tests make the code complete.  
> I nibble on updates with a cheerful beat!  
> 🐇✨ Happy coding in every leap!

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡💬 Agentic Chat (Pro Plan, General Availability)</summary>
> 
> - We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=dfir-iris/iris-web&utm_content=776):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: whikernel
created_at: 2025-04-14T07:14:11Z
body: @coderabbitai review
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-04-14T07:14:16Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>:white_check_mark: Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: NONE