From 54932da93d7bbebfe7c6663d71ec5a297fc5c76e Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:56:01 +0200 Subject: [PATCH 01/38] Moved code into persistence layer --- .../rest/case/case_timeline_routes.py | 17 ++++------------- source/app/datamgmt/case/case_assets_db.py | 17 ++++++++++++++--- source/app/datamgmt/case/case_events_db.py | 12 +++++++++++- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/source/app/blueprints/rest/case/case_timeline_routes.py b/source/app/blueprints/rest/case/case_timeline_routes.py index 79c8f6625..cd41a065e 100644 --- a/source/app/blueprints/rest/case/case_timeline_routes.py +++ b/source/app/blueprints/rest/case/case_timeline_routes.py @@ -32,7 +32,9 @@ from app.blueprints.rest.endpoints import endpoint_deprecated from app.blueprints.iris_user import iris_current_user from app.datamgmt.case.case_assets_db import get_asset_by_name +from app.datamgmt.case.case_assets_db import get_assets_by_case from app.datamgmt.case.case_events_db import add_comment_to_event +from app.datamgmt.case.case_events_db import get_events_by_case 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_comment @@ -178,19 +180,8 @@ def case_get_timeline_state(caseid): @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access) @ac_api_requires() def case_getgraph_assets(caseid): - assets_cache = CaseAssets.query.with_entities( - CaseEventsAssets.event_id, - CaseAssets.asset_name - ).filter( - CaseEventsAssets.case_id == caseid, - ).join(CaseEventsAssets.asset).all() - - timeline = CasesEvent.query.filter(and_( - CasesEvent.case_id == caseid, - CasesEvent.event_in_summary - )).order_by( - CasesEvent.event_date - ).all() + assets_cache = get_assets_by_case(caseid) + timeline = get_events_by_case(caseid) tim = [] for row in timeline: diff --git a/source/app/datamgmt/case/case_assets_db.py b/source/app/datamgmt/case/case_assets_db.py index 1abc66e3c..50791de5d 100644 --- a/source/app/datamgmt/case/case_assets_db.py +++ b/source/app/datamgmt/case/case_assets_db.py @@ -23,17 +23,19 @@ from sqlalchemy import func from flask_sqlalchemy.pagination import Pagination -from app import db, app +from app import db +from app import app from app.blueprints.iris_user import iris_current_user from app.datamgmt.filtering import get_filtered_data from app.datamgmt.states import update_assets_state -from app.models.models import AnalysisStatus +from app.models.models import AnalysisStatus, CaseAssets, CaseEventsAssets from app.models.models import CaseStatus from app.models.models import AssetsType from app.models.models import CaseAssets from app.models.models import CaseEventsAssets from app.models.cases import Cases -from app.models.comments import Comments, AssetComments +from app.models.comments import Comments +from app.models.comments import AssetComments from app.models.models import CompromiseStatus from app.models.iocs import Ioc from app.models.models import IocAssetLink @@ -106,6 +108,15 @@ def get_assets(case_identifier): return assets +def get_assets_by_case(case_identifier): + return CaseAssets.query.with_entities( + CaseEventsAssets.event_id, + CaseAssets.asset_name + ).filter( + CaseEventsAssets.case_id == case_identifier, + ).join(CaseEventsAssets.asset).all() + + def filter_assets(case_identifier, pagination_parameters: PaginationParameters, request_parameters: dict) -> Pagination: base_filter = CaseAssets.case_id == case_identifier return get_filtered_data(CaseAssets, base_filter, pagination_parameters, request_parameters, relationship_model_map) diff --git a/source/app/datamgmt/case/case_events_db.py b/source/app/datamgmt/case/case_events_db.py index fd45b8118..de41e8586 100644 --- a/source/app/datamgmt/case/case_events_db.py +++ b/source/app/datamgmt/case/case_events_db.py @@ -15,6 +15,7 @@ # 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 sqlalchemy import and_ from app import db @@ -26,7 +27,8 @@ from app.models.models import CaseEventsAssets from app.models.models import CaseEventsIoc from app.models.cases import CasesEvent -from app.models.comments import Comments, EventComments +from app.models.comments import Comments +from app.models.comments import EventComments from app.models.models import EventCategory from app.models.iocs import Ioc from app.models.models import IocAssetLink @@ -412,3 +414,11 @@ def get_default_category(): EventCategory.name == "Unspecified" ).first() + +def get_events_by_case(case_identifier): + return CasesEvent.query.filter(and_( + CasesEvent.case_id == case_identifier, + CasesEvent.event_in_summary + )).order_by( + CasesEvent.event_date + ).all() From 8c18603d39cd2703d9a41a7ba8b3d9642531ee01 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:57:41 +0200 Subject: [PATCH 02/38] Import logger directly --- source/app/datamgmt/case/case_assets_db.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/source/app/datamgmt/case/case_assets_db.py b/source/app/datamgmt/case/case_assets_db.py index 50791de5d..2fb112602 100644 --- a/source/app/datamgmt/case/case_assets_db.py +++ b/source/app/datamgmt/case/case_assets_db.py @@ -24,11 +24,11 @@ from flask_sqlalchemy.pagination import Pagination from app import db -from app import app +from app.logger import logger from app.blueprints.iris_user import iris_current_user from app.datamgmt.filtering import get_filtered_data from app.datamgmt.states import update_assets_state -from app.models.models import AnalysisStatus, CaseAssets, CaseEventsAssets +from app.models.models import AnalysisStatus from app.models.models import CaseStatus from app.models.models import AssetsType from app.models.models import CaseAssets @@ -44,9 +44,6 @@ from app.models.pagination_parameters import PaginationParameters -log = app.logger - - relationship_model_map = { 'case': Cases, 'user': User, @@ -289,7 +286,7 @@ def set_ioc_links(ioc_list, asset_id): db.session.commit() except Exception as e: db.session.rollback() - log.exception(e) + logger.exception(e) return True, e.__str__() return False, "" From 2f8e1ce1e1f2ce41cf464ff4c7e684b2c2ea7095 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:00:19 +0200 Subject: [PATCH 03/38] Reuse method --- source/app/blueprints/rest/case/case_timeline_routes.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/source/app/blueprints/rest/case/case_timeline_routes.py b/source/app/blueprints/rest/case/case_timeline_routes.py index cd41a065e..8a4fe76f1 100644 --- a/source/app/blueprints/rest/case/case_timeline_routes.py +++ b/source/app/blueprints/rest/case/case_timeline_routes.py @@ -207,12 +207,7 @@ def case_getgraph_assets(caseid): @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access) @ac_api_requires() def case_getgraph(caseid): - timeline = CasesEvent.query.filter(and_( - CasesEvent.case_id == caseid, - CasesEvent.event_in_summary - )).order_by( - CasesEvent.event_date - ).all() + timeline = get_events_by_case(caseid) tim = [] for row in timeline: From a6ef96220528ea6ed37735271d102fb5b6624ea5 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:50:35 +0200 Subject: [PATCH 04/38] Extracted method --- .../rest/case/case_timeline_routes.py | 92 ++++++++++--------- 1 file changed, 50 insertions(+), 42 deletions(-) diff --git a/source/app/blueprints/rest/case/case_timeline_routes.py b/source/app/blueprints/rest/case/case_timeline_routes.py index 8a4fe76f1..a5ef0a5f9 100644 --- a/source/app/blueprints/rest/case/case_timeline_routes.py +++ b/source/app/blueprints/rest/case/case_timeline_routes.py @@ -345,6 +345,11 @@ def case_filter_timeline(caseid): assets = filter_d.get('asset') assets_id = filter_d.get('asset_id') event_ids = filter_d.get('event_id') + if event_ids: + try: + event_ids = [int(event_id) for event_id in event_ids] + except Exception as _: + return response_error('Invalid event id') iocs = filter_d.get('ioc') iocs_id = filter_d.get('ioc_id') tags = filter_d.get('tag') @@ -357,6 +362,46 @@ def case_filter_timeline(caseid): sources = filter_d.get('source') flag = filter_d.get('flag') + cache, events_list, tim = _extract_timeline(assets, assets_id, caseid, categories, descriptions, end_date, event_ids, + flag, iocs, iocs_id, raws, sources, start_date, tags, titles) + + if request.cookies.get('session'): + + iocs = Ioc.query.with_entities( + Ioc.ioc_id, + Ioc.ioc_value, + Ioc.ioc_description, + ).filter( + Ioc.case_id == caseid + ).all() + + events_comments_map = {} + events_comments_set = get_case_events_comments_count(events_list) + for k, v in events_comments_set: + events_comments_map.setdefault(k, []).append(v) + + resp = { + "tim": tim, + "comments_map": events_comments_map, + "assets": cache, + "iocs": [ioc._asdict() for ioc in iocs], + "categories": [cat.name for cat in get_events_categories()], + "state": get_timeline_state(caseid=caseid) + } + + else: + resp = { + "timeline": tim, + "state": get_timeline_state(caseid=caseid) + } + + return response_success("ok", data=resp) + + +def _extract_timeline(assets: str | None, assets_id: str | None, caseid, categories: str | None, + descriptions: str | None, end_date: str | None, event_ids: list[int] | None, + flag: str | None, iocs: str | None, iocs_id: str | None, raws: str | None, sources: str | None, + start_date: str | None, tags: str | None, titles: str | None): condition = (CasesEvent.case_id == caseid) if assets: @@ -423,11 +468,6 @@ def case_filter_timeline(caseid): EventCategory.name == category) if event_ids: - try: - event_ids = [int(event_id) for event_id in event_ids] - except Exception as _: - return response_error('Invalid event id') - condition = and_(condition, CasesEvent.event_id.in_(event_ids)) @@ -477,7 +517,7 @@ def case_filter_timeline(caseid): ).filter( assets_cache_condition ).join(CaseEventsAssets.asset) - .join(CaseAssets.asset_type).all()) + .join(CaseAssets.asset_type).all()) iocs_cache_condition = and_( CaseEventsIoc.case_id == caseid @@ -507,8 +547,7 @@ def case_filter_timeline(caseid): if asset.asset_id not in cache: cache[asset.asset_id] = [asset.asset_name, asset.type] - if (assets and asset.asset_name.lower() in assets) \ - or (assets_id and asset.asset_id in assets_id): + if (assets and asset.asset_name.lower() in assets) or (assets_id and asset.asset_id in assets_id): if asset.event_id in assets_map: assets_map[asset.event_id] += 1 else: @@ -535,10 +574,10 @@ def case_filter_timeline(caseid): events_list = [] for row in timeline: if (assets is not None or assets_id is not None) and row.event_id not in assets_filter: - continue + continue if iocs is not None and row.event_id not in iocs_filter: - continue + continue ras = row._asdict() @@ -580,38 +619,7 @@ def case_filter_timeline(caseid): ras['iocs'] = alki tim.append(ras) - - if request.cookies.get('session'): - - iocs = Ioc.query.with_entities( - Ioc.ioc_id, - Ioc.ioc_value, - Ioc.ioc_description, - ).filter( - Ioc.case_id == caseid - ).all() - - events_comments_map = {} - events_comments_set = get_case_events_comments_count(events_list) - for k, v in events_comments_set: - events_comments_map.setdefault(k, []).append(v) - - resp = { - "tim": tim, - "comments_map": events_comments_map, - "assets": cache, - "iocs": [ioc._asdict() for ioc in iocs], - "categories": [cat.name for cat in get_events_categories()], - "state": get_timeline_state(caseid=caseid) - } - - else: - resp = { - "timeline": tim, - "state": get_timeline_state(caseid=caseid) - } - - return response_success("ok", data=resp) + return cache, events_list, tim @case_timeline_rest_blueprint.route('/case/timeline/events/delete/', methods=['POST']) From c205de728850e70073e187401e71f56b3b98a012 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:58:51 +0200 Subject: [PATCH 05/38] Started implementation of POST /api/v2/manage/customers --- .../rest/manage/manage_customers_routes.py | 12 +++--- source/app/blueprints/rest/v2/manage.py | 3 +- .../rest/v2/manage_routes/customers.py | 43 +++++++++++++++++++ .../rest/v2/manage_routes/groups.py | 10 ++--- .../blueprints/rest/v2/manage_routes/users.py | 3 +- source/app/datamgmt/client/client_db.py | 9 +--- tests/tests_rest_customers.py | 4 ++ 7 files changed, 64 insertions(+), 20 deletions(-) create mode 100644 source/app/blueprints/rest/v2/manage_routes/customers.py diff --git a/source/app/blueprints/rest/manage/manage_customers_routes.py b/source/app/blueprints/rest/manage/manage_customers_routes.py index 3568ebf8f..4e1d9b069 100644 --- a/source/app/blueprints/rest/manage/manage_customers_routes.py +++ b/source/app/blueprints/rest/manage/manage_customers_routes.py @@ -244,22 +244,24 @@ def add_customers(): if not request.is_json: return response_error("Invalid request") + customer_schema = CustomerSchema() try: - client = create_client(request.json) + customer = customer_schema.load(request.json) + + customer = create_client(customer) except ValidationError as e: return response_error(msg='Error adding customer', data=e.messages) except Exception as e: print(traceback.format_exc()) return response_error(f'An error occurred during customer addition. {e}') - track_activity(f"Added customer {client.name}", ctx_less=True) + track_activity(f"Added customer {customer.name}", ctx_less=True) # Associate the created customer with the current user - add_user_to_customer(iris_current_user.id, client.client_id) + add_user_to_customer(iris_current_user.id, customer.client_id) # Return the customer - client_schema = CustomerSchema() - return response_success("Added successfully", data=client_schema.dump(client)) + return response_success('Added successfully', data=customer_schema.dump(customer)) @manage_customers_rest_blueprint.route('/manage/customers/delete/', methods=['POST']) diff --git a/source/app/blueprints/rest/v2/manage.py b/source/app/blueprints/rest/v2/manage.py index 40a80b287..08f024935 100644 --- a/source/app/blueprints/rest/v2/manage.py +++ b/source/app/blueprints/rest/v2/manage.py @@ -20,10 +20,11 @@ from app.blueprints.rest.v2.manage_routes.groups import create_groups_blueprint from app.blueprints.rest.v2.manage_routes.users import users_blueprint +from app.blueprints.rest.v2.manage_routes.customers import customers_blueprint manage_v2_blueprint = Blueprint('manage', __name__, url_prefix='/manage') groups_blueprint = create_groups_blueprint() manage_v2_blueprint.register_blueprint(groups_blueprint) manage_v2_blueprint.register_blueprint(users_blueprint) - +manage_v2_blueprint.register_blueprint(customers_blueprint) diff --git a/source/app/blueprints/rest/v2/manage_routes/customers.py b/source/app/blueprints/rest/v2/manage_routes/customers.py new file mode 100644 index 000000000..169c75494 --- /dev/null +++ b/source/app/blueprints/rest/v2/manage_routes/customers.py @@ -0,0 +1,43 @@ +# 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. + +from flask import Blueprint + + +from app.blueprints.rest.endpoints import response_api_created +from app.blueprints.access_controls import ac_api_requires +from app.models.authorization import Permissions +from app.schema.marshables import CustomerSchema + + +class Customers: + + def __init__(self): + self._schema = CustomerSchema() + + def create(self): + return response_api_created(None) + +customers_blueprint = Blueprint('customers_rest_v2', __name__, url_prefix='/customers') + +customers = Customers() + +@customers_blueprint.post('') +@ac_api_requires(Permissions.customers_write) +def create_customer(): + return customers.create() diff --git a/source/app/blueprints/rest/v2/manage_routes/groups.py b/source/app/blueprints/rest/v2/manage_routes/groups.py index f01b5826e..ba6ba727d 100644 --- a/source/app/blueprints/rest/v2/manage_routes/groups.py +++ b/source/app/blueprints/rest/v2/manage_routes/groups.py @@ -31,7 +31,8 @@ from app.business.groups import groups_get from app.business.groups import groups_update from app.business.groups import groups_delete -from app.models.authorization import Permissions, ac_flag_match_mask +from app.models.authorization import Permissions +from app.models.authorization import ac_flag_match_mask from app.business.errors import BusinessProcessingError from app.business.errors import ObjectNotFoundError from app.blueprints.iris_user import iris_current_user @@ -43,13 +44,10 @@ class Groups: def __init__(self): self._schema = AuthorizationGroupSchema() - def _load(self, request_data, **kwargs): - return self._schema.load(request_data, **kwargs) - def create(self): try: request_data = request.get_json() - group = self._load(request_data) + group = self._schema.load(request_data) group = groups_create(group) result = self._schema.dump(group) return response_api_created(result) @@ -69,7 +67,7 @@ def update(self, identifier): group = groups_get(identifier) request_data = request.get_json() request_data['group_id'] = identifier - updated_group = self._load(request_data, instance=group, partial=True) + updated_group = self._schema.load(request_data, instance=group, partial=True) if not ac_flag_match_mask(request_data['group_permissions'], Permissions.server_administrator.value) and ac_ldp_group_update(iris_current_user.id): return response_api_error('That might not be a good idea Dave', data='Update the group permissions will lock you out') groups_update() diff --git a/source/app/blueprints/rest/v2/manage_routes/users.py b/source/app/blueprints/rest/v2/manage_routes/users.py index 025cd5ebc..e2d7c4d9e 100644 --- a/source/app/blueprints/rest/v2/manage_routes/users.py +++ b/source/app/blueprints/rest/v2/manage_routes/users.py @@ -28,7 +28,8 @@ from app.blueprints.rest.endpoints import response_api_deleted from app.schema.marshables import UserSchemaForAPIV2 from app.models.authorization import Permissions -from app.business.errors import ObjectNotFoundError, BusinessProcessingError +from app.business.errors import ObjectNotFoundError +from app.business.errors import BusinessProcessingError from app.business.users import users_create from app.business.users import users_get from app.business.users import users_update diff --git a/source/app/datamgmt/client/client_db.py b/source/app/datamgmt/client/client_db.py index 42437dbc5..33642e74d 100644 --- a/source/app/datamgmt/client/client_db.py +++ b/source/app/datamgmt/client/client_db.py @@ -97,16 +97,11 @@ def get_client_cases(client_id: int): return cases_list -def create_client(data) -> Client: +def create_client(customer: Client): - client_schema = CustomerSchema() - client = client_schema.load(data) - - db.session.add(client) + db.session.add(customer) db.session.commit() - return client - def get_client_contacts(client_id: int) -> List[Contact]: contacts = Contact.query.filter( diff --git a/tests/tests_rest_customers.py b/tests/tests_rest_customers.py index 5ab4361b6..aa24a0247 100644 --- a/tests/tests_rest_customers.py +++ b/tests/tests_rest_customers.py @@ -39,3 +39,7 @@ def test_create_customer_should_return_200_when_user_has_customer_write_right(se response = user.create('/manage/customers/add', body) self.assertEqual(200, response.status_code) + + def test_create_customer_should_return_201(self): + response = self._subject.create('/api/v2/manage/customers', {}) + self.assertEqual(201, response.status_code) \ No newline at end of file From 5335c51d4f91c57668f9aa610e678b6ac2440e9e Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:09:39 +0200 Subject: [PATCH 06/38] Create customer should allow to set customer_name --- .../rest/v2/manage_routes/customers.py | 7 ++++-- source/app/business/customers.py | 24 +++++++++++++++++++ source/app/datamgmt/client/client_db.py | 7 ++++-- tests/tests_rest_customers.py | 7 +++++- 4 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 source/app/business/customers.py diff --git a/source/app/blueprints/rest/v2/manage_routes/customers.py b/source/app/blueprints/rest/v2/manage_routes/customers.py index 169c75494..f1b74dfd7 100644 --- a/source/app/blueprints/rest/v2/manage_routes/customers.py +++ b/source/app/blueprints/rest/v2/manage_routes/customers.py @@ -17,7 +17,7 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from flask import Blueprint - +from flask import request from app.blueprints.rest.endpoints import response_api_created from app.blueprints.access_controls import ac_api_requires @@ -31,7 +31,10 @@ def __init__(self): self._schema = CustomerSchema() def create(self): - return response_api_created(None) + request_data = request.get_json() + customer = self._schema.load(request_data) + result = self._schema.dump(customer) + return response_api_created(result) customers_blueprint = Blueprint('customers_rest_v2', __name__, url_prefix='/customers') diff --git a/source/app/business/customers.py b/source/app/business/customers.py new file mode 100644 index 000000000..f0a3bf5c9 --- /dev/null +++ b/source/app/business/customers.py @@ -0,0 +1,24 @@ +# 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. + +from app.datamgmt.client.client_db import create_client +from app.models.models import Client + + +def customers_create(customer: Client): + create_client(customer) diff --git a/source/app/datamgmt/client/client_db.py b/source/app/datamgmt/client/client_db.py index 33642e74d..ed2f745d0 100644 --- a/source/app/datamgmt/client/client_db.py +++ b/source/app/datamgmt/client/client_db.py @@ -15,8 +15,10 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + import marshmallow -from sqlalchemy import func, and_ +from sqlalchemy import func +from sqlalchemy import and_ from typing import List from app import db @@ -25,7 +27,8 @@ from app.models.cases import Cases from app.models.models import Client from app.models.models import Contact -from app.models.authorization import User, UserClient +from app.models.authorization import User +from app.models.authorization import UserClient from app.schema.marshables import ContactSchema from app.schema.marshables import CustomerSchema diff --git a/tests/tests_rest_customers.py b/tests/tests_rest_customers.py index aa24a0247..01d421947 100644 --- a/tests/tests_rest_customers.py +++ b/tests/tests_rest_customers.py @@ -42,4 +42,9 @@ def test_create_customer_should_return_200_when_user_has_customer_write_right(se def test_create_customer_should_return_201(self): response = self._subject.create('/api/v2/manage/customers', {}) - self.assertEqual(201, response.status_code) \ No newline at end of file + self.assertEqual(201, response.status_code) + + def test_create_customer_should_return_customer_name(self): + body = {'customer_name': 'customer'} + response = self._subject.create('/api/v2/manage/customers', body).json() + self.assertEqual('customer', response['customer_name']) From ee4a875d8ba24c31bbeb845a595f6e62c25a0d7c Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:13:57 +0200 Subject: [PATCH 07/38] Create customer should return 400 when another customer with the same name already exists --- .../rest/v2/manage_routes/customers.py | 16 ++++++++++++---- tests/tests_rest_customers.py | 6 ++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/source/app/blueprints/rest/v2/manage_routes/customers.py b/source/app/blueprints/rest/v2/manage_routes/customers.py index f1b74dfd7..fca1ad9ef 100644 --- a/source/app/blueprints/rest/v2/manage_routes/customers.py +++ b/source/app/blueprints/rest/v2/manage_routes/customers.py @@ -18,11 +18,14 @@ from flask import Blueprint from flask import request +from marshmallow import ValidationError from app.blueprints.rest.endpoints import response_api_created +from app.blueprints.rest.endpoints import response_api_error from app.blueprints.access_controls import ac_api_requires from app.models.authorization import Permissions from app.schema.marshables import CustomerSchema +from app.business.customers import customers_create class Customers: @@ -31,10 +34,15 @@ def __init__(self): self._schema = CustomerSchema() def create(self): - request_data = request.get_json() - customer = self._schema.load(request_data) - result = self._schema.dump(customer) - return response_api_created(result) + try: + request_data = request.get_json() + customer = self._schema.load(request_data) + customers_create(customer) + result = self._schema.dump(customer) + return response_api_created(result) + except ValidationError as e: + return response_api_error('Data error', data=e.messages) + customers_blueprint = Blueprint('customers_rest_v2', __name__, url_prefix='/customers') diff --git a/tests/tests_rest_customers.py b/tests/tests_rest_customers.py index 01d421947..3a41df3bc 100644 --- a/tests/tests_rest_customers.py +++ b/tests/tests_rest_customers.py @@ -48,3 +48,9 @@ def test_create_customer_should_return_customer_name(self): body = {'customer_name': 'customer'} response = self._subject.create('/api/v2/manage/customers', body).json() self.assertEqual('customer', response['customer_name']) + + def test_create_customer_should_return_400_when_another_customer_with_the_same_name_already_exists(self): + body = {'customer_name': 'customer'} + response = self._subject.create('/api/v2/manage/customers', body) + response = self._subject.create('/api/v2/manage/customers', body) + self.assertEqual(400, response.status_code) From 5611cbcab2b4d10378452145e1486efc3431890e Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:17:12 +0200 Subject: [PATCH 08/38] Fixed regression --- source/app/blueprints/rest/manage/manage_customers_routes.py | 2 +- source/app/blueprints/rest/v2/manage_routes/customers.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/source/app/blueprints/rest/manage/manage_customers_routes.py b/source/app/blueprints/rest/manage/manage_customers_routes.py index 4e1d9b069..3928300f1 100644 --- a/source/app/blueprints/rest/manage/manage_customers_routes.py +++ b/source/app/blueprints/rest/manage/manage_customers_routes.py @@ -248,7 +248,7 @@ def add_customers(): try: customer = customer_schema.load(request.json) - customer = create_client(customer) + create_client(customer) except ValidationError as e: return response_error(msg='Error adding customer', data=e.messages) except Exception as e: diff --git a/source/app/blueprints/rest/v2/manage_routes/customers.py b/source/app/blueprints/rest/v2/manage_routes/customers.py index fca1ad9ef..320dd5fe4 100644 --- a/source/app/blueprints/rest/v2/manage_routes/customers.py +++ b/source/app/blueprints/rest/v2/manage_routes/customers.py @@ -48,6 +48,7 @@ def create(self): customers = Customers() + @customers_blueprint.post('') @ac_api_requires(Permissions.customers_write) def create_customer(): From 9fdaf1e187b3b3ca44c567c9173b3a1701bb98fb Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:28:58 +0200 Subject: [PATCH 09/38] POST /api/v2/manage/customers adds an activity --- source/app/business/customers.py | 2 ++ tests/tests_rest_customers.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/source/app/business/customers.py b/source/app/business/customers.py index f0a3bf5c9..c5be5ae84 100644 --- a/source/app/business/customers.py +++ b/source/app/business/customers.py @@ -18,7 +18,9 @@ from app.datamgmt.client.client_db import create_client from app.models.models import Client +from app.iris_engine.utils.tracker import track_activity def customers_create(customer: Client): create_client(customer) + track_activity(f'Added customer {customer.name}', ctx_less=True) diff --git a/tests/tests_rest_customers.py b/tests/tests_rest_customers.py index 3a41df3bc..a98e1e2fc 100644 --- a/tests/tests_rest_customers.py +++ b/tests/tests_rest_customers.py @@ -51,6 +51,12 @@ def test_create_customer_should_return_customer_name(self): def test_create_customer_should_return_400_when_another_customer_with_the_same_name_already_exists(self): body = {'customer_name': 'customer'} - response = self._subject.create('/api/v2/manage/customers', body) + self._subject.create('/api/v2/manage/customers', body) response = self._subject.create('/api/v2/manage/customers', body) self.assertEqual(400, response.status_code) + + def test_create_customer_should_add_an_activity(self): + body = {'customer_name': 'customer_name'} + self._subject.create('/api/v2/manage/customers', body) + last_activity = self._subject.get_latest_activity() + self.assertEqual('Added customer customer_name', last_activity['activity_desc']) \ No newline at end of file From 06d65fd4bef9b460a41edd669eb0aadc2e3cbb3f Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:30:34 +0200 Subject: [PATCH 10/38] Fixed incorrect test --- tests/tests_rest_customers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/tests_rest_customers.py b/tests/tests_rest_customers.py index a98e1e2fc..ff7f4ec3d 100644 --- a/tests/tests_rest_customers.py +++ b/tests/tests_rest_customers.py @@ -41,7 +41,8 @@ def test_create_customer_should_return_200_when_user_has_customer_write_right(se self.assertEqual(200, response.status_code) def test_create_customer_should_return_201(self): - response = self._subject.create('/api/v2/manage/customers', {}) + body = {'customer_name': 'customer'} + response = self._subject.create('/api/v2/manage/customers', body) self.assertEqual(201, response.status_code) def test_create_customer_should_return_customer_name(self): From 5e4b5369404ba467f8ba7dc872b207f97c651fa0 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:44:13 +0200 Subject: [PATCH 11/38] POST /api/v2/manage/customers adds user to customer --- .../blueprints/rest/v2/manage_routes/customers.py | 3 ++- source/app/business/customers.py | 4 +++- source/app/datamgmt/manage/manage_users_db.py | 4 +--- tests/tests_rest_customers.py | 13 ++++++++++++- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/source/app/blueprints/rest/v2/manage_routes/customers.py b/source/app/blueprints/rest/v2/manage_routes/customers.py index 320dd5fe4..830a7a0de 100644 --- a/source/app/blueprints/rest/v2/manage_routes/customers.py +++ b/source/app/blueprints/rest/v2/manage_routes/customers.py @@ -26,6 +26,7 @@ from app.models.authorization import Permissions from app.schema.marshables import CustomerSchema from app.business.customers import customers_create +from app.blueprints.iris_user import iris_current_user class Customers: @@ -37,7 +38,7 @@ def create(self): try: request_data = request.get_json() customer = self._schema.load(request_data) - customers_create(customer) + customers_create(iris_current_user, customer) result = self._schema.dump(customer) return response_api_created(result) except ValidationError as e: diff --git a/source/app/business/customers.py b/source/app/business/customers.py index c5be5ae84..9eeb48cf2 100644 --- a/source/app/business/customers.py +++ b/source/app/business/customers.py @@ -19,8 +19,10 @@ from app.datamgmt.client.client_db import create_client from app.models.models import Client from app.iris_engine.utils.tracker import track_activity +from app.datamgmt.manage.manage_users_db import add_user_to_customer -def customers_create(customer: Client): +def customers_create(user, customer: Client): create_client(customer) track_activity(f'Added customer {customer.name}', ctx_less=True) + add_user_to_customer(user.id, customer.client_id) \ No newline at end of file diff --git a/source/app/datamgmt/manage/manage_users_db.py b/source/app/datamgmt/manage/manage_users_db.py index 54ca288cd..7a536dcf6 100644 --- a/source/app/datamgmt/manage/manage_users_db.py +++ b/source/app/datamgmt/manage/manage_users_db.py @@ -143,7 +143,7 @@ def add_user_to_customer(user_id, customer_id): ).first() if user_client: - return True + return user_client = UserClient() user_client.user_id = user_id @@ -155,8 +155,6 @@ def add_user_to_customer(user_id, customer_id): ac_auto_update_user_effective_access(user_id) - return True - def update_user_customers(user_id, customers): # Update the user's customers directly diff --git a/tests/tests_rest_customers.py b/tests/tests_rest_customers.py index ff7f4ec3d..502a60ddc 100644 --- a/tests/tests_rest_customers.py +++ b/tests/tests_rest_customers.py @@ -19,6 +19,7 @@ from unittest import TestCase from iris import Iris from iris import IRIS_PERMISSION_CUSTOMERS_WRITE +from iris import ADMINISTRATOR_USER_IDENTIFIER class TestsRestCustomers(TestCase): @@ -60,4 +61,14 @@ def test_create_customer_should_add_an_activity(self): body = {'customer_name': 'customer_name'} self._subject.create('/api/v2/manage/customers', body) last_activity = self._subject.get_latest_activity() - self.assertEqual('Added customer customer_name', last_activity['activity_desc']) \ No newline at end of file + self.assertEqual('Added customer customer_name', last_activity['activity_desc']) + + def test_create_customer_should_add_user_to_the_customer(self): + body = {'customer_name': 'customer_name'} + response = self._subject.create('/api/v2/manage/customers', body).json() + identifier = response['customer_id'] + response = self._subject.get(f'/api/v2/manage/users/{ADMINISTRATOR_USER_IDENTIFIER}').json() + user_customers_identifiers = [] + for customer in response['user_customers']: + user_customers_identifiers.append(customer['customer_id']) + self.assertIn(identifier, user_customers_identifiers) From d4a6b4943a5605a8aad3b4b304cc677288563298 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:45:58 +0200 Subject: [PATCH 12/38] Fixed ruff warning --- source/app/business/customers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/app/business/customers.py b/source/app/business/customers.py index 9eeb48cf2..cacb7805d 100644 --- a/source/app/business/customers.py +++ b/source/app/business/customers.py @@ -25,4 +25,4 @@ def customers_create(user, customer: Client): create_client(customer) track_activity(f'Added customer {customer.name}', ctx_less=True) - add_user_to_customer(user.id, customer.client_id) \ No newline at end of file + add_user_to_customer(user.id, customer.client_id) From e36816a83ad6df78b7ad91095ed59ddd4031e83a Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:48:07 +0200 Subject: [PATCH 13/38] Deprecated POST /manage/customers/add --- source/app/blueprints/rest/manage/manage_customers_routes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/app/blueprints/rest/manage/manage_customers_routes.py b/source/app/blueprints/rest/manage/manage_customers_routes.py index 3928300f1..a0678ea95 100644 --- a/source/app/blueprints/rest/manage/manage_customers_routes.py +++ b/source/app/blueprints/rest/manage/manage_customers_routes.py @@ -46,6 +46,7 @@ from app.blueprints.access_controls import ac_api_requires_client_access from app.blueprints.responses import response_error from app.blueprints.responses import response_success +from app.blueprints.rest.endpoints import endpoint_deprecated manage_customers_rest_blueprint = Blueprint('manage_customers_rest', __name__) @@ -239,6 +240,7 @@ def view_customers(client_id): @manage_customers_rest_blueprint.route('/manage/customers/add', methods=['POST']) +@endpoint_deprecated('POST', '/api/v2/manage/customers') @ac_api_requires(Permissions.customers_write) def add_customers(): if not request.is_json: From 70fd435fa1316dcd88e40cfa70dcec967c33df7f Mon Sep 17 00:00:00 2001 From: Elise17 Date: Wed, 1 Oct 2025 16:46:16 +0200 Subject: [PATCH 14/38] Changed branch alert_similarities --- source/app/blueprints/rest/alerts_routes.py | 1 + source/app/blueprints/rest/v2/alerts.py | 35 +++++++++ tests/tests_rest_alerts.py | 87 +++++++++++++++++++++ 3 files changed, 123 insertions(+) diff --git a/source/app/blueprints/rest/alerts_routes.py b/source/app/blueprints/rest/alerts_routes.py index e2e7976e2..87ee0e236 100644 --- a/source/app/blueprints/rest/alerts_routes.py +++ b/source/app/blueprints/rest/alerts_routes.py @@ -257,6 +257,7 @@ def alerts_get_route(alert_id) -> Response: @alerts_rest_blueprint.route('/alerts/similarities/', methods=['GET']) +@endpoint_deprecated('GET', '/api/v2/alerts/{identifier}/related-alerts') @ac_api_requires(Permissions.alerts_read) def alerts_similarities_route(alert_id) -> Response: """ diff --git a/source/app/blueprints/rest/v2/alerts.py b/source/app/blueprints/rest/v2/alerts.py index a7c16a241..c854f6561 100644 --- a/source/app/blueprints/rest/v2/alerts.py +++ b/source/app/blueprints/rest/v2/alerts.py @@ -43,6 +43,8 @@ from app.business.errors import BusinessProcessingError from app.business.errors import ObjectNotFoundError from app.business.access_controls import access_controls_user_has_customer_access +from app.datamgmt.manage.manage_access_control_db import user_has_client_access +from app.datamgmt.alerts.alerts_db import get_related_alerts_details class AlertsOperations: @@ -165,6 +167,33 @@ def get(self, identifier): except ObjectNotFoundError: return response_api_not_found() + def get_related_alerts(self, identifier): + + try: + alert = alerts_get(iris_current_user, identifier) + if not user_has_client_access(iris_current_user.id, alert.alert_customer_id): + return response_api_error('User not entitled to create alerts for the client') + + open_alerts = request.args.get('open-alerts', 'false').lower() == 'true' + open_cases = request.args.get('open-cases', 'false').lower() == 'true' + closed_cases = request.args.get('closed-cases', 'false').lower() == 'true' + closed_alerts = request.args.get('closed-alerts', 'false').lower() == 'true' + days_back = request.args.get('days-back', 180, type=int) + number_of_results = request.args.get('number-of-nodes', 100, type=int) + + if number_of_results < 0: + number_of_results = 100 + if days_back < 0: + days_back = 180 + + similar_alerts = get_related_alerts_details(alert.alert_customer_id, alert.assets, alert.iocs, + open_alerts, closed_alerts, open_cases, closed_cases, + days_back, number_of_results) + return response_api_success(similar_alerts) + + except ObjectNotFoundError: + return response_api_not_found() + def update(self, identifier): try: alert = alerts_get(iris_current_user, identifier) @@ -243,3 +272,9 @@ def update_alert(identifier): @ac_api_requires(Permissions.alerts_delete) def delete_alert(identifier): return alerts_operations.delete(identifier) + + +@alerts_blueprint.get('/related-alerts') +@ac_api_requires(Permissions.alerts_read) +def get_related_alerts(identifier): + return alerts_operations.get_related_alerts(identifier) diff --git a/tests/tests_rest_alerts.py b/tests/tests_rest_alerts.py index 0a8f25bbc..f53ce4d29 100644 --- a/tests/tests_rest_alerts.py +++ b/tests/tests_rest_alerts.py @@ -441,3 +441,90 @@ def test_get_alert_should_return_404_after_delete_alert(self): self._subject.delete(f'/api/v2/alerts/{identifier}') response = self._subject.get(f'/api/v2/alerts/{identifier}') self.assertEqual(404, response.status_code) + + def test_get_related_alerts_should_return_200(self): + body = { + 'alert_title': 'title', + 'alert_severity_id': 4, + 'alert_status_id': 3, + 'alert_customer_id': 1 + } + response = self._subject.create('api/v2/alerts', body).json() + identifier = response['alert_id'] + response = self._subject.get(f'/api/v2/alerts/{identifier}/related-alerts') + self.assertEqual(200, response.status_code) + + def test_get_related_alerts_should_return_two_alerts_identifier_in_iocs_table(self): + body = { + 'alert_title': 'title', + 'alert_severity_id': 4, + 'alert_status_id': 3, + 'alert_customer_id': 1, + "alert_iocs": [{ + "ioc_value": "Tarzan 5", + "ioc_description": "description of Tarzan", + "ioc_tlp_id": 1, + "ioc_type_id": 2, + "ioc_tags": "tag1,tag2", + }] + } + response = self._subject.create('api/v2/alerts', body).json() + identifier = response['alert_id'] + body = { + 'alert_title': 'title_2', + 'alert_severity_id': 4, + 'alert_status_id': 3, + 'alert_customer_id': 1, + "alert_iocs": [{ + "ioc_value": "Tarzan 5", + "ioc_description": "description of Tarzan", + "ioc_tlp_id": 1, + "ioc_type_id": 2, + "ioc_tags": "tag1,tag2", + }] + } + response = self._subject.create('api/v2/alerts', body).json() + identifier2 = response['alert_id'] + response = self._subject.get(f'/api/v2/alerts/{identifier}/related-alerts').json() + alert_ids = [identifier, identifier2] + self.assertEqual(alert_ids, response['iocs']) + + def test_get_related_alerts_should_return_404_when_alert_not_found(self): + response = self._subject.get(f'/api/v2/alerts/{_IDENTIFIER_FOR_NONEXISTENT_OBJECT}/related-alerts') + self.assertEqual(404, response.status_code) + + def test_get_related_alerts_should_return_403_when_user_has_no_permission_to_get_alert(self): + user = self._subject.create_dummy_user() + body = { + 'alert_title': 'title', + 'alert_severity_id': 4, + 'alert_status_id': 3, + 'alert_customer_id': 1 + } + response = self._subject.create('api/v2/alerts', body).json() + identifier = response['alert_id'] + response = user.get(f'/api/v2/alerts/{identifier}/related-alerts') + self.assertEqual(403, response.status_code) + + def test_get_related_alerts_should_return_404_when_user_has_no_customer_access(self): + body = { + 'group_name': 'Customer read', + 'group_description': 'Group with customers can read alert', + 'group_permissions': [IRIS_PERMISSION_ALERTS_READ] + } + response = self._subject.create('/manage/groups/add', body).json() + group_identifier = response['data']['group_id'] + user = self._subject.create_dummy_user() + body = {'groups_membership': [group_identifier]} + self._subject.create(f'/manage/users/{user.get_identifier()}/groups/update', body) + + body = { + 'alert_title': 'title', + 'alert_severity_id': 4, + 'alert_status_id': 3, + 'alert_customer_id': 1, + } + response = self._subject.create('/api/v2/alerts', body).json() + identifier = response['alert_id'] + response = user.get(f'/api/v2/alerts/{identifier}/related-alerts') + self.assertEqual(404, response.status_code) From 3b9a545eac79a1e806e09274969be9c5aed799e5 Mon Sep 17 00:00:00 2001 From: Elise17 Date: Wed, 1 Oct 2025 17:09:12 +0200 Subject: [PATCH 15/38] Changed test rest alerts --- source/app/blueprints/rest/v2/alerts.py | 4 +-- tests/tests_rest_alerts.py | 35 ------------------------- 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/source/app/blueprints/rest/v2/alerts.py b/source/app/blueprints/rest/v2/alerts.py index c854f6561..1d9d34f49 100644 --- a/source/app/blueprints/rest/v2/alerts.py +++ b/source/app/blueprints/rest/v2/alerts.py @@ -173,7 +173,7 @@ def get_related_alerts(self, identifier): alert = alerts_get(iris_current_user, identifier) if not user_has_client_access(iris_current_user.id, alert.alert_customer_id): return response_api_error('User not entitled to create alerts for the client') - + open_alerts = request.args.get('open-alerts', 'false').lower() == 'true' open_cases = request.args.get('open-cases', 'false').lower() == 'true' closed_cases = request.args.get('closed-cases', 'false').lower() == 'true' @@ -185,7 +185,7 @@ def get_related_alerts(self, identifier): number_of_results = 100 if days_back < 0: days_back = 180 - + similar_alerts = get_related_alerts_details(alert.alert_customer_id, alert.assets, alert.iocs, open_alerts, closed_alerts, open_cases, closed_cases, days_back, number_of_results) diff --git a/tests/tests_rest_alerts.py b/tests/tests_rest_alerts.py index f53ce4d29..58c6b96ff 100644 --- a/tests/tests_rest_alerts.py +++ b/tests/tests_rest_alerts.py @@ -454,41 +454,6 @@ def test_get_related_alerts_should_return_200(self): response = self._subject.get(f'/api/v2/alerts/{identifier}/related-alerts') self.assertEqual(200, response.status_code) - def test_get_related_alerts_should_return_two_alerts_identifier_in_iocs_table(self): - body = { - 'alert_title': 'title', - 'alert_severity_id': 4, - 'alert_status_id': 3, - 'alert_customer_id': 1, - "alert_iocs": [{ - "ioc_value": "Tarzan 5", - "ioc_description": "description of Tarzan", - "ioc_tlp_id": 1, - "ioc_type_id": 2, - "ioc_tags": "tag1,tag2", - }] - } - response = self._subject.create('api/v2/alerts', body).json() - identifier = response['alert_id'] - body = { - 'alert_title': 'title_2', - 'alert_severity_id': 4, - 'alert_status_id': 3, - 'alert_customer_id': 1, - "alert_iocs": [{ - "ioc_value": "Tarzan 5", - "ioc_description": "description of Tarzan", - "ioc_tlp_id": 1, - "ioc_type_id": 2, - "ioc_tags": "tag1,tag2", - }] - } - response = self._subject.create('api/v2/alerts', body).json() - identifier2 = response['alert_id'] - response = self._subject.get(f'/api/v2/alerts/{identifier}/related-alerts').json() - alert_ids = [identifier, identifier2] - self.assertEqual(alert_ids, response['iocs']) - def test_get_related_alerts_should_return_404_when_alert_not_found(self): response = self._subject.get(f'/api/v2/alerts/{_IDENTIFIER_FOR_NONEXISTENT_OBJECT}/related-alerts') self.assertEqual(404, response.status_code) From afde506f613be4ab0bff1e16ff809f8fea528094 Mon Sep 17 00:00:00 2001 From: Elise17 Date: Wed, 1 Oct 2025 17:16:23 +0200 Subject: [PATCH 16/38] Changed import from the API layer --- source/app/blueprints/rest/v2/alerts.py | 7 +++---- source/app/business/alerts.py | 8 ++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/source/app/blueprints/rest/v2/alerts.py b/source/app/blueprints/rest/v2/alerts.py index 1d9d34f49..12f373f30 100644 --- a/source/app/blueprints/rest/v2/alerts.py +++ b/source/app/blueprints/rest/v2/alerts.py @@ -40,12 +40,12 @@ from app.business.alerts import alerts_get from app.business.alerts import alerts_update from app.business.alerts import alerts_delete +from app.business.alerts import related_alerts_get +from app.business.alerts import get_related_alerts_details from app.business.errors import BusinessProcessingError from app.business.errors import ObjectNotFoundError from app.business.access_controls import access_controls_user_has_customer_access from app.datamgmt.manage.manage_access_control_db import user_has_client_access -from app.datamgmt.alerts.alerts_db import get_related_alerts_details - class AlertsOperations: @@ -186,8 +186,7 @@ def get_related_alerts(self, identifier): if days_back < 0: days_back = 180 - similar_alerts = get_related_alerts_details(alert.alert_customer_id, alert.assets, alert.iocs, - open_alerts, closed_alerts, open_cases, closed_cases, + similar_alerts = related_alerts_get(alert, open_alerts, closed_alerts, open_cases, closed_cases, days_back, number_of_results) return response_api_success(similar_alerts) diff --git a/source/app/business/alerts.py b/source/app/business/alerts.py index 8e509026b..fe9bea312 100644 --- a/source/app/business/alerts.py +++ b/source/app/business/alerts.py @@ -31,6 +31,7 @@ from app.datamgmt.alerts.alerts_db import get_alert_by_id from app.datamgmt.alerts.alerts_db import delete_alert from app.datamgmt.alerts.alerts_db import get_filtered_alerts +from app.datamgmt.alerts.alerts_db import get_related_alerts_details from app.iris_engine.module_handler.module_handler import call_modules_hook from app.iris_engine.utils.tracker import track_activity from app.util import add_obj_history_entry @@ -110,6 +111,13 @@ def alerts_get(user, identifier) -> Alert: return alert +def related_alerts_get(alert, open_alerts, closed_alerts, open_cases, closed_cases, + days_back, number_of_results): + return get_related_alerts_details(alert.alert_customer_id, alert.assets, alert.iocs, + open_alerts, closed_alerts, open_cases, closed_cases, + days_back, number_of_results) + + def alerts_exists(user, identifier) -> bool: alert = _get(user, identifier) From c99d81782edf4a24240cd160b0eebb663469b44b Mon Sep 17 00:00:00 2001 From: Elise17 Date: Wed, 1 Oct 2025 17:17:26 +0200 Subject: [PATCH 17/38] Fixed check analysis --- source/app/blueprints/rest/v2/alerts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/app/blueprints/rest/v2/alerts.py b/source/app/blueprints/rest/v2/alerts.py index 12f373f30..879534109 100644 --- a/source/app/blueprints/rest/v2/alerts.py +++ b/source/app/blueprints/rest/v2/alerts.py @@ -41,12 +41,12 @@ from app.business.alerts import alerts_update from app.business.alerts import alerts_delete from app.business.alerts import related_alerts_get -from app.business.alerts import get_related_alerts_details from app.business.errors import BusinessProcessingError from app.business.errors import ObjectNotFoundError from app.business.access_controls import access_controls_user_has_customer_access from app.datamgmt.manage.manage_access_control_db import user_has_client_access + class AlertsOperations: def __init__(self): From 7f3dcd79cdc1f4121c16064376fe0367a88bbf29 Mon Sep 17 00:00:00 2001 From: Elise17 Date: Fri, 3 Oct 2025 17:26:59 +0200 Subject: [PATCH 18/38] changed message api error --- source/app/blueprints/rest/v2/alerts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/app/blueprints/rest/v2/alerts.py b/source/app/blueprints/rest/v2/alerts.py index 879534109..d9a55ef58 100644 --- a/source/app/blueprints/rest/v2/alerts.py +++ b/source/app/blueprints/rest/v2/alerts.py @@ -172,7 +172,7 @@ def get_related_alerts(self, identifier): try: alert = alerts_get(iris_current_user, identifier) if not user_has_client_access(iris_current_user.id, alert.alert_customer_id): - return response_api_error('User not entitled to create alerts for the client') + return response_api_error('Alert not found') open_alerts = request.args.get('open-alerts', 'false').lower() == 'true' open_cases = request.args.get('open-cases', 'false').lower() == 'true' From 2f1a1d4a981cefdceb711e592c7cffbbe7e26607 Mon Sep 17 00:00:00 2001 From: Elise17 Date: Fri, 3 Oct 2025 18:04:30 +0200 Subject: [PATCH 19/38] Removed unused function --- source/app/blueprints/rest/v2/alerts.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/source/app/blueprints/rest/v2/alerts.py b/source/app/blueprints/rest/v2/alerts.py index d9a55ef58..b683071ed 100644 --- a/source/app/blueprints/rest/v2/alerts.py +++ b/source/app/blueprints/rest/v2/alerts.py @@ -171,8 +171,6 @@ def get_related_alerts(self, identifier): try: alert = alerts_get(iris_current_user, identifier) - if not user_has_client_access(iris_current_user.id, alert.alert_customer_id): - return response_api_error('Alert not found') open_alerts = request.args.get('open-alerts', 'false').lower() == 'true' open_cases = request.args.get('open-cases', 'false').lower() == 'true' From 0516cc4131ce6e5bac7dd9855d5eb1d3d3d5d876 Mon Sep 17 00:00:00 2001 From: Elise17 Date: Mon, 6 Oct 2025 16:38:11 +0200 Subject: [PATCH 20/38] Removed import --- source/app/blueprints/rest/v2/alerts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/source/app/blueprints/rest/v2/alerts.py b/source/app/blueprints/rest/v2/alerts.py index b683071ed..afd2eea6d 100644 --- a/source/app/blueprints/rest/v2/alerts.py +++ b/source/app/blueprints/rest/v2/alerts.py @@ -44,7 +44,6 @@ from app.business.errors import BusinessProcessingError from app.business.errors import ObjectNotFoundError from app.business.access_controls import access_controls_user_has_customer_access -from app.datamgmt.manage.manage_access_control_db import user_has_client_access class AlertsOperations: From 9a33abff7c66f92bf9113aa712ddc1902c2ece45 Mon Sep 17 00:00:00 2001 From: Elise17 Date: Wed, 8 Oct 2025 14:04:05 +0200 Subject: [PATCH 21/38] Started implementation of POST /api/v2/alerts-filters --- source/app/blueprints/rest/api_v2_routes.py | 2 + .../app/blueprints/rest/v2/alerts_filters.py | 56 ++++++++++++++++++ tests/tests_rest_alerts_filters.py | 58 +++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 source/app/blueprints/rest/v2/alerts_filters.py create mode 100644 tests/tests_rest_alerts_filters.py diff --git a/source/app/blueprints/rest/api_v2_routes.py b/source/app/blueprints/rest/api_v2_routes.py index 3fa9c7c5b..782dac1b7 100644 --- a/source/app/blueprints/rest/api_v2_routes.py +++ b/source/app/blueprints/rest/api_v2_routes.py @@ -31,6 +31,7 @@ from app.blueprints.rest.v2.tags import tags_blueprint from app.blueprints.rest.v2.tasks import tasks_blueprint from app.blueprints.rest.v2.profile import profile_blueprint +from app.blueprints.rest.v2.alerts_filters import alerts_filters_blueprint # Create root /api/v2 blueprint @@ -50,3 +51,4 @@ rest_v2_blueprint.register_blueprint(manage_v2_blueprint) rest_v2_blueprint.register_blueprint(tags_blueprint) rest_v2_blueprint.register_blueprint(profile_blueprint) +rest_v2_blueprint.register_blueprint(alerts_filters_blueprint) diff --git a/source/app/blueprints/rest/v2/alerts_filters.py b/source/app/blueprints/rest/v2/alerts_filters.py new file mode 100644 index 000000000..caa3647a1 --- /dev/null +++ b/source/app/blueprints/rest/v2/alerts_filters.py @@ -0,0 +1,56 @@ +# 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 import Blueprint +from flask import request +from marshmallow 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_error +from app.blueprints.iris_user import iris_current_user +from app.schema.marshables import SavedFilterSchema + + +class AlertsFiltersOperations: + + def __init__(self): + self._schema = SavedFilterSchema() + + def create(self): + saved_filter_schema = SavedFilterSchema() + + request_data = request.get_json() + request_data ['created_by'] = iris_current_user.id + + try: + new_saved_filter = saved_filter_schema.load(request_data) + return response_api_created(saved_filter_schema.dump(new_saved_filter)) + + except ValidationError as e: + return response_api_error('Data error', e.messages) + + +alerts_filters_blueprint = Blueprint('alerts_filters_rest_v2', __name__, url_prefix='/alerts-filters') +alerts_filters_operations = AlertsFiltersOperations() + + +@alerts_filters_blueprint.post('') +@ac_api_requires() +def create_alert_filter(): + return alerts_filters_operations.create() diff --git a/tests/tests_rest_alerts_filters.py b/tests/tests_rest_alerts_filters.py new file mode 100644 index 000000000..35f36f98a --- /dev/null +++ b/tests/tests_rest_alerts_filters.py @@ -0,0 +1,58 @@ +# IRIS Source Code +# Copyright (C) 2023 - DFIR-IRIS +# contact@dfir-iris.org +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +from unittest import TestCase +from iris import Iris + + +class TestsRestAlertsFilters(TestCase): + + def setUp(self) -> None: + self._subject = Iris() + + def tearDown(self): + self._subject.clear_database() + + def test_create_alert_filter_should_return_201(self): + body = { + 'filter_is_private': 'true', + 'filter_type': 'alerts', + 'filter_name': 'filter name', + 'filter_description': 'filter description', + 'filter_data' :{ + 'alert_tilte': 'filter name', + 'alert_description': '', + 'alert_source': '', + 'alert_tags': '', + 'alert_severity_id': '', + 'alert_start_date': '', + 'source_start_date': '', + 'source_end_date': '', + 'creation_end_date': '', + 'creation_start_date': '', + 'alert_assets': '', + 'alert_iocs': '', + 'alert_ids': '', + 'source_reference': '', + 'case_id': '', + 'custom_conditions': '', + + }, + } + response = self._subject.create('/api/v2/alerts-filters', body) + self.assertEqual(201, response.status_code) From b7088fd7803420b452a48e4c854b48977c667d72 Mon Sep 17 00:00:00 2001 From: Elise17 Date: Wed, 8 Oct 2025 14:06:01 +0200 Subject: [PATCH 22/38] Fixed analysis check --- source/app/blueprints/rest/v2/alerts_filters.py | 2 +- tests/tests_rest_alerts_filters.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/app/blueprints/rest/v2/alerts_filters.py b/source/app/blueprints/rest/v2/alerts_filters.py index caa3647a1..db9497a39 100644 --- a/source/app/blueprints/rest/v2/alerts_filters.py +++ b/source/app/blueprints/rest/v2/alerts_filters.py @@ -34,7 +34,7 @@ def __init__(self): def create(self): saved_filter_schema = SavedFilterSchema() - + request_data = request.get_json() request_data ['created_by'] = iris_current_user.id diff --git a/tests/tests_rest_alerts_filters.py b/tests/tests_rest_alerts_filters.py index 35f36f98a..41f91e714 100644 --- a/tests/tests_rest_alerts_filters.py +++ b/tests/tests_rest_alerts_filters.py @@ -34,7 +34,7 @@ def test_create_alert_filter_should_return_201(self): 'filter_type': 'alerts', 'filter_name': 'filter name', 'filter_description': 'filter description', - 'filter_data' :{ + 'filter_data' : { 'alert_tilte': 'filter name', 'alert_description': '', 'alert_source': '', From 103ed2c9c023e2272d61d15ef9a1e3c0169bf7d8 Mon Sep 17 00:00:00 2001 From: Elise17 Date: Wed, 8 Oct 2025 14:09:21 +0200 Subject: [PATCH 23/38] Added function _load --- source/app/blueprints/rest/v2/alerts_filters.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/source/app/blueprints/rest/v2/alerts_filters.py b/source/app/blueprints/rest/v2/alerts_filters.py index db9497a39..07fe23561 100644 --- a/source/app/blueprints/rest/v2/alerts_filters.py +++ b/source/app/blueprints/rest/v2/alerts_filters.py @@ -32,15 +32,16 @@ class AlertsFiltersOperations: def __init__(self): self._schema = SavedFilterSchema() - def create(self): - saved_filter_schema = SavedFilterSchema() + def _load(self, request_data): + return self._schema.load(request_data) + def create(self): request_data = request.get_json() request_data ['created_by'] = iris_current_user.id try: - new_saved_filter = saved_filter_schema.load(request_data) - return response_api_created(saved_filter_schema.dump(new_saved_filter)) + new_saved_filter = self._load(request_data) + return response_api_created(self._schema.dump(new_saved_filter)) except ValidationError as e: return response_api_error('Data error', e.messages) From 82bfec63e2c918159d6b8a50eeb8d894df6710da Mon Sep 17 00:00:00 2001 From: Elise17 Date: Wed, 8 Oct 2025 14:21:44 +0200 Subject: [PATCH 24/38] Deprecated endpoint POST /filters/add in favor of POST /api/v2/alerts-filters --- source/app/blueprints/rest/filters_routes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/app/blueprints/rest/filters_routes.py b/source/app/blueprints/rest/filters_routes.py index 0971a1b49..a742e71e6 100644 --- a/source/app/blueprints/rest/filters_routes.py +++ b/source/app/blueprints/rest/filters_routes.py @@ -20,19 +20,21 @@ from werkzeug import Response from app import db -from app.blueprints.iris_user import iris_current_user from app.datamgmt.filters.filters_db import get_filter_by_id from app.datamgmt.filters.filters_db import list_filters_by_type from app.iris_engine.utils.tracker import track_activity from app.schema.marshables import SavedFilterSchema +from app.blueprints.iris_user import iris_current_user from app.blueprints.access_controls import ac_api_requires from app.blueprints.responses import response_success from app.blueprints.responses import response_error +from app.blueprints.rest.endpoints import endpoint_deprecated saved_filters_rest_blueprint = Blueprint('saved_filters_rest', __name__) @saved_filters_rest_blueprint.route('/filters/add', methods=['POST']) +@endpoint_deprecated('POST', '/api/v2/alerts-filters') @ac_api_requires() def filters_add_route() -> Response: """ From 5b1290eb2cfb5a809eadb7f34ad79c56ee6d8330 Mon Sep 17 00:00:00 2001 From: Elise17 Date: Wed, 8 Oct 2025 14:25:46 +0200 Subject: [PATCH 25/38] Added test create alert when filter data is missing --- tests/tests_rest_alerts_filters.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/tests_rest_alerts_filters.py b/tests/tests_rest_alerts_filters.py index 41f91e714..0093fc251 100644 --- a/tests/tests_rest_alerts_filters.py +++ b/tests/tests_rest_alerts_filters.py @@ -52,7 +52,17 @@ def test_create_alert_filter_should_return_201(self): 'case_id': '', 'custom_conditions': '', - }, + } } response = self._subject.create('/api/v2/alerts-filters', body) self.assertEqual(201, response.status_code) + + def test_create_alert_filter_should_return_400_when_filter_data_is_missing(self): + body = { + 'filter_is_private': 'true', + 'filter_type': 'alerts', + 'filter_name': 'filter name', + 'filter_description': 'filter description', + } + response = self._subject.create('/api/v2/alerts-filters', body) + self.assertEqual(400, response.status_code) From ea45b99a9fcf1775fb9a3c31b4f45d06e57df37e Mon Sep 17 00:00:00 2001 From: Elise17 Date: Wed, 8 Oct 2025 14:40:00 +0200 Subject: [PATCH 26/38] Added test test_create_alert_filter_should_return_filter_type --- tests/tests_rest_alerts_filters.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/tests_rest_alerts_filters.py b/tests/tests_rest_alerts_filters.py index 0093fc251..d44769d08 100644 --- a/tests/tests_rest_alerts_filters.py +++ b/tests/tests_rest_alerts_filters.py @@ -66,3 +66,33 @@ def test_create_alert_filter_should_return_400_when_filter_data_is_missing(self) } response = self._subject.create('/api/v2/alerts-filters', body) self.assertEqual(400, response.status_code) + + def test_create_alert_filter_should_return_filter_type(self): + filter_type = 'alerts' + body = { + 'filter_is_private': 'true', + 'filter_type': filter_type, + 'filter_name': 'filter name', + 'filter_description': 'filter description', + 'filter_data' : { + 'alert_tilte': 'filter name', + 'alert_description': '', + 'alert_source': '', + 'alert_tags': '', + 'alert_severity_id': '', + 'alert_start_date': '', + 'source_start_date': '', + 'source_end_date': '', + 'creation_end_date': '', + 'creation_start_date': '', + 'alert_assets': '', + 'alert_iocs': '', + 'alert_ids': '', + 'source_reference': '', + 'case_id': '', + 'custom_conditions': '', + + } + } + response = self._subject.create('/api/v2/alerts-filters', body).json() + self.assertEqual(filter_type, response['filter_type']) From 4c5ddd6a41b83c78fca6293911932dc0a6b904a3 Mon Sep 17 00:00:00 2001 From: Elise17 Date: Wed, 8 Oct 2025 14:41:40 +0200 Subject: [PATCH 27/38] Added test_create_alert_filter_should_return_filter_name --- tests/tests_rest_alerts_filters.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/tests_rest_alerts_filters.py b/tests/tests_rest_alerts_filters.py index d44769d08..77a7f9334 100644 --- a/tests/tests_rest_alerts_filters.py +++ b/tests/tests_rest_alerts_filters.py @@ -96,3 +96,33 @@ def test_create_alert_filter_should_return_filter_type(self): } response = self._subject.create('/api/v2/alerts-filters', body).json() self.assertEqual(filter_type, response['filter_type']) + + def test_create_alert_filter_should_return_filter_name(self): + filter_name = 'name' + body = { + 'filter_is_private': 'true', + 'filter_type': 'alerts', + 'filter_name': filter_name, + 'filter_description': 'filter description', + 'filter_data' : { + 'alert_tilte': 'filter name', + 'alert_description': '', + 'alert_source': '', + 'alert_tags': '', + 'alert_severity_id': '', + 'alert_start_date': '', + 'source_start_date': '', + 'source_end_date': '', + 'creation_end_date': '', + 'creation_start_date': '', + 'alert_assets': '', + 'alert_iocs': '', + 'alert_ids': '', + 'source_reference': '', + 'case_id': '', + 'custom_conditions': '', + + } + } + response = self._subject.create('/api/v2/alerts-filters', body).json() + self.assertEqual(filter_name, response['filter_name']) From 0a0f63d9fc25a0e8951267904e95c6cb26cfed9f Mon Sep 17 00:00:00 2001 From: Elise17 Date: Wed, 8 Oct 2025 14:43:59 +0200 Subject: [PATCH 28/38] Added test_create_alert_filter_should_return_in_filter_data_alert_title --- tests/tests_rest_alerts_filters.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/tests_rest_alerts_filters.py b/tests/tests_rest_alerts_filters.py index 77a7f9334..7546f74a4 100644 --- a/tests/tests_rest_alerts_filters.py +++ b/tests/tests_rest_alerts_filters.py @@ -126,3 +126,33 @@ def test_create_alert_filter_should_return_filter_name(self): } response = self._subject.create('/api/v2/alerts-filters', body).json() self.assertEqual(filter_name, response['filter_name']) + + def test_create_alert_filter_should_return_in_filter_data_alert_title(self): + alert_title = 'alert_title' + body = { + 'filter_is_private': 'true', + 'filter_type': 'alerts', + 'filter_name': 'filter_name', + 'filter_description': 'filter description', + 'filter_data' : { + 'alert_title': alert_title, + 'alert_description': '', + 'alert_source': '', + 'alert_tags': '', + 'alert_severity_id': '', + 'alert_start_date': '', + 'source_start_date': '', + 'source_end_date': '', + 'creation_end_date': '', + 'creation_start_date': '', + 'alert_assets': '', + 'alert_iocs': '', + 'alert_ids': '', + 'source_reference': '', + 'case_id': '', + 'custom_conditions': '', + + } + } + response = self._subject.create('/api/v2/alerts-filters', body).json() + self.assertEqual(alert_title, response['filter_data']['alert_title']) From af0bf2b60e5afd39d2ed644e59d51324f14b8a28 Mon Sep 17 00:00:00 2001 From: Elise17 Date: Fri, 17 Oct 2025 18:11:06 +0200 Subject: [PATCH 29/38] Started implementation of GET /api/v2/alerts-filters/{identifier} --- .../app/blueprints/rest/v2/alerts_filters.py | 23 +++++++++++++ source/app/business/alerts_filters.py | 24 ++++++++++++++ tests/tests_rest_alerts_filters.py | 32 +++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 source/app/business/alerts_filters.py diff --git a/source/app/blueprints/rest/v2/alerts_filters.py b/source/app/blueprints/rest/v2/alerts_filters.py index 07fe23561..f04cf104a 100644 --- a/source/app/blueprints/rest/v2/alerts_filters.py +++ b/source/app/blueprints/rest/v2/alerts_filters.py @@ -23,8 +23,15 @@ 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_error +from app.blueprints.rest.endpoints import response_api_success +from app.blueprints.rest.endpoints import response_api_not_found from app.blueprints.iris_user import iris_current_user + + from app.schema.marshables import SavedFilterSchema +from app.business.errors import ObjectNotFoundError +from app.business.alerts_filters import add_alerts_filters +from app.datamgmt.filters.filters_db import get_filter_by_id class AlertsFiltersOperations: @@ -41,10 +48,20 @@ def create(self): try: new_saved_filter = self._load(request_data) + add_alerts_filters(new_saved_filter) return response_api_created(self._schema.dump(new_saved_filter)) except ValidationError as e: return response_api_error('Data error', e.messages) + + def get(self, identifier): + try: + saved_filter = get_filter_by_id(identifier) + return response_api_success(self._schema.dump(saved_filter)) + + except ObjectNotFoundError: + return response_api_not_found() + alerts_filters_blueprint = Blueprint('alerts_filters_rest_v2', __name__, url_prefix='/alerts-filters') @@ -55,3 +72,9 @@ def create(self): @ac_api_requires() def create_alert_filter(): return alerts_filters_operations.create() + +@alerts_filters_blueprint.get('/') +@ac_api_requires() +def get_alert(identifier): + return alerts_filters_operations.get(identifier) + diff --git a/source/app/business/alerts_filters.py b/source/app/business/alerts_filters.py new file mode 100644 index 000000000..2feed9dbc --- /dev/null +++ b/source/app/business/alerts_filters.py @@ -0,0 +1,24 @@ +# 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 app import db + + +def add_alerts_filters(new_saved_filter): + db.session.add(new_saved_filter) + db.session.commit() diff --git a/tests/tests_rest_alerts_filters.py b/tests/tests_rest_alerts_filters.py index 7546f74a4..b49894a32 100644 --- a/tests/tests_rest_alerts_filters.py +++ b/tests/tests_rest_alerts_filters.py @@ -156,3 +156,35 @@ def test_create_alert_filter_should_return_in_filter_data_alert_title(self): } response = self._subject.create('/api/v2/alerts-filters', body).json() self.assertEqual(alert_title, response['filter_data']['alert_title']) + + def test_get_alert_filter_should_return_200(self): + body = { + 'filter_is_private': 'true', + 'filter_type': 'alerts', + 'filter_name': 'filter name', + 'filter_description': 'filter description', + 'filter_data' : { + 'alert_tilte': 'filter name', + 'alert_description': '', + 'alert_source': '', + 'alert_tags': '', + 'alert_severity_id': '', + 'alert_start_date': '', + 'source_start_date': '', + 'source_end_date': '', + 'creation_end_date': '', + 'creation_start_date': '', + 'alert_assets': '', + 'alert_iocs': '', + 'alert_ids': '', + 'source_reference': '', + 'case_id': '', + 'custom_conditions': '', + + } + } + user = self._subject.create_dummy_user() + response = user.create('/api/v2/alerts-filters', body).json() + identifier = response['filter_id'] + response = self._subject.get(f'/api/v2/alerts-filters/{identifier}') + self.assertEqual(200, response.status_code) From 0c911feaa74e06fc452d75c49c6590eda123cf85 Mon Sep 17 00:00:00 2001 From: Elise17 Date: Fri, 17 Oct 2025 18:13:22 +0200 Subject: [PATCH 30/38] Fixed check analysis --- source/app/blueprints/rest/v2/alerts_filters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/app/blueprints/rest/v2/alerts_filters.py b/source/app/blueprints/rest/v2/alerts_filters.py index f04cf104a..4de21684f 100644 --- a/source/app/blueprints/rest/v2/alerts_filters.py +++ b/source/app/blueprints/rest/v2/alerts_filters.py @@ -53,7 +53,7 @@ def create(self): except ValidationError as e: return response_api_error('Data error', e.messages) - + def get(self, identifier): try: saved_filter = get_filter_by_id(identifier) @@ -63,7 +63,6 @@ def get(self, identifier): return response_api_not_found() - alerts_filters_blueprint = Blueprint('alerts_filters_rest_v2', __name__, url_prefix='/alerts-filters') alerts_filters_operations = AlertsFiltersOperations() @@ -73,6 +72,7 @@ def get(self, identifier): def create_alert_filter(): return alerts_filters_operations.create() + @alerts_filters_blueprint.get('/') @ac_api_requires() def get_alert(identifier): From 8559ad7e7877f041f6185add9a6481ccd277d37c Mon Sep 17 00:00:00 2001 From: Elise17 Date: Fri, 17 Oct 2025 18:21:27 +0200 Subject: [PATCH 31/38] Fixed importation problem --- source/app/blueprints/rest/v2/alerts_filters.py | 8 ++++---- source/app/business/alerts_filters.py | 7 +++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/source/app/blueprints/rest/v2/alerts_filters.py b/source/app/blueprints/rest/v2/alerts_filters.py index 4de21684f..da77d71d4 100644 --- a/source/app/blueprints/rest/v2/alerts_filters.py +++ b/source/app/blueprints/rest/v2/alerts_filters.py @@ -30,8 +30,8 @@ from app.schema.marshables import SavedFilterSchema from app.business.errors import ObjectNotFoundError -from app.business.alerts_filters import add_alerts_filters -from app.datamgmt.filters.filters_db import get_filter_by_id +from app.business.alerts_filters import alerts_filters_add +from app.business.alerts_filters import alerts_filters_get class AlertsFiltersOperations: @@ -48,7 +48,7 @@ def create(self): try: new_saved_filter = self._load(request_data) - add_alerts_filters(new_saved_filter) + alerts_filters_add(new_saved_filter) return response_api_created(self._schema.dump(new_saved_filter)) except ValidationError as e: @@ -56,7 +56,7 @@ def create(self): def get(self, identifier): try: - saved_filter = get_filter_by_id(identifier) + saved_filter = alerts_filters_get(identifier) return response_api_success(self._schema.dump(saved_filter)) except ObjectNotFoundError: diff --git a/source/app/business/alerts_filters.py b/source/app/business/alerts_filters.py index 2feed9dbc..6b847ac63 100644 --- a/source/app/business/alerts_filters.py +++ b/source/app/business/alerts_filters.py @@ -17,8 +17,11 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from app import db +from app.datamgmt.filters.filters_db import get_filter_by_id - -def add_alerts_filters(new_saved_filter): +def alerts_filters_add(new_saved_filter): db.session.add(new_saved_filter) db.session.commit() + +def alerts_filters_get(identifier): + return get_filter_by_id(identifier) From 7440b09f42b285df66319cfe231121b66494f528 Mon Sep 17 00:00:00 2001 From: Elise17 Date: Fri, 17 Oct 2025 18:22:59 +0200 Subject: [PATCH 32/38] Fixed static analysis --- source/app/business/alerts_filters.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/app/business/alerts_filters.py b/source/app/business/alerts_filters.py index 6b847ac63..3d295028d 100644 --- a/source/app/business/alerts_filters.py +++ b/source/app/business/alerts_filters.py @@ -19,9 +19,11 @@ from app import db from app.datamgmt.filters.filters_db import get_filter_by_id + def alerts_filters_add(new_saved_filter): db.session.add(new_saved_filter) db.session.commit() + def alerts_filters_get(identifier): - return get_filter_by_id(identifier) + return get_filter_by_id(identifier) From 0690eb01a6a0e22db711d326982d20b64d49a877 Mon Sep 17 00:00:00 2001 From: Elise17 Date: Fri, 17 Oct 2025 19:02:09 +0200 Subject: [PATCH 33/38] Removed user in test --- tests/tests_rest_alerts_filters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tests_rest_alerts_filters.py b/tests/tests_rest_alerts_filters.py index b49894a32..3fa25aa3d 100644 --- a/tests/tests_rest_alerts_filters.py +++ b/tests/tests_rest_alerts_filters.py @@ -183,8 +183,8 @@ def test_get_alert_filter_should_return_200(self): } } - user = self._subject.create_dummy_user() - response = user.create('/api/v2/alerts-filters', body).json() + + response = self._subject.create('/api/v2/alerts-filters', body).json() identifier = response['filter_id'] response = self._subject.get(f'/api/v2/alerts-filters/{identifier}') self.assertEqual(200, response.status_code) From 1129b73b40a485359b594d5497c11390bc8f22cf Mon Sep 17 00:00:00 2001 From: Elise17 Date: Fri, 17 Oct 2025 20:18:30 +0200 Subject: [PATCH 34/38] Added test_get_alert_filter_should_return_filter_name --- tests/tests_rest_alerts_filters.py | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/tests_rest_alerts_filters.py b/tests/tests_rest_alerts_filters.py index 3fa25aa3d..65ca4673a 100644 --- a/tests/tests_rest_alerts_filters.py +++ b/tests/tests_rest_alerts_filters.py @@ -188,3 +188,36 @@ def test_get_alert_filter_should_return_200(self): identifier = response['filter_id'] response = self._subject.get(f'/api/v2/alerts-filters/{identifier}') self.assertEqual(200, response.status_code) + + def test_get_alert_filter_should_return_filter_name(self): + filter_name = 'filter name' + body = { + 'filter_is_private': 'true', + 'filter_type': 'alerts', + 'filter_name': filter_name, + 'filter_description': 'filter description', + 'filter_data' : { + 'alert_tilte': 'filter name', + 'alert_description': '', + 'alert_source': '', + 'alert_tags': '', + 'alert_severity_id': '', + 'alert_start_date': '', + 'source_start_date': '', + 'source_end_date': '', + 'creation_end_date': '', + 'creation_start_date': '', + 'alert_assets': '', + 'alert_iocs': '', + 'alert_ids': '', + 'source_reference': '', + 'case_id': '', + 'custom_conditions': '', + + } + } + + response = self._subject.create('/api/v2/alerts-filters', body).json() + identifier = response['filter_id'] + response = self._subject.get(f'/api/v2/alerts-filters/{identifier}').json() + self.assertEqual(filter_name, response['filter_name']) From 97a89e5fc8d003e51ef71b468b026d3a81987998 Mon Sep 17 00:00:00 2001 From: Elise17 Date: Fri, 17 Oct 2025 20:20:17 +0200 Subject: [PATCH 35/38] Deprecated endpoint GET /filters/{identifier} --- source/app/blueprints/rest/filters_routes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/source/app/blueprints/rest/filters_routes.py b/source/app/blueprints/rest/filters_routes.py index a742e71e6..b4203d4a4 100644 --- a/source/app/blueprints/rest/filters_routes.py +++ b/source/app/blueprints/rest/filters_routes.py @@ -130,6 +130,7 @@ def filters_delete_route(filter_id) -> Response: @saved_filters_rest_blueprint.route('/filters/', methods=['GET']) +@endpoint_deprecated('GET', '/api/v2/alerts-filters') @ac_api_requires() def filters_get_route(filter_id) -> Response: """ From 622b46a34f7122a6205074378e3f87c115e94e52 Mon Sep 17 00:00:00 2001 From: Elise17 Date: Fri, 17 Oct 2025 20:32:53 +0200 Subject: [PATCH 36/38] Added test_get_alert_filter_should_return_404 --- source/app/blueprints/rest/v2/alerts_filters.py | 11 +++++------ source/app/business/alerts_filters.py | 11 +++++++---- tests/tests_rest_alerts_filters.py | 6 ++++++ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/source/app/blueprints/rest/v2/alerts_filters.py b/source/app/blueprints/rest/v2/alerts_filters.py index da77d71d4..ff948b2be 100644 --- a/source/app/blueprints/rest/v2/alerts_filters.py +++ b/source/app/blueprints/rest/v2/alerts_filters.py @@ -30,8 +30,8 @@ from app.schema.marshables import SavedFilterSchema from app.business.errors import ObjectNotFoundError -from app.business.alerts_filters import alerts_filters_add -from app.business.alerts_filters import alerts_filters_get +from app.business.alerts_filters import alert_filter_add +from app.business.alerts_filters import alert_filter_get class AlertsFiltersOperations: @@ -48,17 +48,16 @@ def create(self): try: new_saved_filter = self._load(request_data) - alerts_filters_add(new_saved_filter) + alert_filter_add(new_saved_filter) return response_api_created(self._schema.dump(new_saved_filter)) except ValidationError as e: return response_api_error('Data error', e.messages) - + def get(self, identifier): try: - saved_filter = alerts_filters_get(identifier) + saved_filter = alert_filter_get(identifier) return response_api_success(self._schema.dump(saved_filter)) - except ObjectNotFoundError: return response_api_not_found() diff --git a/source/app/business/alerts_filters.py b/source/app/business/alerts_filters.py index 3d295028d..98709534e 100644 --- a/source/app/business/alerts_filters.py +++ b/source/app/business/alerts_filters.py @@ -18,12 +18,15 @@ from app import db from app.datamgmt.filters.filters_db import get_filter_by_id +from app.business.errors import ObjectNotFoundError - -def alerts_filters_add(new_saved_filter): +def alert_filter_add(new_saved_filter): db.session.add(new_saved_filter) db.session.commit() -def alerts_filters_get(identifier): - return get_filter_by_id(identifier) +def alert_filter_get(identifier): + alert_filter = get_filter_by_id(identifier) + if not alert_filter: + raise ObjectNotFoundError() + return alert_filter diff --git a/tests/tests_rest_alerts_filters.py b/tests/tests_rest_alerts_filters.py index 65ca4673a..9709ddee5 100644 --- a/tests/tests_rest_alerts_filters.py +++ b/tests/tests_rest_alerts_filters.py @@ -19,6 +19,8 @@ from unittest import TestCase from iris import Iris +_IDENTIFIER_FOR_NONEXISTENT_OBJECT = 123456789 + class TestsRestAlertsFilters(TestCase): @@ -221,3 +223,7 @@ def test_get_alert_filter_should_return_filter_name(self): identifier = response['filter_id'] response = self._subject.get(f'/api/v2/alerts-filters/{identifier}').json() self.assertEqual(filter_name, response['filter_name']) + + def test_get_alert_filter_should_return_404_when_alert_filter_not_found(self): + response = self._subject.get(f'/api/v2/alerts-filters/{_IDENTIFIER_FOR_NONEXISTENT_OBJECT}') + self.assertEqual(404, response.status_code) From 9a1fffd2821c7316f7a83acbcd62484f6a6a394a Mon Sep 17 00:00:00 2001 From: Elise17 Date: Fri, 17 Oct 2025 20:41:25 +0200 Subject: [PATCH 37/38] Added test_get_alert_filter_should_return_404_when_user_has_not_created_alert_filter --- tests/tests_rest_alerts_filters.py | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/tests_rest_alerts_filters.py b/tests/tests_rest_alerts_filters.py index 9709ddee5..d84ca1015 100644 --- a/tests/tests_rest_alerts_filters.py +++ b/tests/tests_rest_alerts_filters.py @@ -227,3 +227,36 @@ def test_get_alert_filter_should_return_filter_name(self): def test_get_alert_filter_should_return_404_when_alert_filter_not_found(self): response = self._subject.get(f'/api/v2/alerts-filters/{_IDENTIFIER_FOR_NONEXISTENT_OBJECT}') self.assertEqual(404, response.status_code) + + def test_get_alert_filter_should_return_404_when_user_has_not_created_alert_filter(self): + user = self._subject.create_dummy_user() + body = { + 'filter_is_private': 'true', + 'filter_type': 'alerts', + 'filter_name': 'filter_name', + 'filter_description': 'filter description', + 'filter_data' : { + 'alert_tilte': 'filter name', + 'alert_description': '', + 'alert_source': '', + 'alert_tags': '', + 'alert_severity_id': '', + 'alert_start_date': '', + 'source_start_date': '', + 'source_end_date': '', + 'creation_end_date': '', + 'creation_start_date': '', + 'alert_assets': '', + 'alert_iocs': '', + 'alert_ids': '', + 'source_reference': '', + 'case_id': '', + 'custom_conditions': '', + + } + } + + response = self._subject.create('/api/v2/alerts-filters', body).json() + identifier = response['filter_id'] + response = user.get(f'/api/v2/alerts-filters/{identifier}') + self.assertEqual(404, response.status_code) From e4b499894ab7ba9ec2e478c9e1810b5ed3f84b0f Mon Sep 17 00:00:00 2001 From: Elise17 Date: Fri, 17 Oct 2025 20:42:59 +0200 Subject: [PATCH 38/38] Fixed static analysis --- source/app/blueprints/rest/v2/alerts_filters.py | 2 +- source/app/business/alerts_filters.py | 1 + tests/tests_rest_alerts_filters.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/source/app/blueprints/rest/v2/alerts_filters.py b/source/app/blueprints/rest/v2/alerts_filters.py index ff948b2be..38318796e 100644 --- a/source/app/blueprints/rest/v2/alerts_filters.py +++ b/source/app/blueprints/rest/v2/alerts_filters.py @@ -53,7 +53,7 @@ def create(self): except ValidationError as e: return response_api_error('Data error', e.messages) - + def get(self, identifier): try: saved_filter = alert_filter_get(identifier) diff --git a/source/app/business/alerts_filters.py b/source/app/business/alerts_filters.py index 98709534e..f1cb6b9cd 100644 --- a/source/app/business/alerts_filters.py +++ b/source/app/business/alerts_filters.py @@ -20,6 +20,7 @@ from app.datamgmt.filters.filters_db import get_filter_by_id from app.business.errors import ObjectNotFoundError + def alert_filter_add(new_saved_filter): db.session.add(new_saved_filter) db.session.commit() diff --git a/tests/tests_rest_alerts_filters.py b/tests/tests_rest_alerts_filters.py index d84ca1015..0326b7d28 100644 --- a/tests/tests_rest_alerts_filters.py +++ b/tests/tests_rest_alerts_filters.py @@ -223,7 +223,7 @@ def test_get_alert_filter_should_return_filter_name(self): identifier = response['filter_id'] response = self._subject.get(f'/api/v2/alerts-filters/{identifier}').json() self.assertEqual(filter_name, response['filter_name']) - + def test_get_alert_filter_should_return_404_when_alert_filter_not_found(self): response = self._subject.get(f'/api/v2/alerts-filters/{_IDENTIFIER_FOR_NONEXISTENT_OBJECT}') self.assertEqual(404, response.status_code)