fix: stabilize earth bgp geography and rendering
This commit is contained in:
@@ -32,6 +32,7 @@ from app.services.collectors.spacetrack import SpaceTrackTLECollector
|
||||
from app.services.collectors.celestrak import CelesTrakTLECollector
|
||||
from app.services.collectors.ris_live import RISLiveCollector
|
||||
from app.services.collectors.bgpstream import BGPStreamBackfillCollector
|
||||
from app.services.collectors.iptoasn import IPtoASNPrefixGeoCollector
|
||||
|
||||
collector_registry.register(TOP500Collector())
|
||||
collector_registry.register(EpochAIGPUCollector())
|
||||
@@ -55,3 +56,4 @@ collector_registry.register(SpaceTrackTLECollector())
|
||||
collector_registry.register(CelesTrakTLECollector())
|
||||
collector_registry.register(RISLiveCollector())
|
||||
collector_registry.register(BGPStreamBackfillCollector())
|
||||
collector_registry.register(IPtoASNPrefixGeoCollector())
|
||||
|
||||
@@ -18,6 +18,8 @@ from app.services.bgp_detectors import (
|
||||
detect_mass_withdrawal_anomalies,
|
||||
detect_more_specific_burst_anomalies,
|
||||
detect_origin_change_anomalies,
|
||||
detect_path_flap_anomalies,
|
||||
detect_route_leak_anomalies,
|
||||
)
|
||||
from app.services.bgp_enrichment import enrich_bgp_events_for_batch, extract_bgp_network_fields
|
||||
|
||||
@@ -282,6 +284,18 @@ async def create_bgp_anomalies_for_batch(
|
||||
task_id=task_id,
|
||||
events=enriched_events,
|
||||
),
|
||||
*detect_route_leak_anomalies(
|
||||
source=source,
|
||||
snapshot_id=snapshot_id,
|
||||
task_id=task_id,
|
||||
events=enriched_events,
|
||||
),
|
||||
*detect_path_flap_anomalies(
|
||||
source=source,
|
||||
snapshot_id=snapshot_id,
|
||||
task_id=task_id,
|
||||
events=enriched_events,
|
||||
),
|
||||
]
|
||||
|
||||
if not pending_anomalies:
|
||||
@@ -302,16 +316,29 @@ async def create_bgp_anomalies_for_batch(
|
||||
|
||||
created = 0
|
||||
created_anomalies: list[BGPAnomaly] = []
|
||||
refreshed_anomalies: list[BGPAnomaly] = []
|
||||
existing_map = {item.entity_key: item for item in existing_anomalies if item.entity_key}
|
||||
for anomaly in pending_anomalies:
|
||||
if anomaly.entity_key in existing_keys:
|
||||
existing = existing_map.get(anomaly.entity_key)
|
||||
if existing is not None:
|
||||
existing.severity = anomaly.severity
|
||||
existing.status = anomaly.status
|
||||
existing.summary = anomaly.summary
|
||||
existing.confidence = anomaly.confidence
|
||||
existing.peer_scope = anomaly.peer_scope
|
||||
existing.evidence = anomaly.evidence
|
||||
existing.new_origin_asn = anomaly.new_origin_asn
|
||||
existing.origin_asn = anomaly.origin_asn
|
||||
refreshed_anomalies.append(existing)
|
||||
continue
|
||||
db.add(anomaly)
|
||||
created_anomalies.append(anomaly)
|
||||
created += 1
|
||||
|
||||
if created:
|
||||
if created or refreshed_anomalies:
|
||||
await db.commit()
|
||||
incident_seed_anomalies = [*created_anomalies, *existing_anomalies]
|
||||
incident_seed_anomalies = [*created_anomalies, *refreshed_anomalies]
|
||||
if incident_seed_anomalies:
|
||||
await create_bgp_incidents_for_anomalies(
|
||||
db,
|
||||
|
||||
111
backend/app/services/collectors/iptoasn.py
Normal file
111
backend/app/services/collectors/iptoasn.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""IPtoASN prefix geography collector.
|
||||
|
||||
Downloads the public combined IPv4+IPv6 TSV database and stores coarse
|
||||
prefix-to-country/ASN geography hints for BGP enrichment.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import gzip
|
||||
from datetime import UTC, datetime
|
||||
from ipaddress import summarize_address_range, ip_address
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from app.services.collectors.base import BaseCollector
|
||||
|
||||
|
||||
class IPtoASNPrefixGeoCollector(BaseCollector):
|
||||
name = "iptoasn_prefix_geo"
|
||||
priority = "P1"
|
||||
module = "L3"
|
||||
frequency_hours = 24
|
||||
data_type = "prefix_geography"
|
||||
fail_on_empty = True
|
||||
|
||||
async def fetch(self) -> list[dict[str, Any]]:
|
||||
if not self._resolved_url:
|
||||
raise RuntimeError("IPtoASN combined URL is not configured")
|
||||
|
||||
async with httpx.AsyncClient(timeout=180.0, follow_redirects=True) as client:
|
||||
response = await client.get(
|
||||
self._resolved_url,
|
||||
headers={
|
||||
"User-Agent": "Planet-Intelligence-System/1.0 (Python/collector)",
|
||||
"Accept": "application/gzip,application/octet-stream,*/*",
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
body = gzip.decompress(response.content).decode("utf-8", errors="replace")
|
||||
|
||||
rows: list[dict[str, Any]] = []
|
||||
for raw_line in body.splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
if len(parts) < 5:
|
||||
continue
|
||||
range_start, range_end, asn, country_code, as_name = parts[:5]
|
||||
rows.append(
|
||||
{
|
||||
"range_start": range_start,
|
||||
"range_end": range_end,
|
||||
"asn": asn,
|
||||
"country_code": country_code,
|
||||
"as_name": as_name,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
def transform(self, raw_data: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
reference_date = datetime.now(UTC).isoformat()
|
||||
transformed: list[dict[str, Any]] = []
|
||||
|
||||
for item in raw_data:
|
||||
try:
|
||||
start_ip = ip_address(str(item["range_start"]))
|
||||
end_ip = ip_address(str(item["range_end"]))
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if start_ip.version != end_ip.version:
|
||||
continue
|
||||
|
||||
summarized = list(summarize_address_range(start_ip, end_ip))
|
||||
primary_prefix = str(summarized[0]) if summarized else f"{start_ip}/{32 if start_ip.version == 4 else 128}"
|
||||
family = f"ipv{start_ip.version}"
|
||||
|
||||
asn_value = item.get("asn")
|
||||
try:
|
||||
normalized_asn = int(str(asn_value))
|
||||
except (TypeError, ValueError):
|
||||
normalized_asn = None
|
||||
|
||||
transformed.append(
|
||||
{
|
||||
"source_id": f"{family}:{item['range_start']}-{item['range_end']}",
|
||||
"name": primary_prefix,
|
||||
"title": f"{primary_prefix} {item.get('country_code', '').strip()}".strip(),
|
||||
"country": item.get("country_code"),
|
||||
"city": "",
|
||||
"latitude": None,
|
||||
"longitude": None,
|
||||
"metadata": {
|
||||
"family": family,
|
||||
"range_start": item["range_start"],
|
||||
"range_end": item["range_end"],
|
||||
"prefix": primary_prefix,
|
||||
"prefixes": [str(prefix) for prefix in summarized[:8]],
|
||||
"range_prefix_count": len(summarized),
|
||||
"country_code": item.get("country_code"),
|
||||
"asn": normalized_asn,
|
||||
"as_name": item.get("as_name"),
|
||||
"source_dataset": "iptoasn_combined",
|
||||
},
|
||||
"reference_date": reference_date,
|
||||
}
|
||||
)
|
||||
|
||||
return transformed
|
||||
Reference in New Issue
Block a user