hcornet 506716e703
Some checks failed
Deployment Verification / deploy-and-test (push) Failing after 29s
first sync
2025-03-04 07:59:21 +01:00

1231 lines
41 KiB
Python

#!/usr/bin/env python3
#
# 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 functools import reduce
import json
from datetime import datetime, timedelta
from flask_login import current_user
from operator import and_
from sqlalchemy import desc, asc, func, tuple_, or_
from sqlalchemy.orm import joinedload
from typing import List, Tuple
import app
from app import db
from app.datamgmt.case.case_assets_db import create_asset, set_ioc_links, get_unspecified_analysis_status_id
from app.datamgmt.case.case_events_db import update_event_assets, update_event_iocs
from app.datamgmt.case.case_iocs_db import add_ioc, add_ioc_link
from app.datamgmt.manage.manage_case_state_db import get_case_state_by_name
from app.datamgmt.manage.manage_case_templates_db import case_template_pre_modifier, get_case_template_by_id, \
case_template_post_modifier
from app.datamgmt.states import update_timeline_state
from app.models import Cases, EventCategory, Tags, AssetsType, Comments, CaseAssets, alert_assets_association, \
alert_iocs_association, Ioc, IocLink
from app.models.alerts import Alert, AlertStatus, AlertCaseAssociation, SimilarAlertsCache, AlertResolutionStatus
from app.schema.marshables import IocSchema, CaseAssetsSchema, EventSchema
from app.util import add_obj_history_entry
from sqlalchemy.orm import aliased
def db_list_all_alerts():
"""
List all alerts in the database
"""
return db.session.query(Alert).all()
def get_filtered_alerts(
start_date: str = None,
end_date: str = None,
title: str = None,
description: str = None,
status: int = None,
severity: int = None,
owner: int = None,
source: str = None,
tags: str = None,
case_id: int = None,
client: int = None,
classification: int = None,
alert_ids: List[int] = None,
assets: List[str] = None,
iocs: List[str] = None,
resolution_status: int = None,
page: int = 1,
per_page: int = 10,
sort: str = 'desc'
):
"""
Get a list of alerts that match the given filter conditions
args:
start_date (datetime): The start date of the alert creation time
end_date (datetime): The end date of the alert creation time
title (str): The title of the alert
description (str): The description of the alert
status (str): The status of the alert
severity (str): The severity of the alert
owner (str): The owner of the alert
source (str): The source of the alert
tags (str): The tags of the alert
case_id (int): The case id of the alert
client (int): The client id of the alert
classification (int): The classification id of the alert
alert_ids (int): The alert ids
assets (list): The assets of the alert
iocs (list): The iocs of the alert
resolution_status (int): The resolution status of the alert
page (int): The page number
per_page (int): The number of alerts per page
sort (str): The sort order
returns:
list: A list of alerts that match the given filter conditions
"""
# Build the filter conditions
conditions = []
if start_date is not None and end_date is not None:
conditions.append(Alert.alert_creation_time.between(start_date, end_date))
if title is not None:
conditions.append(Alert.alert_title.ilike(f'%{title}%'))
if description is not None:
conditions.append(Alert.alert_description.ilike(f'%{description}%'))
if status is not None:
conditions.append(Alert.alert_status_id == status)
if severity is not None:
conditions.append(Alert.alert_severity_id == severity)
if resolution_status is not None:
conditions.append(Alert.alert_resolution_status_id == resolution_status)
if owner is not None:
if owner == -1:
conditions.append(Alert.alert_owner_id.is_(None))
else:
conditions.append(Alert.alert_owner_id == owner)
if source is not None:
conditions.append(Alert.alert_source.ilike(f'%{source}%'))
if tags is not None:
conditions.append(Alert.alert_tags.ilike(f"%{tags}%"))
if client is not None:
conditions.append(Alert.alert_customer_id == client)
if alert_ids is not None:
if isinstance(alert_ids, list):
conditions.append(Alert.alert_id.in_(alert_ids))
if classification is not None:
conditions.append(Alert.alert_classification_id == classification)
if case_id is not None:
conditions.append(Alert.cases.any(AlertCaseAssociation.case_id == case_id))
if assets is not None:
if isinstance(assets, list):
conditions.append(Alert.assets.any(CaseAssets.asset_name.in_(assets)))
if iocs is not None:
if isinstance(iocs, list):
conditions.append(Alert.iocs.any(Ioc.ioc_value.in_(iocs)))
if len(conditions) > 1:
conditions = [reduce(and_, conditions)]
order_func = desc if sort == "desc" else asc
try:
# Query the alerts using the filter conditions
filtered_alerts = db.session.query(
Alert
).filter(
*conditions
).options(
joinedload(Alert.severity), joinedload(Alert.status), joinedload(Alert.customer), joinedload(Alert.cases),
joinedload(Alert.iocs), joinedload(Alert.assets)
).order_by(
order_func(Alert.alert_source_event_time)
).paginate(page, per_page, error_out=False)
except Exception as e:
app.app.logger.exception(f"Error getting alerts: {str(e)}")
filtered_alerts = None
return filtered_alerts
def add_alert(
title,
description,
source,
status,
severity,
owner
):
"""
Add an alert to the database
args:
title (str): The title of the alert
description (str): The description of the alert
source (str): The source of the alert
status (str): The status of the alert
severity (str): The severity of the alert
owner (str): The owner of the alert
returns:
Alert: The alert that was added to the database
"""
# Create the alert
alert = Alert()
alert.alert_title = title
alert.alert_description = description
alert.alert_source = source
alert.alert_status = status
alert.alert_severity = severity
alert.alert_owner_id = owner
# Add the alert to the database
db.session.add(alert)
db.session.commit()
return alert
def get_alert_by_id(alert_id: int) -> Alert:
"""
Get an alert from the database
args:
alert_id (int): The ID of the alert
returns:
Alert: The alert that was retrieved from the database
"""
return (
db.session.query(Alert)
.options(joinedload(Alert.iocs), joinedload(Alert.assets))
.filter(Alert.alert_id == alert_id)
.first()
)
def get_unspecified_event_category():
"""
Get the id of the 'Unspecified' event category
"""
event_cat = EventCategory.query.filter(
EventCategory.name == 'Unspecified'
).first()
return event_cat
def create_case_from_alerts(alerts: List[Alert], iocs_list: List[str], assets_list: List[str], case_title: str,
note: str, import_as_event: bool, case_tags: str, template_id: int) -> Cases:
"""
Create a case from multiple alerts
args:
alerts (Alert): The Alerts
iocs_list (list): The list of IOCs
assets_list (list): The list of assets
note (str): The note to add to the case
import_as_event (bool): Whether to import the alert as an event
case_tags (str): The tags to add to the case
case_title (str): The title of the case
template_id (int): The ID of the template to use
returns:
Cases: The case that was created from the alert
"""
escalation_note = ""
if note:
escalation_note = f"\n\n### Escalation note\n\n{note}\n\n"
if template_id is not None and template_id != 0 and template_id != '':
case_template = get_case_template_by_id(template_id)
if case_template:
case_template_title_prefix = case_template.title_prefix
# Create the case
case = Cases(
name=f"[ALERT]{case_template_title_prefix} "
f"Merge of alerts {', '.join([str(alert.alert_id) for alert in alerts])}" if not case_title else
f"{case_template_title_prefix} {case_title}",
description=f"*Alerts escalated by {current_user.name}*\n\n{escalation_note}"
f"[Alerts link](/alerts?alert_ids={','.join([str(alert.alert_id) for alert in alerts])})",
soc_id='',
client_id=alerts[0].alert_customer_id,
user=current_user,
classification_id=alerts[0].alert_classification_id,
state_id=get_case_state_by_name('Open').state_id
)
case.save()
for tag in case_tags.split(','):
tag = Tags(tag_title=tag)
tag = tag.save()
case.tags.append(tag)
db.session.commit()
# Link the alert to the case
for alert in alerts:
alert.cases.append(case)
ioc_links = []
asset_links = []
# Add the IOCs to the case
for ioc_uuid in iocs_list:
for alert_ioc in alert.iocs:
if str(alert_ioc.ioc_uuid) == ioc_uuid:
ioc, existed = add_ioc(alert_ioc, current_user.id, case.case_id)
add_ioc_link(ioc.ioc_id, case.case_id)
ioc_links.append(ioc.ioc_id)
# Add the assets to the case
for asset_uuid in assets_list:
for alert_asset in alert.assets:
if str(alert_asset.asset_uuid) == asset_uuid:
alert_asset.analysis_status_id = get_unspecified_analysis_status_id()
asset = create_asset(asset=alert_asset,
caseid=case.case_id,
user_id=current_user.id
)
asset.asset_uuid = alert_asset.asset_uuid
set_ioc_links(ioc_links, asset.asset_id)
asset_links.append(asset.asset_id)
# Add event to timeline
if import_as_event:
unspecified_cat = get_unspecified_event_category()
event_schema = EventSchema()
event = event_schema.load({
'event_title': f"[ALERT] {alert.alert_title}",
'event_content': alert.alert_description,
'event_source': alert.alert_source,
'event_raw': json.dumps(alert.alert_source_content, indent=4),
'event_date': alert.alert_source_event_time.strftime("%Y-%m-%dT%H:%M:%S.%f"),
'event_date_wtz': alert.alert_source_event_time.strftime("%Y-%m-%dT%H:%M:%S.%f"),
'event_iocs': ioc_links,
'event_assets': asset_links,
'event_tags': alert.alert_tags,
'event_tz': '+00:00',
'event_category_id': unspecified_cat.id,
}, session=db.session)
event.case_id = case.case_id
event.user_id = current_user.id
event.event_added = datetime.utcnow()
add_obj_history_entry(event, 'created')
db.session.add(event)
update_timeline_state(caseid=case.case_id)
event.category = [unspecified_cat]
update_event_assets(event_id=event.event_id,
caseid=case.case_id,
assets_list=asset_links,
iocs_list=ioc_links,
sync_iocs_assets=False)
update_event_iocs(event_id=event.event_id,
caseid=case.case_id,
iocs_list=ioc_links)
if template_id is not None and template_id != 0 and template_id != '':
case, logs = case_template_post_modifier(case, template_id)
db.session.commit()
return case
def create_case_from_alert(alert: Alert, iocs_list: List[str], assets_list: List[str], case_title: str,
note: str, import_as_event: bool, case_tags: str, template_id: int) -> Cases:
"""
Create a case from an alert
args:
alert (Alert): The Alert
iocs_list (list): The list of IOCs
assets_list (list): The list of assets
note (str): The note to add to the case
import_as_event (bool): Whether to import the alert as an event
case_tags (str): The tags to add to the case
case_title (str): The title of the case
template_id (int): The template to use for the case
returns:
Cases: The case that was created from the alert
"""
escalation_note = ""
if note:
escalation_note = f"\n\n### Escalation note\n\n{note}\n\n"
case_template_title_prefix = ""
if template_id is not None and template_id != 0 and template_id != '':
case_template = get_case_template_by_id(template_id)
if case_template:
case_template_title_prefix = case_template.title_prefix
# Create the case
case = Cases(
name=f"[ALERT]{case_template_title_prefix} {alert.alert_title}" if not case_title else f"{case_template_title_prefix} {case_title}",
description=f"*Alert escalated by {current_user.name}*\n\n{escalation_note}"
f"### Alert description\n\n{alert.alert_description}"
f"\n\n### IRIS alert link\n\n"
f"[<i class='fa-solid fa-bell'></i> #{alert.alert_id}](/alerts?alert_ids={alert.alert_id})",
soc_id=alert.alert_id,
client_id=alert.alert_customer_id,
user=current_user,
classification_id=alert.alert_classification_id,
state_id=get_case_state_by_name('Open').state_id
)
case.save()
for tag in case_tags.split(','):
tag = Tags(tag_title=tag)
tag = tag.save()
case.tags.append(tag)
db.session.commit()
# Link the alert to the case
alert.cases.append(case)
ioc_links = []
asset_links = []
# Add the IOCs to the case
for ioc_uuid in iocs_list:
for alert_ioc in alert.iocs:
if str(alert_ioc.ioc_uuid) == ioc_uuid:
ioc, existed = add_ioc(alert_ioc, current_user.id, case.case_id)
add_ioc_link(ioc.ioc_id, case.case_id)
ioc_links.append(ioc.ioc_id)
# Add the assets to the case
for asset_uuid in assets_list:
for alert_asset in alert.assets:
if str(alert_asset.asset_uuid) == asset_uuid:
alert_asset.analysis_status_id = get_unspecified_analysis_status_id()
asset = create_asset(asset=alert_asset,
caseid=case.case_id,
user_id=current_user.id
)
asset.asset_uuid = alert_asset.asset_uuid
set_ioc_links(ioc_links, asset.asset_id)
asset_links.append(asset.asset_id)
# Add event to timeline
if import_as_event:
unspecified_cat = get_unspecified_event_category()
event_schema = EventSchema()
event = event_schema.load({
'event_title': f"[ALERT] {alert.alert_title}",
'event_content': alert.alert_description if alert.alert_description else "",
'event_source': alert.alert_source if alert.alert_source else "",
'event_raw': json.dumps(alert.alert_source_content, indent=4) if alert.alert_source_content else "",
'event_date': alert.alert_source_event_time.strftime("%Y-%m-%dT%H:%M:%S.%f") if alert.alert_source_event_time else datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f"),
'event_date_wtz': alert.alert_source_event_time.strftime("%Y-%m-%dT%H:%M:%S.%f") if alert.alert_source_event_time else datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f"),
'event_iocs': ioc_links,
'event_assets': asset_links,
'event_tags': alert.alert_tags + ',alert' if alert.alert_tags else "alert",
'event_tz': '+00:00',
'event_category_id': unspecified_cat.id,
'event_in_graph': True,
'event_in_summary': True
}, session=db.session)
event.case_id = case.case_id
event.user_id = current_user.id
event.event_added = datetime.utcnow()
add_obj_history_entry(event, 'created')
db.session.add(event)
update_timeline_state(caseid=case.case_id)
event.category = [unspecified_cat]
update_event_assets(event_id=event.event_id,
caseid=case.case_id,
assets_list=asset_links,
iocs_list=ioc_links,
sync_iocs_assets=False)
update_event_iocs(event_id=event.event_id,
caseid=case.case_id,
iocs_list=ioc_links)
if template_id is not None and template_id != 0 and template_id != '':
case, logs = case_template_post_modifier(case, template_id)
db.session.commit()
return case
def merge_alert_in_case(alert: Alert, case: Cases, iocs_list: List[str],
assets_list: List[str], note: str, import_as_event: bool, case_tags: str):
"""
Merge an alert in a case
args:
alert (Alert): The Alert
case (Cases): The Case
iocs_list (list): The list of IOCs
case_title (str): The title of the case
assets_list (list): The list of assets
note (str): The note to add to the case
import_as_event (bool): Whether to import the alert as an event
case_tags (str): The tags to add to the case
"""
if case in alert.cases:
return case
escalation_note = ""
if note:
escalation_note = f"\n\n### Escalation note\n\n{note}\n\n"
case.description += f"\n\n*Alert [#{alert.alert_id}](/alerts?alert_ids={alert.alert_id}) escalated by {current_user.name}*\n\n{escalation_note}"
for tag in case_tags.split(',') if case_tags else []:
tag = Tags(tag_title=tag).save()
case.tags.append(tag)
# Link the alert to the case
alert.cases.append(case)
ioc_links = []
asset_links = []
# Add the IOCs to the case
for ioc_uuid in iocs_list:
for alert_ioc in alert.iocs:
if str(alert_ioc.ioc_uuid) == ioc_uuid:
ioc, existed = add_ioc(alert_ioc, current_user.id, case.case_id)
add_ioc_link(ioc.ioc_id, case.case_id)
ioc_links.append(ioc.ioc_id)
# Add the assets to the case
for asset_uuid in assets_list:
for alert_asset in alert.assets:
if str(alert_asset.asset_uuid) == asset_uuid:
alert_asset.analysis_status_id = get_unspecified_analysis_status_id()
asset = create_asset(asset=alert_asset,
caseid=case.case_id,
user_id=current_user.id
)
set_ioc_links(ioc_links, asset.asset_id)
asset_links.append(asset.asset_id)
# Add event to timeline
if import_as_event:
unspecified_cat = get_unspecified_event_category()
event_schema = EventSchema()
event = event_schema.load({
'event_title': f"[ALERT] {alert.alert_title}",
'event_content': alert.alert_description,
'event_source': alert.alert_source,
'event_raw': json.dumps(alert.alert_source_content, indent=4),
'event_date': alert.alert_source_event_time.strftime("%Y-%m-%dT%H:%M:%S.%f"),
'event_date_wtz': alert.alert_source_event_time.strftime("%Y-%m-%dT%H:%M:%S.%f"),
'event_iocs': ioc_links,
'event_assets': asset_links,
'event_tags': alert.alert_tags,
'event_tz': '+00:00',
'event_category_id': unspecified_cat.id,
'event_in_graph': True,
'event_in_summary': True
}, session=db.session)
event.case_id = case.case_id
event.user_id = current_user.id
event.event_added = datetime.utcnow()
add_obj_history_entry(event, 'created')
db.session.add(event)
update_timeline_state(caseid=case.case_id)
event.category = [unspecified_cat]
update_event_assets(event_id=event.event_id,
caseid=case.case_id,
assets_list=asset_links,
iocs_list=ioc_links,
sync_iocs_assets=False)
update_event_iocs(event_id=event.event_id,
caseid=case.case_id,
iocs_list=ioc_links)
db.session.commit()
def unmerge_alert_from_case(alert: Alert, case: Cases):
"""
Unmerge an alert from a case
args:
alert (Alert): The Alert
case (Cases): The Case
"""
# Check if the case is in the alert.cases list
if case in alert.cases:
# Unlink the alert from the case
alert.cases.remove(case)
db.session.commit()
else:
return False, f"Case {case.case_id} not linked with alert {alert.alert_id}"
return True, f"Alert {alert.alert_id} unlinked from case {case.case_id}"
def get_alert_status_list():
"""
Get a list of alert statuses
returns:
list: A list of alert statuses
"""
return db.session.query(AlertStatus).distinct().all()
def get_alert_status_by_id(status_id: int) -> AlertStatus:
"""
Get an alert status from the database
args:
status_id (int): The ID of the alert status
returns:
AlertStatus: The alert status that was retrieved from the database
"""
return db.session.query(AlertStatus).filter(AlertStatus.status_id == status_id).first()
def search_alert_status_by_name(status_name: str, exact_match: False) -> AlertStatus:
"""
Get an alert status from the database from its name
args:
status_name (str): The name of the alert status
exact_match (bool): Whether to perform an exact match or not
returns:
AlertStatus: The alert status that was retrieved from the database
"""
if exact_match:
return db.session.query(AlertStatus).filter(func.lower(AlertStatus.status_name) == status_name.lower()).first()
return db.session.query(AlertStatus).filter(AlertStatus.status_name.ilike(f"%{status_name}%")).all()
def get_alert_resolution_list():
"""
Get a list of alert resolutions
returns:
list: A list of alert resolutions
"""
return db.session.query(AlertResolutionStatus).distinct().all()
def get_alert_resolution_by_id(resolution_id: int) -> AlertResolutionStatus:
"""
Get an alert resolution from the database
args:
resolution_id (int): The ID of the alert resolution
returns:
Alertresolution: The alert resolution that was retrieved from the database
"""
return db.session.query(AlertResolutionStatus).filter(AlertResolutionStatus.resolution_status_id == resolution_id).first()
def search_alert_resolution_by_name(resolution_status_name: str, exact_match: False) -> AlertResolutionStatus:
"""
Get an alert resolution from the database from its name
args:
resolution_name (str): The name of the alert resolution
exact_match (bool): Whether to perform an exact match or not
returns:
Alertresolution: The alert resolution that was retrieved from the database
"""
if exact_match:
return db.session.query(AlertResolutionStatus).filter(func.lower(
AlertResolutionStatus.resolution_status_name) == resolution_status_name.lower()).first()
return db.session.query(AlertResolutionStatus).filter(
AlertResolutionStatus.resolution_status_name.ilike(f"%{resolution_status_name}%")).all()
def cache_similar_alert(customer_id, assets, iocs, alert_id, creation_date):
"""
Cache similar alerts
args:
customer_id (int): The ID of the customer
assets (list): The list of assets
iocs (list): The list of IOCs
alert_id (int): The ID of the alert
creation_date (datetime): The creation date of the alert
returns:
None
"""
for asset in assets:
cache_entry = SimilarAlertsCache(customer_id=customer_id, asset_name=asset['asset_name'],
asset_type_id=asset["asset_type_id"], alert_id=alert_id,
created_at=creation_date)
db.session.add(cache_entry)
for ioc in iocs:
cache_entry = SimilarAlertsCache(customer_id=customer_id, ioc_value=ioc['ioc_value'],
ioc_type_id=ioc['ioc_type_id'], alert_id=alert_id,
created_at=creation_date)
db.session.add(cache_entry)
db.session.commit()
def delete_similar_alert_cache(alert_id):
"""
Delete the similar alert cache
args:
alert_id (int): The ID of the alert
returns:
None
"""
SimilarAlertsCache.query.filter(SimilarAlertsCache.alert_id == alert_id).delete()
db.session.commit()
def delete_similar_alerts_cache(alert_ids: List[int]):
"""
Delete the similar alerts cache
args:
alert_ids (List(int)): The ID of the alert
returns:
None
"""
SimilarAlertsCache.query.filter(SimilarAlertsCache.alert_id.in_(alert_ids)).delete()
db.session.commit()
def get_related_alerts(customer_id, assets, iocs, details=False):
"""
Check if an alert is related to another alert
args:
customer_id (int): The ID of the customer
assets (list): The list of assets
iocs (list): The list of IOCs
details (bool): Whether to return the details of the related alerts
returns:
bool: True if the alert is related to another alert, False otherwise
"""
asset_names = [asset.asset_name for asset in assets]
ioc_values = [ioc.ioc_value for ioc in iocs]
similar_assets = SimilarAlertsCache.query.filter(
SimilarAlertsCache.customer_id == customer_id,
SimilarAlertsCache.asset_name.in_(asset_names)
).all()
similar_iocs = SimilarAlertsCache.query.filter(
SimilarAlertsCache.customer_id == customer_id,
SimilarAlertsCache.ioc_value.in_(ioc_values)
).all()
similarities = {
'assets': [asset.alert_id for asset in similar_assets],
'iocs': [ioc.alert_id for ioc in similar_iocs]
}
return similarities
def get_related_alerts_details(customer_id, assets, iocs, open_alerts, closed_alerts, open_cases, closed_cases,
days_back=30, number_of_results=200):
"""
Get the details of the related alerts
args:
customer_id (int): The ID of the customer
assets (list): The list of assets
iocs (list): The list of IOCs
open_alerts (bool): Include open alerts
closed_alerts (bool): Include closed alerts
open_cases (bool): Include open cases
closed_cases (bool): Include closed cases
days_back (int): The number of days to look back
number_of_results (int): The maximum number of alerts to return
returns:
dict: The details of the related alerts with matched assets and/or IOCs
"""
if not assets and not iocs:
return {
'nodes': [],
'edges': []
}
asset_names = [(asset.asset_name, asset.asset_type_id) for asset in assets]
ioc_values = [(ioc.ioc_value, ioc.ioc_type_id) for ioc in iocs]
asset_type_alias = aliased(AssetsType)
alert_status_filter = []
conditions = and_(SimilarAlertsCache.customer_id == customer_id,
or_(
tuple_(SimilarAlertsCache.asset_name,SimilarAlertsCache.asset_type_id).in_(asset_names)
,
tuple_(SimilarAlertsCache.ioc_value, SimilarAlertsCache.ioc_type_id).in_(ioc_values)
))
if open_alerts:
open_alert_status_ids = AlertStatus.query.with_entities(
AlertStatus.status_id
).filter(AlertStatus.status_name.in_(['New', 'Assigned', 'In progress', 'Pending', 'Unspecified'])).all()
alert_status_filter += open_alert_status_ids
if closed_alerts:
closed_alert_status_ids = AlertStatus.query.with_entities(
AlertStatus.status_id
).filter(AlertStatus.status_name.in_(['Closed', 'Merged', 'Escalated'])).all()
alert_status_filter += closed_alert_status_ids
alert_status_filter = [status_id[0] for status_id in alert_status_filter]
# Add alert_status_filter to the conditions
conditions = and_(conditions, Alert.alert_status_id.in_(alert_status_filter))
related_alerts = (
db.session.query(Alert, SimilarAlertsCache.asset_name, SimilarAlertsCache.ioc_value,
asset_type_alias.asset_icon_not_compromised)
.join(SimilarAlertsCache, Alert.alert_id == SimilarAlertsCache.alert_id)
.outerjoin(asset_type_alias, SimilarAlertsCache.asset_type_id == asset_type_alias.asset_id)
.filter(conditions)
.filter(SimilarAlertsCache.created_at >= (func.now() - timedelta(days=days_back)))
.limit(number_of_results)
.all()
)
alerts_dict = {}
for alert, asset_name, ioc_value, asset_icon_not_compromised in related_alerts:
if alert.alert_id not in alerts_dict:
alerts_dict[alert.alert_id] = {'alert': alert, 'assets': [], 'iocs': []}
if any(name == asset_name for name, _ in asset_names):
asset_info = {'asset_name': asset_name, 'icon': asset_icon_not_compromised}
alerts_dict[alert.alert_id]['assets'].append(asset_info)
if any(value == ioc_value for value, _ in ioc_values):
alerts_dict[alert.alert_id]['iocs'].append(ioc_value)
nodes = []
edges = []
added_assets = set()
added_iocs = set()
added_cases = set()
for alert_id, alert_info in alerts_dict.items():
alert_color = '#c95029' if alert_info['alert'].status.status_name in ['Closed', 'Merged', 'Escalated'] else ''
nodes.append({
'id': f'alert_{alert_id}',
'label': f'[Closed] Alert #{alert_id}' if alert_color != '' else f'Alert #{alert_id}',
'title': alert_info['alert'].alert_title,
'group': 'alert',
'shape': 'icon',
'icon': {
'face': 'FontAwesome',
'code': '\uf0f3',
'color': alert_color,
'weight': "bold"
},
'font': "12px verdana white" if current_user.in_dark_mode else ''
})
for asset_info in alert_info['assets']:
asset_id = asset_info['asset_name']
if asset_id not in added_assets:
nodes.append({
'id': f'asset_{asset_id}',
'label': asset_id,
'group': 'asset',
'shape': 'image',
'image': '/static/assets/img/graph/' + asset_info['icon'],
'font': "12px verdana white" if current_user.in_dark_mode else ''
})
added_assets.add(asset_id)
edges.append({
'from': f'alert_{alert_id}',
'to': f'asset_{asset_id}'
})
for ioc_value in alert_info['iocs']:
if ioc_value not in added_iocs:
nodes.append({
'id': f'ioc_{ioc_value}',
'label': ioc_value,
'group': 'ioc',
'shape': 'icon',
'icon': {
'face': 'FontAwesome',
'code': '\ue4a8',
'color': 'white' if current_user.in_dark_mode else '',
'weight': "bold"
},
'font': "12px verdana white" if current_user.in_dark_mode else ''
})
added_iocs.add(ioc_value)
edges.append({
'from': f'alert_{alert_id}',
'to': f'ioc_{ioc_value}',
'dashes': True
})
if open_cases or closed_cases:
close_condition = None
if open_cases and not closed_cases:
close_condition = Cases.close_date.is_(None)
if closed_cases and not open_cases:
close_condition = Cases.close_date.isnot(None)
if open_cases and closed_cases:
close_condition = Cases.close_date.isnot(None) | Cases.close_date.is_(None)
# Find cases with matching IOC value and IOC type
matching_ioc_cases = (
db.session.query(IocLink)
.with_entities(IocLink.case_id, Ioc.ioc_value, Cases.name, Cases.close_date)
.join(IocLink.ioc, IocLink.case)
.filter(
Ioc.ioc_value.in_(added_iocs),
close_condition
)
.distinct()
.all()
)
# Find cases with matching asset_title and asset_type
matching_asset_cases = (
db.session.query(CaseAssets)
.with_entities(CaseAssets.case_id, CaseAssets.asset_name, Cases.name, Cases.close_date)
.join(CaseAssets.case)
.filter(
CaseAssets.asset_name.in_(added_assets),
close_condition
)
.distinct(CaseAssets.case_id)
.all()
)
cases_data = {}
# Iterate through matching_ioc_cases and update cases_data
for case_id, ioc_value, case_name, close_date in matching_ioc_cases:
if case_id not in cases_data:
cases_data[case_id] = {'name': case_name, 'matching_ioc': [], 'matching_assets': [],
'close_date': close_date}
cases_data[case_id]['matching_ioc'].append(ioc_value)
# Iterate through matching_asset_cases and update cases_data
for case_id, asset_name, case_name, close_date in matching_asset_cases:
if case_id not in cases_data:
cases_data[case_id] = {'name': case_name, 'matching_ioc': [], 'matching_assets': [],
'close_date': close_date}
cases_data[case_id]['matching_assets'].append(asset_name)
# Add nodes and edges for matching cases
for case_id in cases_data:
if case_id not in added_cases:
nodes.append({
'id': f'case_{case_id}',
'label': f'[Closed] Case #{case_id}' if cases_data[case_id].get('close_date') else f'Case #{case_id}',
'title': cases_data[case_id]['name'],
'group': 'case',
'shape': 'icon',
'icon': {
'face': 'FontAwesome',
'code': '\uf0b1',
'color': '#c95029' if cases_data[case_id].get('close_date') else '#4cba4f'
},
'font': "12px verdana white" if current_user.in_dark_mode else ''
})
added_cases.add(case_id)
# Add edges for matching IOC
for ioc_value in cases_data[case_id]['matching_ioc']:
edges.append({
'from': f'ioc_{ioc_value}',
'to': f'case_{case_id}',
'dashes': True
})
# Add edges for matching assets
for asset_name in cases_data[case_id]['matching_assets']:
edges.append({
'from': f'asset_{asset_name}',
'to': f'case_{case_id}',
'dashes': True
})
return {
'nodes': nodes,
'edges': edges
}
def get_alert_comments(alert_id: int) -> List[Comments]:
"""
Get the comments of an alert
args:
alert_id (int): The ID of the alert
returns:
list: The list of comments
"""
return Comments.query.filter(Comments.comment_alert_id == alert_id).all()
def get_alert_comment(alert_id: int, comment_id: int) -> Comments:
"""
Get a comment of an alert
args:
alert_id (int): The ID of the alert
comment_id (int): The ID of the comment
returns:
Comments: The comment
"""
return Comments.query.filter(
Comments.comment_alert_id == alert_id,
Comments.comment_id == comment_id
).first()
def delete_alert_comment(comment_id: int, alert_id: int) -> Tuple[bool, str]:
"""
Delete a comment of an alert
args:
comment_id (int): The ID of the comment
"""
comment = Comments.query.filter(
Comments.comment_id == comment_id,
Comments.comment_user_id == current_user.id,
Comments.comment_alert_id == alert_id
).first()
if not comment:
return False, "You are not allowed to delete this comment"
db.session.delete(comment)
db.session.commit()
return True, "Comment deleted successfully"
def remove_alerts_from_assets_by_ids(alert_ids: List[int]) -> None:
"""
Remove the alerts from the CaseAssets based on the alert_ids
args:
alert_ids (List[int]): list of alerts to remove
returns:
None
"""
# Query the affected CaseAssets based on the alert_ids
affected_case_assets = (
db.session.query(CaseAssets)
.join(alert_assets_association)
.join(Alert, alert_assets_association.c.alert_id == Alert.alert_id)
.filter(Alert.alert_id.in_(alert_ids))
.all()
)
# Remove the alerts and delete the CaseAssets if not related to a case
for case_asset in affected_case_assets:
# Remove the alerts based on alert_ids
case_asset.alerts = [alert for alert in case_asset.alerts if alert.alert_id not in alert_ids]
# Delete the CaseAsset if it's not related to a case
if case_asset.case_id is None:
db.session.delete(case_asset)
# Commit the changes
db.session.commit()
def remove_alerts_from_iocs_by_ids(alert_ids: List[int]) -> None:
"""
Remove the alerts from the Ioc based on the alert_ids
args:
alert_ids (List[int]): list of alerts to remove
returns:
None
"""
# Query the affected CaseAssets based on the alert_ids
affected_case_iocs = (
db.session.query(Ioc)
.join(alert_iocs_association)
.join(Alert, alert_iocs_association.c.alert_id == Alert.alert_id)
.filter(Alert.alert_id.in_(alert_ids))
.all()
)
# Remove the alerts and delete the Ioc if not related to a case
for ioc in affected_case_iocs:
# Remove the alerts based on alert_ids
ioc.alerts = [alert for alert in ioc.alerts if alert.alert_id not in alert_ids]
# Commit the changes
db.session.commit()
def remove_case_alerts_by_ids(alert_ids: List[int]) -> None:
"""
Remove the alerts from the Case based on the alert_ids
args:
alert_ids (List[int]): list of alerts to remove
returns:
None
"""
affected_cases = (
db.session.query(Cases)
.join(AlertCaseAssociation)
.join(Alert, AlertCaseAssociation.alert_id == Alert.alert_id)
.filter(Alert.alert_id.in_(alert_ids))
.all()
)
for case in affected_cases:
# Remove the alerts based on alert_ids
case.alerts = [alert for alert in case.alerts if alert.alert_id not in alert_ids]
db.session.query(AlertCaseAssociation).filter(
AlertCaseAssociation.alert_id.in_(alert_ids)
).delete(synchronize_session='fetch')
db.session.commit()
def delete_alerts(alert_ids: List[int]) -> tuple[bool, str]:
"""
Delete multiples alerts from the database
args:
alert_ids (List[int]): list of alerts to delete
returns:
True if deleted successfully
"""
try:
delete_similar_alerts_cache(alert_ids)
remove_alerts_from_assets_by_ids(alert_ids)
remove_alerts_from_iocs_by_ids(alert_ids)
remove_case_alerts_by_ids(alert_ids)
Comments.query.filter(Comments.comment_alert_id.in_(alert_ids)).delete()
Alert.query.filter(Alert.alert_id.in_(alert_ids)).delete()
except Exception as e:
db.session.rollback()
app.logger.exception(str(e))
return False, "Server side error"
return True, ""
def get_alert_status_by_name(name: str) -> AlertStatus:
"""
Get the alert status by name
args:
name (str): The name of the alert status
returns:
AlertStatus: The alert status
"""
return AlertStatus.query.filter(AlertStatus.status_name == name).first()