152 lines
4.9 KiB
Python
152 lines
4.9 KiB
Python
"""Incident aggregation helpers for BGP anomalies."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.bgp_anomaly import BGPAnomaly
|
|
from app.models.bgp_incident import BGPIncident
|
|
|
|
|
|
def _severity_rank(value: str | None) -> int:
|
|
mapping = {"critical": 4, "high": 3, "medium": 2, "low": 1, "info": 0}
|
|
return mapping.get(str(value or "").lower(), 0)
|
|
|
|
|
|
def _pick_severity(values: list[str]) -> str:
|
|
ordered = sorted(values, key=_severity_rank, reverse=True)
|
|
return ordered[0] if ordered else "medium"
|
|
|
|
|
|
def _collector_regions_from_anomaly(anomaly: BGPAnomaly) -> list[dict]:
|
|
evidence = anomaly.evidence or {}
|
|
regions = evidence.get("impacted_regions") or []
|
|
if regions:
|
|
return regions
|
|
|
|
collected = []
|
|
for item in evidence.get("events") or []:
|
|
collector = item.get("collector")
|
|
location = item.get("collector_location") or {}
|
|
if collector or location:
|
|
collected.append(
|
|
{
|
|
"collector": collector,
|
|
"country": location.get("country"),
|
|
"city": location.get("city"),
|
|
"latitude": location.get("latitude"),
|
|
"longitude": location.get("longitude"),
|
|
}
|
|
)
|
|
return collected
|
|
|
|
|
|
async def create_bgp_incidents_for_anomalies(
|
|
db: AsyncSession,
|
|
*,
|
|
source: str,
|
|
snapshot_id: int | None,
|
|
task_id: int | None,
|
|
anomalies: list[BGPAnomaly],
|
|
) -> int:
|
|
if not anomalies:
|
|
return 0
|
|
|
|
grouped: dict[str, list[BGPAnomaly]] = {}
|
|
for anomaly in anomalies:
|
|
incident_key = f"{anomaly.anomaly_type}:{anomaly.prefix or 'unknown'}:{anomaly.new_origin_asn or anomaly.origin_asn or 'na'}"
|
|
grouped.setdefault(incident_key, []).append(anomaly)
|
|
|
|
existing_result = await db.execute(
|
|
select(BGPIncident.incident_key).where(BGPIncident.incident_key.in_(sorted(grouped.keys())))
|
|
)
|
|
existing_keys = {row[0] for row in existing_result.fetchall()}
|
|
|
|
created = 0
|
|
for incident_key, items in grouped.items():
|
|
if incident_key in existing_keys:
|
|
continue
|
|
|
|
items = sorted(items, key=lambda item: item.created_at or item.started_at or datetime.now(UTC))
|
|
primary = items[0]
|
|
prefixes = sorted({item.prefix for item in items if item.prefix})
|
|
asns = sorted(
|
|
{
|
|
asn
|
|
for item in items
|
|
for asn in [item.origin_asn, item.new_origin_asn]
|
|
if asn is not None
|
|
}
|
|
)
|
|
collectors = sorted(
|
|
{
|
|
collector
|
|
for item in items
|
|
for collector in (item.peer_scope or [])
|
|
if collector
|
|
}
|
|
)
|
|
regions: list[dict] = []
|
|
seen_regions: set[tuple] = set()
|
|
for item in items:
|
|
for region in _collector_regions_from_anomaly(item):
|
|
region_key = (
|
|
region.get("collector"),
|
|
region.get("country"),
|
|
region.get("city"),
|
|
)
|
|
if region_key in seen_regions:
|
|
continue
|
|
seen_regions.add(region_key)
|
|
regions.append(region)
|
|
|
|
if not collectors:
|
|
collectors = sorted(
|
|
{
|
|
region.get("collector")
|
|
for region in regions
|
|
if region.get("collector")
|
|
}
|
|
)
|
|
|
|
evidence_refs = [item.entity_key for item in items if item.entity_key]
|
|
severity = _pick_severity([item.severity for item in items])
|
|
confidence = max((item.confidence or 0.0) for item in items)
|
|
title = f"{primary.anomaly_type.replace('_', ' ').title()} incident on {primary.prefix or 'unknown prefix'}"
|
|
summary = (
|
|
f"{len(items)} anomaly signal(s) grouped into one {primary.anomaly_type} incident, "
|
|
f"affecting {len(prefixes) or 1} prefix scope(s) across {len(collectors)} collector(s)."
|
|
)
|
|
|
|
db.add(
|
|
BGPIncident(
|
|
snapshot_id=snapshot_id,
|
|
task_id=task_id,
|
|
source=source,
|
|
incident_key=incident_key,
|
|
incident_type=primary.anomaly_type,
|
|
title=title,
|
|
summary=summary,
|
|
severity=severity,
|
|
status="active",
|
|
confidence=confidence,
|
|
started_at=primary.started_at or datetime.now(UTC),
|
|
affected_prefixes=prefixes,
|
|
affected_asns=asns,
|
|
affected_collectors=collectors,
|
|
affected_regions=regions,
|
|
related_cables=[],
|
|
related_ixps=[],
|
|
evidence_refs=evidence_refs,
|
|
)
|
|
)
|
|
created += 1
|
|
|
|
if created:
|
|
await db.commit()
|
|
|
|
return created
|