fix: expand bgp pipeline and stabilize backend tests
This commit is contained in:
151
backend/app/services/bgp_incidents.py
Normal file
151
backend/app/services/bgp_incidents.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user