Some checks failed
Deployment Verification / deploy-and-test (push) Failing after 29s
1231 lines
41 KiB
Python
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()
|
|
|