From 5aa1920d3b3f6c1185ef2c4f16fcd4bc95d5c87d Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 1 Mar 2026 14:14:04 -0700 Subject: [PATCH 1/6] feat(reports): add task comments to investigation export (#692) --- source/app/datamgmt/reporter/report_db.py | 24 ++++++++++++++++++- .../variable_task_comments.md | 5 ++++ tests/tests_rest_reports.py | 17 +++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tests/data/report_templates/variable_task_comments.md diff --git a/source/app/datamgmt/reporter/report_db.py b/source/app/datamgmt/reporter/report_db.py index 5ea6897c8..adccd12a1 100644 --- a/source/app/datamgmt/reporter/report_db.py +++ b/source/app/datamgmt/reporter/report_db.py @@ -30,7 +30,7 @@ from app.models.models import CaseTasks from app.models.cases import Cases from app.models.cases import CasesEvent -from app.models.comments import Comments +from app.models.comments import Comments, TaskComments from app.models.models import EventCategory from app.models.iocs import Ioc from app.models.models import IocAssetLink @@ -328,11 +328,33 @@ def export_case_tasks_json(case_id): 'id': member.id }) task['task_assignees'] = assignee_list.get(task['id'], []) + task['comments'] = export_case_task_comments_json(task_id) task_with_assignees.append(task) return task_with_assignees +def export_case_task_comments_json(task_id): + comments = Comments.query.with_entities( + Comments.comment_id, + Comments.comment_uuid, + Comments.comment_text, + User.name.label('comment_by'), + Comments.comment_date + ).filter( + TaskComments.comment_task_id == task_id + ).join( + TaskComments, + Comments.comment_id == TaskComments.comment_id + ).join( + Comments.user + ).order_by( + Comments.comment_date.asc() + ).all() + + return [row._asdict() for row in comments] + + def export_case_assets_json(case_id): ret = [] diff --git a/tests/data/report_templates/variable_task_comments.md b/tests/data/report_templates/variable_task_comments.md new file mode 100644 index 000000000..2163cd4bb --- /dev/null +++ b/tests/data/report_templates/variable_task_comments.md @@ -0,0 +1,5 @@ +{% for task in tasks %} +{% for comment in task.comments %} +{{ comment.comment_text }} +{% endfor %} +{% endfor %} diff --git a/tests/tests_rest_reports.py b/tests/tests_rest_reports.py index 8b9e07794..e5276bf22 100644 --- a/tests/tests_rest_reports.py +++ b/tests/tests_rest_reports.py @@ -81,3 +81,20 @@ def test_generate_md_activities_report_should_render_variable_case_for_customer_ response = self._subject.get(f'/case/report/generate-activities/{report_identifier}', {'cid': case_identifier, 'safe': True}) self.assertEqual('IrisInitialClient (legacy::use client.customer_name)', response.text) + + def test_generate_md_report_should_render_task_comments(self): + data = {'report_name': 'name', 'report_type': 1, 'report_language': 1, 'report_description': 'description', + 'report_name_format': 'report_name_format'} + report_identifier = self._subject.create_report(data, 'variable_task_comments.md') + case_identifier = self._subject.create_dummy_case() + + task_data = {'task_assignees_id': [], 'task_description': '', 'task_status_id': 1, 'task_tags': '', + 'task_title': 'dummy title', 'custom_attributes': {}} + task_response = self._subject.create(f'/api/v2/cases/{case_identifier}/tasks', task_data).json() + task_identifier = task_response['id'] + comment_text = 'task comment for report export' + self._subject.create(f'/api/v2/tasks/{task_identifier}/comments', {'comment_text': comment_text}) + + response = self._subject.get(f'/case/report/generate-investigation/{report_identifier}', + {'cid': case_identifier, 'safe': True}) + self.assertIn(comment_text, response.text) From 1131644ae0564ebe5e5654caaf8354d6a8080560 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 1 Mar 2026 14:23:04 -0700 Subject: [PATCH 2/6] feat(reports): add asset comments to investigation export --- source/app/datamgmt/reporter/report_db.py | 25 ++++++++++++++++++- .../variable_asset_comments.md | 5 ++++ tests/tests_rest_reports.py | 16 ++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tests/data/report_templates/variable_asset_comments.md diff --git a/source/app/datamgmt/reporter/report_db.py b/source/app/datamgmt/reporter/report_db.py index adccd12a1..015a8fd4e 100644 --- a/source/app/datamgmt/reporter/report_db.py +++ b/source/app/datamgmt/reporter/report_db.py @@ -30,7 +30,7 @@ from app.models.models import CaseTasks from app.models.cases import Cases from app.models.cases import CasesEvent -from app.models.comments import Comments, TaskComments +from app.models.comments import Comments, TaskComments, AssetComments from app.models.models import EventCategory from app.models.iocs import Ioc from app.models.models import IocAssetLink @@ -401,6 +401,8 @@ def export_case_assets_json(case_id): else: row['asset_ioc'] = [] + row['comments'] = export_case_asset_comments_json(row['asset_id']) + if row['asset_compromise_status_id'] is None: row['asset_compromise_status_id'] = CompromiseStatus.unknown.value status_text = CompromiseStatus.unknown.name.replace('_', ' ').title() @@ -414,6 +416,27 @@ def export_case_assets_json(case_id): return ret +def export_case_asset_comments_json(asset_id): + comments = Comments.query.with_entities( + Comments.comment_id, + Comments.comment_uuid, + Comments.comment_text, + User.name.label('comment_by'), + Comments.comment_date + ).filter( + AssetComments.comment_asset_id == asset_id + ).join( + AssetComments, + Comments.comment_id == AssetComments.comment_id + ).join( + Comments.user + ).order_by( + Comments.comment_date.asc() + ).all() + + return [row._asdict() for row in comments] + + def export_case_comments_json(case_id): comments = Comments.query.with_entities( Comments.comment_id, diff --git a/tests/data/report_templates/variable_asset_comments.md b/tests/data/report_templates/variable_asset_comments.md new file mode 100644 index 000000000..e057bf13c --- /dev/null +++ b/tests/data/report_templates/variable_asset_comments.md @@ -0,0 +1,5 @@ +{% for asset in assets %} +{% for comment in asset.comments %} +{{ comment.comment_text }} +{% endfor %} +{% endfor %} diff --git a/tests/tests_rest_reports.py b/tests/tests_rest_reports.py index e5276bf22..b85b53826 100644 --- a/tests/tests_rest_reports.py +++ b/tests/tests_rest_reports.py @@ -98,3 +98,19 @@ def test_generate_md_report_should_render_task_comments(self): response = self._subject.get(f'/case/report/generate-investigation/{report_identifier}', {'cid': case_identifier, 'safe': True}) self.assertIn(comment_text, response.text) + + def test_generate_md_report_should_render_asset_comments(self): + data = {'report_name': 'name', 'report_type': 1, 'report_language': 1, 'report_description': 'description', + 'report_name_format': 'report_name_format'} + report_identifier = self._subject.create_report(data, 'variable_asset_comments.md') + case_identifier = self._subject.create_dummy_case() + + asset_data = {'asset_type_id': 1, 'asset_name': 'asset with comments'} + asset_response = self._subject.create(f'/api/v2/cases/{case_identifier}/assets', asset_data).json() + asset_identifier = asset_response['asset_id'] + comment_text = 'asset comment for report export' + self._subject.create(f'/api/v2/assets/{asset_identifier}/comments', {'comment_text': comment_text}) + + response = self._subject.get(f'/case/report/generate-investigation/{report_identifier}', + {'cid': case_identifier, 'safe': True}) + self.assertIn(comment_text, response.text) From 392512a172f81bac06c6518c7f5b309a41a7ecee Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 1 Mar 2026 14:45:26 -0700 Subject: [PATCH 3/6] feat(reports): add IOC comments to investigation export (#692) --- source/app/business/iocs.py | 31 +++++++++++++++++-- .../report_templates/variable_ioc_comments.md | 5 +++ tests/tests_rest_reports.py | 16 ++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 tests/data/report_templates/variable_ioc_comments.md diff --git a/source/app/business/iocs.py b/source/app/business/iocs.py index 7247a991d..a109f7407 100644 --- a/source/app/business/iocs.py +++ b/source/app/business/iocs.py @@ -31,6 +31,8 @@ from app.iris_engine.utils.tracker import track_activity from app.models.errors import BusinessProcessingError from app.models.errors import ObjectNotFoundError +from app.models.authorization import User +from app.models.comments import Comments, IocComments from app.datamgmt.case.case_iocs_db import get_ioc from app.util import add_obj_history_entry from app.datamgmt.case.case_iocs_db import get_filtered_iocs @@ -106,8 +108,33 @@ def iocs_delete(ioc: Ioc): def iocs_exports_to_json(case_id): iocs = get_iocs(case_id) - - return IocSchema().dump(iocs, many=True) + serialized_iocs = IocSchema().dump(iocs, many=True) + + for ioc in serialized_iocs: + ioc['comments'] = _ioc_comments_export_to_json(ioc['ioc_id']) + + return serialized_iocs + + +def _ioc_comments_export_to_json(ioc_id): + comments = Comments.query.with_entities( + Comments.comment_id, + Comments.comment_uuid, + Comments.comment_text, + User.name.label('comment_by'), + Comments.comment_date + ).filter( + IocComments.comment_ioc_id == ioc_id + ).join( + IocComments, + Comments.comment_id == IocComments.comment_id + ).join( + Comments.user + ).order_by( + Comments.comment_date.asc() + ).all() + + return [row._asdict() for row in comments] def iocs_build_filter_query(ioc_id: int = None, diff --git a/tests/data/report_templates/variable_ioc_comments.md b/tests/data/report_templates/variable_ioc_comments.md new file mode 100644 index 000000000..f0e211cf8 --- /dev/null +++ b/tests/data/report_templates/variable_ioc_comments.md @@ -0,0 +1,5 @@ +{% for ioc in iocs %} +{% for comment in ioc.comments %} +{{ comment.comment_text }} +{% endfor %} +{% endfor %} diff --git a/tests/tests_rest_reports.py b/tests/tests_rest_reports.py index b85b53826..059eb61f2 100644 --- a/tests/tests_rest_reports.py +++ b/tests/tests_rest_reports.py @@ -114,3 +114,19 @@ def test_generate_md_report_should_render_asset_comments(self): response = self._subject.get(f'/case/report/generate-investigation/{report_identifier}', {'cid': case_identifier, 'safe': True}) self.assertIn(comment_text, response.text) + + def test_generate_md_report_should_render_ioc_comments(self): + data = {'report_name': 'name', 'report_type': 1, 'report_language': 1, 'report_description': 'description', + 'report_name_format': 'report_name_format'} + report_identifier = self._subject.create_report(data, 'variable_ioc_comments.md') + case_identifier = self._subject.create_dummy_case() + + ioc_data = {'ioc_type_id': 1, 'ioc_tlp_id': 2, 'ioc_value': '8.8.8.8', 'ioc_description': '', 'ioc_tags': ''} + ioc_response = self._subject.create(f'/api/v2/cases/{case_identifier}/iocs', ioc_data).json() + ioc_identifier = ioc_response['ioc_id'] + comment_text = 'ioc comment for report export' + self._subject.create(f'/api/v2/iocs/{ioc_identifier}/comments', {'comment_text': comment_text}) + + response = self._subject.get(f'/case/report/generate-investigation/{report_identifier}', + {'cid': case_identifier, 'safe': True}) + self.assertIn(comment_text, response.text) From cd56518c784fa0d4b5e0af43bb4e68dcad652fdb Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 1 Mar 2026 14:53:11 -0700 Subject: [PATCH 4/6] feat(reports): add event comments to investigation export (#692) --- source/app/datamgmt/reporter/report_db.py | 24 ++++++++++++++++++- .../variable_event_comments.md | 5 ++++ tests/tests_rest_reports.py | 18 ++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tests/data/report_templates/variable_event_comments.md diff --git a/source/app/datamgmt/reporter/report_db.py b/source/app/datamgmt/reporter/report_db.py index 015a8fd4e..da7d393b5 100644 --- a/source/app/datamgmt/reporter/report_db.py +++ b/source/app/datamgmt/reporter/report_db.py @@ -30,7 +30,7 @@ from app.models.models import CaseTasks from app.models.cases import Cases from app.models.cases import CasesEvent -from app.models.comments import Comments, TaskComments, AssetComments +from app.models.comments import Comments, TaskComments, AssetComments, EventComments from app.models.models import EventCategory from app.models.iocs import Ioc from app.models.models import IocAssetLink @@ -272,6 +272,7 @@ def export_case_tm_json(case_id): ).all() ras['iocs'] = [ioc._asdict() for ioc in iocs_list] + ras['comments'] = export_case_event_comments_json(row.event_id) tim.append(ras) @@ -355,6 +356,27 @@ def export_case_task_comments_json(task_id): return [row._asdict() for row in comments] +def export_case_event_comments_json(event_id): + comments = Comments.query.with_entities( + Comments.comment_id, + Comments.comment_uuid, + Comments.comment_text, + User.name.label('comment_by'), + Comments.comment_date + ).filter( + EventComments.comment_event_id == event_id + ).join( + EventComments, + Comments.comment_id == EventComments.comment_id + ).join( + Comments.user + ).order_by( + Comments.comment_date.asc() + ).all() + + return [row._asdict() for row in comments] + + def export_case_assets_json(case_id): ret = [] diff --git a/tests/data/report_templates/variable_event_comments.md b/tests/data/report_templates/variable_event_comments.md new file mode 100644 index 000000000..76ceaf091 --- /dev/null +++ b/tests/data/report_templates/variable_event_comments.md @@ -0,0 +1,5 @@ +{% for event in timeline %} +{% for comment in event.comments %} +{{ comment.comment_text }} +{% endfor %} +{% endfor %} diff --git a/tests/tests_rest_reports.py b/tests/tests_rest_reports.py index 059eb61f2..83becd3f5 100644 --- a/tests/tests_rest_reports.py +++ b/tests/tests_rest_reports.py @@ -130,3 +130,21 @@ def test_generate_md_report_should_render_ioc_comments(self): response = self._subject.get(f'/case/report/generate-investigation/{report_identifier}', {'cid': case_identifier, 'safe': True}) self.assertIn(comment_text, response.text) + + def test_generate_md_report_should_render_event_comments(self): + data = {'report_name': 'name', 'report_type': 1, 'report_language': 1, 'report_description': 'description', + 'report_name_format': 'report_name_format'} + report_identifier = self._subject.create_report(data, 'variable_event_comments.md') + case_identifier = self._subject.create_dummy_case() + + event_data = {'event_title': 'title', 'event_category_id': 1, + 'event_date': '2025-03-26T00:00:00.000', 'event_tz': '+00:00', + 'event_assets': [], 'event_iocs': []} + event_response = self._subject.create(f'/api/v2/cases/{case_identifier}/events', event_data).json() + event_identifier = event_response['event_id'] + comment_text = 'event comment for report export' + self._subject.create(f'/api/v2/events/{event_identifier}/comments', {'comment_text': comment_text}) + + response = self._subject.get(f'/case/report/generate-investigation/{report_identifier}', + {'cid': case_identifier, 'safe': True}) + self.assertIn(comment_text, response.text) From e969ab01cf33f302f66385e832606c124f966426 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 1 Mar 2026 14:58:33 -0700 Subject: [PATCH 5/6] feat(reports): add note comments to investigation export (#692) --- source/app/datamgmt/reporter/report_db.py | 31 ++++++++++++++----- .../variable_note_comments.md | 5 +++ tests/tests_rest_reports.py | 19 ++++++++++++ 3 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 tests/data/report_templates/variable_note_comments.md diff --git a/source/app/datamgmt/reporter/report_db.py b/source/app/datamgmt/reporter/report_db.py index da7d393b5..2a305b2ed 100644 --- a/source/app/datamgmt/reporter/report_db.py +++ b/source/app/datamgmt/reporter/report_db.py @@ -21,7 +21,6 @@ from sqlalchemy import desc from app.datamgmt.case.case_notes_db import get_notes_from_group -from app.datamgmt.case.case_notes_db import get_case_note_comments from app.models.assets import CompromiseStatus, AssetsType, CaseAssets, AnalysisStatus from app.models.models import TaskAssignee from app.models.models import CaseEventsAssets @@ -30,7 +29,7 @@ from app.models.models import CaseTasks from app.models.cases import Cases from app.models.cases import CasesEvent -from app.models.comments import Comments, TaskComments, AssetComments, EventComments +from app.models.comments import Comments, TaskComments, AssetComments, EventComments, NotesComments from app.models.models import EventCategory from app.models.iocs import Ioc from app.models.models import IocAssetLink @@ -41,7 +40,6 @@ from app.models.iocs import Tlp from app.models.authorization import User from app.schema.marshables import CaseDetailsSchema -from app.schema.marshables import CommentSchema from app.schema.marshables import CaseNoteSchema @@ -185,16 +183,14 @@ def export_case_notes_json(case_id): Notes.note_case_id == case_id ).all() - # Initialize the schemas + # Initialize the schema note_schema = CaseNoteSchema() - comments_schema = CommentSchema(many=True) # Serialize the notes and their comments serialized_notes = [] for note in notes: - note_comments = get_case_note_comments(note.note_id) serialized_note = note_schema.dump(note) - serialized_note['comments'] = comments_schema.dump(note_comments) + serialized_note['comments'] = export_case_note_comments_json(note.note_id) serialized_note['note_content'] = process_md_images_links_for_report(serialized_note['note_content']) serialized_notes.append(serialized_note) @@ -202,6 +198,27 @@ def export_case_notes_json(case_id): return serialized_notes +def export_case_note_comments_json(note_id): + comments = Comments.query.with_entities( + Comments.comment_id, + Comments.comment_uuid, + Comments.comment_text, + User.name.label('comment_by'), + Comments.comment_date + ).filter( + NotesComments.comment_note_id == note_id + ).join( + NotesComments, + Comments.comment_id == NotesComments.comment_id + ).join( + Comments.user + ).order_by( + Comments.comment_date.asc() + ).all() + + return [row._asdict() for row in comments] + + def export_case_tm_json(case_id): timeline = CasesEvent.query.with_entities( CasesEvent.event_id, diff --git a/tests/data/report_templates/variable_note_comments.md b/tests/data/report_templates/variable_note_comments.md new file mode 100644 index 000000000..9c9895117 --- /dev/null +++ b/tests/data/report_templates/variable_note_comments.md @@ -0,0 +1,5 @@ +{% for note in notes %} +{% for comment in note.comments %} +{{ comment.comment_text }} +{% endfor %} +{% endfor %} diff --git a/tests/tests_rest_reports.py b/tests/tests_rest_reports.py index 83becd3f5..eca54665d 100644 --- a/tests/tests_rest_reports.py +++ b/tests/tests_rest_reports.py @@ -148,3 +148,22 @@ def test_generate_md_report_should_render_event_comments(self): response = self._subject.get(f'/case/report/generate-investigation/{report_identifier}', {'cid': case_identifier, 'safe': True}) self.assertIn(comment_text, response.text) + + def test_generate_md_report_should_render_note_comments(self): + data = {'report_name': 'name', 'report_type': 1, 'report_language': 1, 'report_description': 'description', + 'report_name_format': 'report_name_format'} + report_identifier = self._subject.create_report(data, 'variable_note_comments.md') + case_identifier = self._subject.create_dummy_case() + + directory_response = self._subject.create(f'/api/v2/cases/{case_identifier}/notes-directories', + {'name': 'directory_name'}).json() + directory_identifier = directory_response['id'] + note_response = self._subject.create(f'/api/v2/cases/{case_identifier}/notes', + {'directory_id': directory_identifier}).json() + note_identifier = note_response['note_id'] + comment_text = 'note comment for report export' + self._subject.create(f'/api/v2/notes/{note_identifier}/comments', {'comment_text': comment_text}) + + response = self._subject.get(f'/case/report/generate-investigation/{report_identifier}', + {'cid': case_identifier, 'safe': True}) + self.assertIn(comment_text, response.text) From 458139488f4543e9d3a7d0f25ce3b7acf07ab077 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 1 Mar 2026 15:03:35 -0700 Subject: [PATCH 6/6] feat(reports): add evidence comments to investigation export (#692) --- source/app/datamgmt/reporter/report_db.py | 30 +++++++++++++++++-- .../variable_evidence_comments.md | 5 ++++ tests/tests_rest_reports.py | 16 ++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 tests/data/report_templates/variable_evidence_comments.md diff --git a/source/app/datamgmt/reporter/report_db.py b/source/app/datamgmt/reporter/report_db.py index 2a305b2ed..5bb51eda9 100644 --- a/source/app/datamgmt/reporter/report_db.py +++ b/source/app/datamgmt/reporter/report_db.py @@ -29,7 +29,7 @@ from app.models.models import CaseTasks from app.models.cases import Cases from app.models.cases import CasesEvent -from app.models.comments import Comments, TaskComments, AssetComments, EventComments, NotesComments +from app.models.comments import Comments, TaskComments, AssetComments, EventComments, NotesComments, EvidencesComments from app.models.models import EventCategory from app.models.iocs import Ioc from app.models.models import IocAssetLink @@ -171,12 +171,38 @@ def export_case_evidences_json(case_id): ).all() if evidences: + serialized_evidences = [] + for row in evidences: + serialized_evidence = row._asdict() + serialized_evidence['comments'] = export_case_evidence_comments_json(serialized_evidence['id']) + serialized_evidences.append(serialized_evidence) - return [row._asdict() for row in evidences] + return serialized_evidences return [] +def export_case_evidence_comments_json(evidence_id): + comments = Comments.query.with_entities( + Comments.comment_id, + Comments.comment_uuid, + Comments.comment_text, + User.name.label('comment_by'), + Comments.comment_date + ).filter( + EvidencesComments.comment_evidence_id == evidence_id + ).join( + EvidencesComments, + Comments.comment_id == EvidencesComments.comment_id + ).join( + Comments.user + ).order_by( + Comments.comment_date.asc() + ).all() + + return [row._asdict() for row in comments] + + def export_case_notes_json(case_id): # Fetch all notes associated with the case notes = Notes.query.filter( diff --git a/tests/data/report_templates/variable_evidence_comments.md b/tests/data/report_templates/variable_evidence_comments.md new file mode 100644 index 000000000..e212394bf --- /dev/null +++ b/tests/data/report_templates/variable_evidence_comments.md @@ -0,0 +1,5 @@ +{% for evidence in evidences %} +{% for comment in evidence.comments %} +{{ comment.comment_text }} +{% endfor %} +{% endfor %} diff --git a/tests/tests_rest_reports.py b/tests/tests_rest_reports.py index eca54665d..c3520522d 100644 --- a/tests/tests_rest_reports.py +++ b/tests/tests_rest_reports.py @@ -167,3 +167,19 @@ def test_generate_md_report_should_render_note_comments(self): response = self._subject.get(f'/case/report/generate-investigation/{report_identifier}', {'cid': case_identifier, 'safe': True}) self.assertIn(comment_text, response.text) + + def test_generate_md_report_should_render_evidence_comments(self): + data = {'report_name': 'name', 'report_type': 1, 'report_language': 1, 'report_description': 'description', + 'report_name_format': 'report_name_format'} + report_identifier = self._subject.create_report(data, 'variable_evidence_comments.md') + case_identifier = self._subject.create_dummy_case() + + evidence_response = self._subject.create(f'/api/v2/cases/{case_identifier}/evidences', + {'filename': 'filename'}).json() + evidence_identifier = evidence_response['id'] + comment_text = 'evidence comment for report export' + self._subject.create(f'/api/v2/evidences/{evidence_identifier}/comments', {'comment_text': comment_text}) + + response = self._subject.get(f'/case/report/generate-investigation/{report_identifier}', + {'cid': case_identifier, 'safe': True}) + self.assertIn(comment_text, response.text)