Skip to content

Commit 738ae85

Browse files
authored
Merge pull request #932 from dfir-iris/api_v2_alert_similarity
Api v2 alert similarity
2 parents a76930e + 0516cc4 commit 738ae85

File tree

16 files changed

+314
-93
lines changed

16 files changed

+314
-93
lines changed

source/app/blueprints/rest/alerts_routes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ def alerts_get_route(alert_id) -> Response:
257257

258258

259259
@alerts_rest_blueprint.route('/alerts/similarities/<int:alert_id>', methods=['GET'])
260+
@endpoint_deprecated('GET', '/api/v2/alerts/{identifier}/related-alerts')
260261
@ac_api_requires(Permissions.alerts_read)
261262
def alerts_similarities_route(alert_id) -> Response:
262263
"""

source/app/blueprints/rest/case/case_timeline_routes.py

Lines changed: 55 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
from app.blueprints.rest.endpoints import endpoint_deprecated
3333
from app.blueprints.iris_user import iris_current_user
3434
from app.datamgmt.case.case_assets_db import get_asset_by_name
35+
from app.datamgmt.case.case_assets_db import get_assets_by_case
3536
from app.datamgmt.case.case_events_db import add_comment_to_event
37+
from app.datamgmt.case.case_events_db import get_events_by_case
3638
from app.datamgmt.case.case_events_db import get_category_by_name
3739
from app.datamgmt.case.case_events_db import get_default_category
3840
from app.datamgmt.case.case_events_db import delete_event_comment
@@ -178,19 +180,8 @@ def case_get_timeline_state(caseid):
178180
@ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
179181
@ac_api_requires()
180182
def case_getgraph_assets(caseid):
181-
assets_cache = CaseAssets.query.with_entities(
182-
CaseEventsAssets.event_id,
183-
CaseAssets.asset_name
184-
).filter(
185-
CaseEventsAssets.case_id == caseid,
186-
).join(CaseEventsAssets.asset).all()
187-
188-
timeline = CasesEvent.query.filter(and_(
189-
CasesEvent.case_id == caseid,
190-
CasesEvent.event_in_summary
191-
)).order_by(
192-
CasesEvent.event_date
193-
).all()
183+
assets_cache = get_assets_by_case(caseid)
184+
timeline = get_events_by_case(caseid)
194185

195186
tim = []
196187
for row in timeline:
@@ -216,12 +207,7 @@ def case_getgraph_assets(caseid):
216207
@ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
217208
@ac_api_requires()
218209
def case_getgraph(caseid):
219-
timeline = CasesEvent.query.filter(and_(
220-
CasesEvent.case_id == caseid,
221-
CasesEvent.event_in_summary
222-
)).order_by(
223-
CasesEvent.event_date
224-
).all()
210+
timeline = get_events_by_case(caseid)
225211

226212
tim = []
227213
for row in timeline:
@@ -359,6 +345,11 @@ def case_filter_timeline(caseid):
359345
assets = filter_d.get('asset')
360346
assets_id = filter_d.get('asset_id')
361347
event_ids = filter_d.get('event_id')
348+
if event_ids:
349+
try:
350+
event_ids = [int(event_id) for event_id in event_ids]
351+
except Exception as _:
352+
return response_error('Invalid event id')
362353
iocs = filter_d.get('ioc')
363354
iocs_id = filter_d.get('ioc_id')
364355
tags = filter_d.get('tag')
@@ -371,6 +362,46 @@ def case_filter_timeline(caseid):
371362
sources = filter_d.get('source')
372363
flag = filter_d.get('flag')
373364

365+
cache, events_list, tim = _extract_timeline(assets, assets_id, caseid, categories, descriptions, end_date, event_ids,
366+
flag, iocs, iocs_id, raws, sources, start_date, tags, titles)
367+
368+
if request.cookies.get('session'):
369+
370+
iocs = Ioc.query.with_entities(
371+
Ioc.ioc_id,
372+
Ioc.ioc_value,
373+
Ioc.ioc_description,
374+
).filter(
375+
Ioc.case_id == caseid
376+
).all()
377+
378+
events_comments_map = {}
379+
events_comments_set = get_case_events_comments_count(events_list)
380+
for k, v in events_comments_set:
381+
events_comments_map.setdefault(k, []).append(v)
382+
383+
resp = {
384+
"tim": tim,
385+
"comments_map": events_comments_map,
386+
"assets": cache,
387+
"iocs": [ioc._asdict() for ioc in iocs],
388+
"categories": [cat.name for cat in get_events_categories()],
389+
"state": get_timeline_state(caseid=caseid)
390+
}
391+
392+
else:
393+
resp = {
394+
"timeline": tim,
395+
"state": get_timeline_state(caseid=caseid)
396+
}
397+
398+
return response_success("ok", data=resp)
399+
400+
401+
def _extract_timeline(assets: str | None, assets_id: str | None, caseid, categories: str | None,
402+
descriptions: str | None, end_date: str | None, event_ids: list[int] | None,
403+
flag: str | None, iocs: str | None, iocs_id: str | None, raws: str | None, sources: str | None,
404+
start_date: str | None, tags: str | None, titles: str | None):
374405
condition = (CasesEvent.case_id == caseid)
375406

376407
if assets:
@@ -437,11 +468,6 @@ def case_filter_timeline(caseid):
437468
EventCategory.name == category)
438469

439470
if event_ids:
440-
try:
441-
event_ids = [int(event_id) for event_id in event_ids]
442-
except Exception as _:
443-
return response_error('Invalid event id')
444-
445471
condition = and_(condition,
446472
CasesEvent.event_id.in_(event_ids))
447473

@@ -491,7 +517,7 @@ def case_filter_timeline(caseid):
491517
).filter(
492518
assets_cache_condition
493519
).join(CaseEventsAssets.asset)
494-
.join(CaseAssets.asset_type).all())
520+
.join(CaseAssets.asset_type).all())
495521

496522
iocs_cache_condition = and_(
497523
CaseEventsIoc.case_id == caseid
@@ -521,8 +547,7 @@ def case_filter_timeline(caseid):
521547
if asset.asset_id not in cache:
522548
cache[asset.asset_id] = [asset.asset_name, asset.type]
523549

524-
if (assets and asset.asset_name.lower() in assets) \
525-
or (assets_id and asset.asset_id in assets_id):
550+
if (assets and asset.asset_name.lower() in assets) or (assets_id and asset.asset_id in assets_id):
526551
if asset.event_id in assets_map:
527552
assets_map[asset.event_id] += 1
528553
else:
@@ -549,10 +574,10 @@ def case_filter_timeline(caseid):
549574
events_list = []
550575
for row in timeline:
551576
if (assets is not None or assets_id is not None) and row.event_id not in assets_filter:
552-
continue
577+
continue
553578

554579
if iocs is not None and row.event_id not in iocs_filter:
555-
continue
580+
continue
556581

557582
ras = row._asdict()
558583

@@ -594,38 +619,7 @@ def case_filter_timeline(caseid):
594619
ras['iocs'] = alki
595620

596621
tim.append(ras)
597-
598-
if request.cookies.get('session'):
599-
600-
iocs = Ioc.query.with_entities(
601-
Ioc.ioc_id,
602-
Ioc.ioc_value,
603-
Ioc.ioc_description,
604-
).filter(
605-
Ioc.case_id == caseid
606-
).all()
607-
608-
events_comments_map = {}
609-
events_comments_set = get_case_events_comments_count(events_list)
610-
for k, v in events_comments_set:
611-
events_comments_map.setdefault(k, []).append(v)
612-
613-
resp = {
614-
"tim": tim,
615-
"comments_map": events_comments_map,
616-
"assets": cache,
617-
"iocs": [ioc._asdict() for ioc in iocs],
618-
"categories": [cat.name for cat in get_events_categories()],
619-
"state": get_timeline_state(caseid=caseid)
620-
}
621-
622-
else:
623-
resp = {
624-
"timeline": tim,
625-
"state": get_timeline_state(caseid=caseid)
626-
}
627-
628-
return response_success("ok", data=resp)
622+
return cache, events_list, tim
629623

630624

631625
@case_timeline_rest_blueprint.route('/case/timeline/events/delete/<int:cur_id>', methods=['POST'])

source/app/blueprints/rest/manage/manage_customers_routes.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from app.blueprints.access_controls import ac_api_requires_client_access
4747
from app.blueprints.responses import response_error
4848
from app.blueprints.responses import response_success
49+
from app.blueprints.rest.endpoints import endpoint_deprecated
4950

5051
manage_customers_rest_blueprint = Blueprint('manage_customers_rest', __name__)
5152

@@ -239,27 +240,30 @@ def view_customers(client_id):
239240

240241

241242
@manage_customers_rest_blueprint.route('/manage/customers/add', methods=['POST'])
243+
@endpoint_deprecated('POST', '/api/v2/manage/customers')
242244
@ac_api_requires(Permissions.customers_write)
243245
def add_customers():
244246
if not request.is_json:
245247
return response_error("Invalid request")
246248

249+
customer_schema = CustomerSchema()
247250
try:
248-
client = create_client(request.json)
251+
customer = customer_schema.load(request.json)
252+
253+
create_client(customer)
249254
except ValidationError as e:
250255
return response_error(msg='Error adding customer', data=e.messages)
251256
except Exception as e:
252257
print(traceback.format_exc())
253258
return response_error(f'An error occurred during customer addition. {e}')
254259

255-
track_activity(f"Added customer {client.name}", ctx_less=True)
260+
track_activity(f"Added customer {customer.name}", ctx_less=True)
256261

257262
# Associate the created customer with the current user
258-
add_user_to_customer(iris_current_user.id, client.client_id)
263+
add_user_to_customer(iris_current_user.id, customer.client_id)
259264

260265
# Return the customer
261-
client_schema = CustomerSchema()
262-
return response_success("Added successfully", data=client_schema.dump(client))
266+
return response_success('Added successfully', data=customer_schema.dump(customer))
263267

264268

265269
@manage_customers_rest_blueprint.route('/manage/customers/delete/<int:client_id>', methods=['POST'])

source/app/blueprints/rest/v2/alerts.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from app.business.alerts import alerts_get
4141
from app.business.alerts import alerts_update
4242
from app.business.alerts import alerts_delete
43+
from app.business.alerts import related_alerts_get
4344
from app.business.errors import BusinessProcessingError
4445
from app.business.errors import ObjectNotFoundError
4546
from app.business.access_controls import access_controls_user_has_customer_access
@@ -165,6 +166,30 @@ def get(self, identifier):
165166
except ObjectNotFoundError:
166167
return response_api_not_found()
167168

169+
def get_related_alerts(self, identifier):
170+
171+
try:
172+
alert = alerts_get(iris_current_user, identifier)
173+
174+
open_alerts = request.args.get('open-alerts', 'false').lower() == 'true'
175+
open_cases = request.args.get('open-cases', 'false').lower() == 'true'
176+
closed_cases = request.args.get('closed-cases', 'false').lower() == 'true'
177+
closed_alerts = request.args.get('closed-alerts', 'false').lower() == 'true'
178+
days_back = request.args.get('days-back', 180, type=int)
179+
number_of_results = request.args.get('number-of-nodes', 100, type=int)
180+
181+
if number_of_results < 0:
182+
number_of_results = 100
183+
if days_back < 0:
184+
days_back = 180
185+
186+
similar_alerts = related_alerts_get(alert, open_alerts, closed_alerts, open_cases, closed_cases,
187+
days_back, number_of_results)
188+
return response_api_success(similar_alerts)
189+
190+
except ObjectNotFoundError:
191+
return response_api_not_found()
192+
168193
def update(self, identifier):
169194
try:
170195
alert = alerts_get(iris_current_user, identifier)
@@ -243,3 +268,9 @@ def update_alert(identifier):
243268
@ac_api_requires(Permissions.alerts_delete)
244269
def delete_alert(identifier):
245270
return alerts_operations.delete(identifier)
271+
272+
273+
@alerts_blueprint.get('<int:identifier>/related-alerts')
274+
@ac_api_requires(Permissions.alerts_read)
275+
def get_related_alerts(identifier):
276+
return alerts_operations.get_related_alerts(identifier)

source/app/blueprints/rest/v2/manage.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@
2020

2121
from app.blueprints.rest.v2.manage_routes.groups import create_groups_blueprint
2222
from app.blueprints.rest.v2.manage_routes.users import users_blueprint
23+
from app.blueprints.rest.v2.manage_routes.customers import customers_blueprint
2324

2425
manage_v2_blueprint = Blueprint('manage', __name__, url_prefix='/manage')
2526

2627
groups_blueprint = create_groups_blueprint()
2728
manage_v2_blueprint.register_blueprint(groups_blueprint)
2829
manage_v2_blueprint.register_blueprint(users_blueprint)
29-
30+
manage_v2_blueprint.register_blueprint(customers_blueprint)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# IRIS Source Code
2+
# Copyright (C) 2025 - DFIR-IRIS
3+
# contact@dfir-iris.org
4+
#
5+
# This program is free software; you can redistribute it and/or
6+
# modify it under the terms of the GNU Lesser General Public
7+
# License as published by the Free Software Foundation; either
8+
# version 3 of the License, or (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
# Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with this program; if not, write to the Free Software Foundation,
17+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18+
19+
from flask import Blueprint
20+
from flask import request
21+
from marshmallow import ValidationError
22+
23+
from app.blueprints.rest.endpoints import response_api_created
24+
from app.blueprints.rest.endpoints import response_api_error
25+
from app.blueprints.access_controls import ac_api_requires
26+
from app.models.authorization import Permissions
27+
from app.schema.marshables import CustomerSchema
28+
from app.business.customers import customers_create
29+
from app.blueprints.iris_user import iris_current_user
30+
31+
32+
class Customers:
33+
34+
def __init__(self):
35+
self._schema = CustomerSchema()
36+
37+
def create(self):
38+
try:
39+
request_data = request.get_json()
40+
customer = self._schema.load(request_data)
41+
customers_create(iris_current_user, customer)
42+
result = self._schema.dump(customer)
43+
return response_api_created(result)
44+
except ValidationError as e:
45+
return response_api_error('Data error', data=e.messages)
46+
47+
48+
customers_blueprint = Blueprint('customers_rest_v2', __name__, url_prefix='/customers')
49+
50+
customers = Customers()
51+
52+
53+
@customers_blueprint.post('')
54+
@ac_api_requires(Permissions.customers_write)
55+
def create_customer():
56+
return customers.create()

source/app/blueprints/rest/v2/manage_routes/groups.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
from app.business.groups import groups_get
3232
from app.business.groups import groups_update
3333
from app.business.groups import groups_delete
34-
from app.models.authorization import Permissions, ac_flag_match_mask
34+
from app.models.authorization import Permissions
35+
from app.models.authorization import ac_flag_match_mask
3536
from app.business.errors import BusinessProcessingError
3637
from app.business.errors import ObjectNotFoundError
3738
from app.blueprints.iris_user import iris_current_user
@@ -43,13 +44,10 @@ class Groups:
4344
def __init__(self):
4445
self._schema = AuthorizationGroupSchema()
4546

46-
def _load(self, request_data, **kwargs):
47-
return self._schema.load(request_data, **kwargs)
48-
4947
def create(self):
5048
try:
5149
request_data = request.get_json()
52-
group = self._load(request_data)
50+
group = self._schema.load(request_data)
5351
group = groups_create(group)
5452
result = self._schema.dump(group)
5553
return response_api_created(result)
@@ -69,7 +67,7 @@ def update(self, identifier):
6967
group = groups_get(identifier)
7068
request_data = request.get_json()
7169
request_data['group_id'] = identifier
72-
updated_group = self._load(request_data, instance=group, partial=True)
70+
updated_group = self._schema.load(request_data, instance=group, partial=True)
7371
if not ac_flag_match_mask(request_data['group_permissions'], Permissions.server_administrator.value) and ac_ldp_group_update(iris_current_user.id):
7472
return response_api_error('That might not be a good idea Dave', data='Update the group permissions will lock you out')
7573
groups_update()

0 commit comments

Comments
 (0)