"""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