Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5daa778
Started implementation of PUT /api/v2/cases/{case_identifier}/events/…
c8y3 Apr 4, 2025
c4c6c5c
PUT /api/v2/cases/{case_identifier}/events/{identifier} should update…
c8y3 Apr 4, 2025
20cde85
Updated test
c8y3 Apr 4, 2025
2c1b9b2
Syntax formatting
c8y3 Apr 4, 2025
b93b90e
Made import more precise
c8y3 Apr 4, 2025
686b1cd
Correctly renamed method
c8y3 Apr 4, 2025
1436664
PUT /api/v2/cases/{case_identifier}/events/{identifier} should send a…
c8y3 Apr 8, 2025
4dbdb39
Simplified test using socket_io
c8y3 Apr 8, 2025
3d9f519
[FIX] Put back missing socket event handlers on notes
c8y3 Apr 8, 2025
f33cd2d
[FIX] Put back missing socket event handlers
c8y3 Apr 8, 2025
5260b94
PUT /api/v2/cases/{case_identifier}/events/{identifier} should return…
c8y3 Apr 9, 2025
639e477
POST /api/v2/cases/{case_identifier}/events should send socketIO noti…
c8y3 Apr 9, 2025
ce7ebd9
Socket IO event sent during event update should directly be a json in…
c8y3 Apr 9, 2025
c8842cb
PUT /api/v2/cases/{case_identifier}/events/{identifier} should return…
c8y3 Apr 9, 2025
bfc0e08
PUT /api/v2/cases/{case_identifier}/events/{identifier} should return…
c8y3 Apr 9, 2025
3a6d665
PUT /api/v2/cases/{case_identifier}/events/{identifier} should return…
c8y3 Apr 9, 2025
bcc48f8
PUT /api/v2/cases/{case_identifier}/events/{identifier} should return…
c8y3 Apr 9, 2025
e433e5a
Deprecated POST /case/timeline/events/update/{cur_id} in favor of PUT…
c8y3 Apr 9, 2025
0b5c533
PUT /api/v2/cases/{case_identifier}/events/{identifier}: added tests …
c8y3 Apr 9, 2025
dccef51
PUT /api/v2/cases/{case_identifier}/events/{identifier}: added test w…
c8y3 Apr 9, 2025
99fb848
Grouped socket_io event handlers
c8y3 Apr 9, 2025
4bacdee
Grouped update socketIO event handlers in namespace app.blueprints.so…
c8y3 Apr 9, 2025
06dae26
Introduced methods to register socketIO event handlers, so as to avoi…
c8y3 Apr 9, 2025
1c216da
Fixed ruff
c8y3 Apr 9, 2025
f758388
Import logger directly
c8y3 Apr 9, 2025
87992ce
Fixed an occurence of ruff RET505
c8y3 Apr 9, 2025
0f514fa
Added type
c8y3 Apr 9, 2025
89d5bfb
Fixed an occurence of Ruff RET505 warning
c8y3 Apr 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions source/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,11 @@ def after_request(response):

lm.user_loader(load_user)
lm.request_loader(load_user_from_request)

from app.blueprints.socket_io_event_handlers.case_event_handlers import register_case_event_handlers
from app.blueprints.socket_io_event_handlers.case_notes_event_handlers import register_notes_event_handlers
from app.blueprints.socket_io_event_handlers.update_event_handlers import register_update_event_handlers

register_case_event_handlers()
register_notes_event_handlers()
register_update_event_handlers()
44 changes: 7 additions & 37 deletions source/app/blueprints/rest/case/case_timeline_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
from app.blueprints.responses import response_success
from app.business.errors import BusinessProcessingError
from app.business.events import events_create
from app.business.events import events_update

case_timeline_rest_blueprint = Blueprint('case_timeline_rest', __name__)

Expand Down Expand Up @@ -684,6 +685,7 @@ def event_view(cur_id, caseid):


@case_timeline_rest_blueprint.route('/case/timeline/events/update/<int:cur_id>', methods=['POST'])
@endpoint_deprecated('PUT', '/api/v2/cases/{case_identifier}/events/{identifier}')
@ac_requires_case_identifier(CaseAccessLevel.full_access)
@ac_api_requires()
def case_edit_event(cur_id, caseid):
Expand All @@ -692,42 +694,10 @@ def case_edit_event(cur_id, caseid):
if not event:
return response_error("Invalid event ID for this case")

event_schema = EventSchema()

request_data = call_modules_hook('on_preload_event_update', data=request.get_json(), caseid=caseid)
request_json = request.get_json()
event = events_update(event, request_json)

request_data['event_id'] = cur_id
event = event_schema.load(request_data, instance=event)

event.event_date, event.event_date_wtz = event_schema.validate_date(
request_data.get(u'event_date'),
request_data.get(u'event_tz')
)

event.case_id = caseid
add_obj_history_entry(event, 'updated')

update_timeline_state(caseid=caseid)
db.session.commit()

save_event_category(event.event_id, request_data.get('event_category_id'))

setattr(event, 'event_category_id', request_data.get('event_category_id'))

success, log = update_event_assets(event.event_id, caseid, request_data.get('event_assets'),
request_data.get('event_iocs'), request_data.get('event_sync_iocs_assets'))
if not success:
return response_error('Error while saving linked assets', data=log)

success, log = update_event_iocs(event_id=event.event_id,
caseid=caseid,
iocs_list=request_data.get('event_iocs'))
if not success:
return response_error('Error while saving linked iocs', data=log)

event = call_modules_hook('on_postload_event_update', data=event, caseid=caseid)

track_activity(f"updated event \"{event.event_title}\"", caseid=caseid)
event_schema = EventSchema()
event_dump = event_schema.dump(event)
collab_notify(case_id=caseid,
object_type='events',
Expand All @@ -737,8 +707,8 @@ def case_edit_event(cur_id, caseid):

return response_success("Event updated", data=event_dump)

except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.normalized_messages())
except BusinessProcessingError as e:
return response_error(e.get_message(), data=e.get_data())


@case_timeline_rest_blueprint.route('/case/timeline/events/add', methods=['POST'])
Expand Down
9 changes: 4 additions & 5 deletions source/app/blueprints/rest/v2/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from app import app
from app import db
from app import oidc_client
from app.logger import logger
from app.blueprints.access_controls import is_authentication_ldap
from app.blueprints.access_controls import is_authentication_oidc
from app.blueprints.access_controls import not_authenticated_redirection_url
Expand All @@ -37,18 +38,16 @@

auth_blueprint = Blueprint('auth', __name__, url_prefix='/auth')

log = app.logger


@auth_blueprint.post('/login')
def login():
"""
Login endpoint. Handles taking user/pass combo and authenticating a local session or returning an error.
"""
log.info('Authenticating user')
logger.info('Authenticating user')
if current_user.is_authenticated:
log.info('User already authenticated - redirecting')
log.debug(f'User {current_user.user} already logged in')
logger.info('User already authenticated - redirecting')
logger.debug(f'User {current_user.user} already logged in')
user = return_authed_user_info(user_id=current_user.id)
return response_api_success(data=user)

Expand Down
34 changes: 32 additions & 2 deletions source/app/blueprints/rest/v2/case_objects/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@
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.models.cases import CasesEvent
from app.schema.marshables import EventSchema
from app.business.errors import BusinessProcessingError
from app.business.errors import ObjectNotFoundError
from app.business.cases import cases_exists
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


Expand All @@ -41,7 +43,7 @@

@case_events_blueprint.post('')
@ac_api_requires()
def create_evidence(case_identifier):
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]):
Expand All @@ -50,7 +52,10 @@ def create_evidence(case_identifier):
try:
event = events_create(case_identifier, request.get_json())
schema = EventSchema()
return response_api_created(schema.dump(event))
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())

Expand All @@ -77,6 +82,31 @@ def get_event(case_identifier, identifier):
return response_api_error(e.get_message(), data=e.get_data())


@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())

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}')
Original file line number Diff line number Diff line change
Expand Up @@ -26,33 +26,43 @@
from app.models.authorization import CaseAccessLevel


@socket_io.on('change')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_summary_onchange(data):

data['last_change'] = current_user.user
emit('change', data, to=data['channel'], skip_sid=request.sid)


@socket_io.on('save')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_summary_onsave(data):

data['last_saved'] = current_user.user
emit('save', data, to=data['channel'], skip_sid=request.sid)


@socket_io.on('clear_buffer')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_summary_on_clear_buffer(message):

emit('clear_buffer', message)


@socket_io.on('join')
@ac_socket_requires(CaseAccessLevel.full_access)
def get_message(data):

room = data['channel']
join_room(room=room)
emit('join', {'message': f"{current_user.user} just joined"}, room=room)


@ac_socket_requires(CaseAccessLevel.full_access)
def socket_join_case_obj_notif(data):
room = data['channel']
join_room(room=room)


def register_case_event_handlers():
socket_io.on_event('change', socket_summary_onchange)
socket_io.on_event('save', socket_summary_onsave)
socket_io.on_event('clear_buffer', socket_summary_on_clear_buffer)
socket_io.on_event('join', get_message)
socket_io.on_event('join-case-obj-notif', socket_join_case_obj_notif)
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,26 @@
from app.models.authorization import CaseAccessLevel


@socket_io.on('change-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_change_note(data):

data['last_change'] = current_user.user
emit('change-note', data, to=data['channel'], skip_sid=request.sid, room=data['channel'])


@socket_io.on('save-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_save_note(data):

data['last_saved'] = current_user.user
emit('save-note', data, to=data['channel'], skip_sid=request.sid, room=data['channel'])


@socket_io.on('clear_buffer-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_clear_buffer_note(message):

emit('clear_buffer-note', message, room=message['channel'])


@socket_io.on('join-notes')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_join_note(data):

Expand All @@ -62,28 +58,24 @@ def socket_join_note(data):
}, room=room)


@socket_io.on('ping-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_ping_note(data):

emit('ping-note', {"user": current_user.name, "note_id": data['note_id']}, room=data['channel'])


@socket_io.on('pong-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_pong_note(data):

emit('pong-note', {"user": current_user.name, "note_id": data['note_id']}, room=data['channel'])


@socket_io.on('overview-map-note')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_overview_map_note(data):

emit('overview-map-note', {"user": current_user.user, "note_id": data['note_id']}, room=data['channel'])


@socket_io.on('join-notes-overview')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_join_overview(data):

Expand All @@ -96,7 +88,18 @@ def socket_join_overview(data):
}, room=room)


@socket_io.on('disconnect')
@ac_socket_requires(CaseAccessLevel.full_access)
def socket_disconnect(data):
emit('disconnect', current_user.user, broadcast=True)


def register_notes_event_handlers():
socket_io.on_event('change-note', socket_change_note)
socket_io.on_event('save-note', socket_save_note)
socket_io.on_event('clear_buffer-note', socket_clear_buffer_note)
socket_io.on_event('join-notes', socket_join_note)
socket_io.on_event('ping-note', socket_ping_note)
socket_io.on_event('pong-note', socket_pong_note)
socket_io.on_event('overview-map-note', socket_overview_map_note)
socket_io.on_event('join-notes-overview', socket_join_overview)
socket_io.on_event('disconnect', socket_disconnect)
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# IRIS Source Code
# Copyright (C) 2024 - DFIR-IRIS
# contact@dfir-iris.org
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from flask_login import current_user
from flask_socketio import emit
from flask_socketio import join_room

from app import socket_io
from app import app


def get_message(data):
room = data['channel']
join_room(room=room)

emit('join', {'message': f"{current_user.user} just joined", 'is_error': False}, room=room,
namespace='/server-updates')


def socket_on_update_ping(msg):
emit('update_ping', {'message': "Server connected", 'is_error': False},
namespace='/server-updates')


def socket_on_update_do_reboot(msg):
socket_io.emit('update_current_version', {"version": app.config.get('IRIS_VERSION')}, to='iris_update_status',
namespace='/server-updates')


def register_update_event_handlers():
socket_io.on_event('join-update', get_message, namespace='/server-updates')
socket_io.on_event('update_ping', socket_on_update_ping, namespace='/server-updates')
socket_io.on_event('update_get_current_version', socket_on_update_do_reboot, namespace='/server-updates')
12 changes: 3 additions & 9 deletions source/app/business/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,20 @@
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

import datetime
from app.logger import logger
import traceback

from flask_login import current_user

from marshmallow.exceptions import ValidationError

from app import db

from app.logger import logger
from app.util import add_obj_history_entry
from app.schema.marshables import CaseSchema

from app.models.models import ReviewStatusList

from app.business.errors import BusinessProcessingError
from app.business.iocs import iocs_exports_to_json

from app.iris_engine.module_handler.module_handler import call_modules_hook
from app.iris_engine.utils.tracker import track_activity
from app.iris_engine.access_control.utils import ac_set_new_case_access

from app.datamgmt.case.case_db import case_db_exists
from app.datamgmt.case.case_db import save_case_tags
from app.datamgmt.case.case_db import register_case_protagonists
Expand All @@ -60,9 +53,10 @@
from app.datamgmt.reporter.report_db import export_case_tasks_json
from app.datamgmt.reporter.report_db import export_case_comments_json
from app.datamgmt.reporter.report_db import export_case_notes_json
from app.models.cases import Cases


def _load(request_data, **kwargs):
def _load(request_data, **kwargs) -> Cases:
try:
add_case_schema = CaseSchema()
return add_case_schema.load(request_data, **kwargs)
Expand Down
Loading