fix: stabilize earth bgp geography and rendering
This commit is contained in:
@@ -55,6 +55,11 @@ def _unique_peers(events: list[dict[str, Any]]) -> list[int]:
|
||||
return sorted(peers)
|
||||
|
||||
|
||||
def _path_signature(metadata: dict[str, Any]) -> tuple[int, ...]:
|
||||
path = metadata.get("as_path") or []
|
||||
return tuple(int(asn) for asn in path if asn is not None)
|
||||
|
||||
|
||||
def detect_origin_change_anomalies(
|
||||
*,
|
||||
source: str,
|
||||
@@ -100,6 +105,7 @@ def detect_origin_change_anomalies(
|
||||
)
|
||||
sample_metadata = sample_event.get("metadata") or {}
|
||||
sample_enrichment = sample_metadata.get("enrichment") or {}
|
||||
sample_prefix_geography = sample_enrichment.get("prefix_geography") or {}
|
||||
anomaly_type = "origin_change"
|
||||
severity = "critical"
|
||||
confidence = 0.86
|
||||
@@ -140,8 +146,10 @@ def detect_origin_change_anomalies(
|
||||
"origin_asn_profile": sample_enrichment.get("origin_asn_profile"),
|
||||
"new_origin_asn_profile": sample_enrichment.get("new_origin_asn_profile"),
|
||||
"rpki_validation": sample_enrichment.get("rpki_validation"),
|
||||
"prefix_geography": sample_prefix_geography,
|
||||
"prefix_scope": sample_enrichment.get("prefix_scope"),
|
||||
"impacted_regions": related_regions
|
||||
"impacted_regions": sample_prefix_geography.get("regions")
|
||||
or related_regions
|
||||
or sample_enrichment.get("prefix_scope", {}).get("regions", []),
|
||||
},
|
||||
)
|
||||
@@ -180,6 +188,7 @@ def detect_more_specific_burst_anomalies(
|
||||
|
||||
sample = more_specifics[0].get("metadata") or {}
|
||||
sample_enrichment = sample.get("enrichment") or {}
|
||||
sample_prefix_geography = sample_enrichment.get("prefix_geography") or {}
|
||||
event_count = len(more_specifics)
|
||||
anomalies.append(
|
||||
BGPAnomaly(
|
||||
@@ -205,8 +214,10 @@ def detect_more_specific_burst_anomalies(
|
||||
"unique_prefixes": unique_prefixes,
|
||||
"rpki_validation": sample_enrichment.get("rpki_validation"),
|
||||
"origin_asn_profile": sample_enrichment.get("origin_asn_profile"),
|
||||
"prefix_geography": sample_prefix_geography,
|
||||
"prefix_scope": sample_enrichment.get("prefix_scope"),
|
||||
"impacted_regions": _iter_event_regions(more_specifics)
|
||||
"impacted_regions": sample_prefix_geography.get("regions")
|
||||
or _iter_event_regions(more_specifics)
|
||||
or sample_enrichment.get("prefix_scope", {}).get("regions", []),
|
||||
},
|
||||
)
|
||||
@@ -242,6 +253,7 @@ def detect_mass_withdrawal_anomalies(
|
||||
sample_event = related_events[0] if related_events else {}
|
||||
sample_metadata = sample_event.get("metadata") or {}
|
||||
sample_enrichment = sample_metadata.get("enrichment") or {}
|
||||
sample_prefix_geography = sample_enrichment.get("prefix_geography") or {}
|
||||
severity = "medium"
|
||||
if count >= 4 or len(related_collectors) >= 3:
|
||||
severity = "high"
|
||||
@@ -277,8 +289,175 @@ def detect_mass_withdrawal_anomalies(
|
||||
],
|
||||
"origin_asn_profile": sample_enrichment.get("origin_asn_profile"),
|
||||
"rpki_validation": sample_enrichment.get("rpki_validation"),
|
||||
"prefix_geography": sample_prefix_geography,
|
||||
"prefix_scope": sample_enrichment.get("prefix_scope"),
|
||||
"impacted_regions": _iter_event_regions(related_events)
|
||||
"impacted_regions": sample_prefix_geography.get("regions")
|
||||
or _iter_event_regions(related_events)
|
||||
or sample_enrichment.get("prefix_scope", {}).get("regions", []),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return anomalies
|
||||
|
||||
|
||||
def detect_route_leak_anomalies(
|
||||
*,
|
||||
source: str,
|
||||
snapshot_id: int | None,
|
||||
task_id: int | None,
|
||||
events: list[dict[str, Any]],
|
||||
) -> list[BGPAnomaly]:
|
||||
events_by_prefix: defaultdict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
for event in events:
|
||||
metadata = event.get("metadata") or {}
|
||||
prefix = metadata.get("prefix")
|
||||
if prefix and metadata.get("event_type") == "announcement":
|
||||
events_by_prefix[str(prefix)].append(event)
|
||||
|
||||
anomalies: list[BGPAnomaly] = []
|
||||
for prefix, related_events in events_by_prefix.items():
|
||||
related_collectors = _unique_collectors(related_events)
|
||||
if len(related_collectors) < 2:
|
||||
continue
|
||||
|
||||
path_signatures = Counter()
|
||||
max_path_length = 0
|
||||
for event in related_events:
|
||||
metadata = event.get("metadata") or {}
|
||||
signature = _path_signature(metadata)
|
||||
if signature:
|
||||
path_signatures[signature] += 1
|
||||
max_path_length = max(max_path_length, len(signature))
|
||||
|
||||
if len(path_signatures) < 2:
|
||||
continue
|
||||
|
||||
dominant_length = len(path_signatures.most_common(1)[0][0])
|
||||
if max_path_length < max(dominant_length + 2, 5):
|
||||
continue
|
||||
|
||||
sample_event = max(
|
||||
related_events,
|
||||
key=lambda event: len(_path_signature((event.get("metadata") or {}))),
|
||||
)
|
||||
sample_metadata = sample_event.get("metadata") or {}
|
||||
sample_enrichment = sample_metadata.get("enrichment") or {}
|
||||
sample_prefix_geography = sample_enrichment.get("prefix_geography") or {}
|
||||
peer_scope = related_collectors
|
||||
path_lengths = sorted({len(signature) for signature in path_signatures if signature})
|
||||
|
||||
anomalies.append(
|
||||
BGPAnomaly(
|
||||
snapshot_id=snapshot_id,
|
||||
task_id=task_id,
|
||||
source=source,
|
||||
anomaly_type="route_leak_candidate",
|
||||
severity="high" if max_path_length >= dominant_length + 3 else "medium",
|
||||
status="active",
|
||||
entity_key=f"route_leak_candidate:{prefix}:{max_path_length}:{len(related_collectors)}",
|
||||
prefix=prefix,
|
||||
origin_asn=sample_metadata.get("origin_asn"),
|
||||
new_origin_asn=None,
|
||||
peer_scope=peer_scope,
|
||||
started_at=datetime.now(UTC),
|
||||
confidence=min(0.58 + (0.05 * min(len(related_collectors), 4)) + (0.03 * min(max_path_length - dominant_length, 4)), 0.88),
|
||||
summary=(
|
||||
f"Prefix {prefix} shows divergent long AS paths across "
|
||||
f"{len(related_collectors)} collectors, suggesting a possible route leak."
|
||||
),
|
||||
evidence={
|
||||
"path_lengths": path_lengths,
|
||||
"dominant_path_length": dominant_length,
|
||||
"max_path_length": max_path_length,
|
||||
"path_signatures": [
|
||||
{"path": list(signature), "count": count}
|
||||
for signature, count in path_signatures.most_common(5)
|
||||
],
|
||||
"events": [(item.get("metadata") or {}) for item in related_events[:10]],
|
||||
"origin_asn_profile": sample_enrichment.get("origin_asn_profile"),
|
||||
"rpki_validation": sample_enrichment.get("rpki_validation"),
|
||||
"prefix_geography": sample_prefix_geography,
|
||||
"prefix_scope": sample_enrichment.get("prefix_scope"),
|
||||
"impacted_regions": sample_prefix_geography.get("regions")
|
||||
or _iter_event_regions(related_events)
|
||||
or sample_enrichment.get("prefix_scope", {}).get("regions", []),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return anomalies
|
||||
|
||||
|
||||
def detect_path_flap_anomalies(
|
||||
*,
|
||||
source: str,
|
||||
snapshot_id: int | None,
|
||||
task_id: int | None,
|
||||
events: list[dict[str, Any]],
|
||||
) -> list[BGPAnomaly]:
|
||||
events_by_prefix: defaultdict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
for event in events:
|
||||
metadata = event.get("metadata") or {}
|
||||
prefix = metadata.get("prefix")
|
||||
if prefix:
|
||||
events_by_prefix[str(prefix)].append(event)
|
||||
|
||||
anomalies: list[BGPAnomaly] = []
|
||||
for prefix, related_events in events_by_prefix.items():
|
||||
ordered = sorted(
|
||||
related_events,
|
||||
key=lambda event: str((event.get("metadata") or {}).get("timestamp") or ""),
|
||||
)
|
||||
event_types = [str((item.get("metadata") or {}).get("event_type") or "") for item in ordered]
|
||||
transitions = sum(1 for index in range(1, len(event_types)) if event_types[index] != event_types[index - 1])
|
||||
distinct_paths = {
|
||||
_path_signature(item.get("metadata") or {})
|
||||
for item in ordered
|
||||
if _path_signature(item.get("metadata") or {})
|
||||
}
|
||||
related_collectors = _unique_collectors(ordered)
|
||||
|
||||
if transitions < 3 and len(distinct_paths) < 3:
|
||||
continue
|
||||
|
||||
sample_metadata = (ordered[0].get("metadata") or {}) if ordered else {}
|
||||
sample_enrichment = sample_metadata.get("enrichment") or {}
|
||||
sample_prefix_geography = sample_enrichment.get("prefix_geography") or {}
|
||||
severity = "medium"
|
||||
if transitions >= 5 or len(distinct_paths) >= 4:
|
||||
severity = "high"
|
||||
|
||||
anomalies.append(
|
||||
BGPAnomaly(
|
||||
snapshot_id=snapshot_id,
|
||||
task_id=task_id,
|
||||
source=source,
|
||||
anomaly_type="path_flap",
|
||||
severity=severity,
|
||||
status="active",
|
||||
entity_key=f"path_flap:{prefix}:{transitions}:{len(distinct_paths)}",
|
||||
prefix=prefix,
|
||||
origin_asn=sample_metadata.get("origin_asn"),
|
||||
new_origin_asn=None,
|
||||
peer_scope=related_collectors,
|
||||
started_at=datetime.now(UTC),
|
||||
confidence=min(0.54 + (0.05 * min(transitions, 5)) + (0.03 * min(len(distinct_paths), 4)), 0.9),
|
||||
summary=(
|
||||
f"Prefix {prefix} shows repeated state/path changes "
|
||||
f"({transitions} transitions, {len(distinct_paths)} distinct paths) in the current window."
|
||||
),
|
||||
evidence={
|
||||
"transitions": transitions,
|
||||
"event_types": event_types[:12],
|
||||
"distinct_paths": [list(path) for path in list(distinct_paths)[:6]],
|
||||
"events": [(item.get("metadata") or {}) for item in ordered[:10]],
|
||||
"origin_asn_profile": sample_enrichment.get("origin_asn_profile"),
|
||||
"rpki_validation": sample_enrichment.get("rpki_validation"),
|
||||
"prefix_geography": sample_prefix_geography,
|
||||
"prefix_scope": sample_enrichment.get("prefix_scope"),
|
||||
"impacted_regions": sample_prefix_geography.get("regions")
|
||||
or _iter_event_regions(ordered)
|
||||
or sample_enrichment.get("prefix_scope", {}).get("regions", []),
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user