Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions source/app/blueprints/rest/alerts_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ def alerts_get_route(alert_id) -> Response:


@alerts_rest_blueprint.route('/alerts/similarities/<int:alert_id>', methods=['GET'])
@endpoint_deprecated('GET', '/api/v2/alerts/{identifier}/related-alerts')
@ac_api_requires(Permissions.alerts_read)
def alerts_similarities_route(alert_id) -> Response:
"""
Expand Down
116 changes: 55 additions & 61 deletions source/app/blueprints/rest/case/case_timeline_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -216,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:
Expand Down Expand Up @@ -359,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')
Expand All @@ -371,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:
Expand Down Expand Up @@ -437,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))

Expand Down Expand Up @@ -491,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
Expand Down Expand Up @@ -521,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:
Expand All @@ -549,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()

Expand Down Expand Up @@ -594,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/<int:cur_id>', methods=['POST'])
Expand Down
14 changes: 9 additions & 5 deletions source/app/blueprints/rest/manage/manage_customers_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -239,27 +240,30 @@ 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:
return response_error("Invalid request")

customer_schema = CustomerSchema()
try:
client = create_client(request.json)
customer = customer_schema.load(request.json)

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/<int:client_id>', methods=['POST'])
Expand Down
31 changes: 31 additions & 0 deletions source/app/blueprints/rest/v2/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
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.errors import BusinessProcessingError
from app.business.errors import ObjectNotFoundError
from app.business.access_controls import access_controls_user_has_customer_access
Expand Down Expand Up @@ -165,6 +166,30 @@ 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)

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 = related_alerts_get(alert, 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)
Expand Down Expand Up @@ -243,3 +268,9 @@ def update_alert(identifier):
@ac_api_requires(Permissions.alerts_delete)
def delete_alert(identifier):
return alerts_operations.delete(identifier)


@alerts_blueprint.get('<int:identifier>/related-alerts')
@ac_api_requires(Permissions.alerts_read)
def get_related_alerts(identifier):
return alerts_operations.get_related_alerts(identifier)
3 changes: 2 additions & 1 deletion source/app/blueprints/rest/v2/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
56 changes: 56 additions & 0 deletions source/app/blueprints/rest/v2/manage_routes/customers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 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 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
from app.blueprints.iris_user import iris_current_user


class Customers:

def __init__(self):
self._schema = CustomerSchema()

def create(self):
try:
request_data = request.get_json()
customer = self._schema.load(request_data)
customers_create(iris_current_user, 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')

customers = Customers()


@customers_blueprint.post('')
@ac_api_requires(Permissions.customers_write)
def create_customer():
return customers.create()
10 changes: 4 additions & 6 deletions source/app/blueprints/rest/v2/manage_routes/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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()
Expand Down
Loading
Loading