diff --git a/README.md b/README.md index 00d146c..7c74879 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,13 @@ curl -X POST http://localhost:8000/api/check/asn-rpki \ - **CRITICAL:** Es wurde ein kritisches RPKI-Problem gefunden, zum Beispiel `invalid_asn` oder `invalid_length`. - **UNKNOWN:** Die Bewertung konnte nicht zuverlässig durchgeführt werden, zum Beispiel wegen fehlender Daten oder API-Fehlern. + +## Registry/IRR Check + +Der Prefix Check enthält zusätzlich einen read-only Registry/IRR-Plausibilitätscheck auf Basis verfügbarer RIPEstat-/Whois-/Registry-Daten. +Der Check bewertet Hinweise auf route/route6-Objekte und eine mögliche Origin-AS-Plausibilität, liefert bei unklarer Datenlage bewusst `UNKNOWN` oder `WARNING` und zeigt Rohdaten zur Nachvollziehbarkeit an. +Er ersetzt keine manuelle Registry-Prüfung. + ## Demo-Modus `ROUTEFORGE_DEMO_MODE=true` nutzt feste Beispieldaten und ist für Präsentationen, Tests und Offline-Demos gedacht. diff --git a/backend/app/services/prefix_checker.py b/backend/app/services/prefix_checker.py index 050a3a9..dfd6acf 100644 --- a/backend/app/services/prefix_checker.py +++ b/backend/app/services/prefix_checker.py @@ -2,6 +2,7 @@ from app.core.recommendations import evaluate_rpki_status from app.core.status import CheckStatus from app.config import settings +from app.services.registry_checker import RegistryChecker from app.services.ripe_db_client import RipeDbClient from app.services.ripe_stat_client import RipeStatClient from app.services.rpki_checker import RpkiChecker @@ -12,6 +13,7 @@ def __init__(self, client: RipeStatClient): self.client = client self.ripe_db = RipeDbClient(client) self.rpki = RpkiChecker(client) + self.registry = RegistryChecker() def check(self, prefix: str, origin_as: str | None) -> dict: normalized_prefix = validate_prefix(prefix) @@ -20,8 +22,8 @@ def check(self, prefix: str, origin_as: str | None) -> dict: whois = self.ripe_db.whois(normalized_prefix) routing_status = self.client.get("routing-status", {"resource": normalized_prefix}) rpki_check = self.rpki.check(normalized_prefix, normalized_origin) + registry_check = self.registry.check(normalized_prefix, normalized_origin, whois) - status = CheckStatus(rpki_check["status"]) warnings: list[str] = [] if whois.get("error") or routing_status.get("error"): warnings.append("Mindestens eine zusätzliche Datenquelle war nicht erreichbar.") @@ -47,7 +49,15 @@ def check(self, prefix: str, origin_as: str | None) -> dict: "risk": rpki_check["risk"], "recommendations": rpki_check["recommendations"], "raw": rpki_check.get("raw", {}), - } + }, + "registry": { + "status": registry_check["status"], + "summary": registry_check["summary"], + "explanation": registry_check["explanation"], + "risk": registry_check["risk"], + "recommendations": registry_check["recommendations"], + "raw": registry_check.get("raw", {}), + }, }, "details": { "whois": whois, @@ -56,6 +66,7 @@ def check(self, prefix: str, origin_as: str | None) -> dict: "whois": whois.get("error"), "routing_status": routing_status.get("error"), "rpki": rpki_check.get("raw", {}).get("error") if isinstance(rpki_check.get("raw"), dict) else None, + "registry": registry_check.get("raw", {}).get("error") if isinstance(registry_check.get("raw"), dict) else None, }, "warnings": warnings, "demo_mode": settings.demo_mode, diff --git a/backend/app/services/registry_checker.py b/backend/app/services/registry_checker.py new file mode 100644 index 0000000..bbe5bee --- /dev/null +++ b/backend/app/services/registry_checker.py @@ -0,0 +1,106 @@ +from app.core.status import CheckStatus + + +class RegistryChecker: + def check(self, prefix: str, origin_as: str | None, whois_payload: dict) -> dict: + if not isinstance(whois_payload, dict) or whois_payload.get("error"): + return { + "status": CheckStatus.UNKNOWN.value, + "summary": "Registry-/IRR-Daten konnten nicht bestimmt werden", + "explanation": "Die Whois-/Registry-Datenquelle war nicht erreichbar oder lieferte einen Fehler.", + "risk": "Die Bewertung ist unvollständig.", + "recommendations": [ + "Prüfe die Rohdaten der Registry-Quelle.", + "Wiederhole die Abfrage später.", + "Vergleiche das Ergebnis mit einer zweiten Registry-/IRR-Quelle.", + ], + "raw": whois_payload if isinstance(whois_payload, dict) else {}, + } + + route_origins = self._extract_route_origins(whois_payload) + has_registry_data = bool(whois_payload.get("data") or route_origins) + + if not has_registry_data: + return { + "status": CheckStatus.UNKNOWN.value, + "summary": "Keine verwertbaren Registry-/IRR-Daten gefunden", + "explanation": "Die Quelle lieferte keine eindeutig parsebaren Daten zum Prefix.", + "risk": "Es kann keine belastbare Plausibilitätsaussage getroffen werden.", + "recommendations": [ + "Prüfe das Prefix manuell in der zuständigen Registry.", + "Vergleiche die Daten mit einer alternativen Whois-/IRR-Quelle.", + ], + "raw": whois_payload, + } + + if not route_origins: + return { + "status": CheckStatus.WARNING.value, + "summary": "Registry-Daten vorhanden, aber kein route/route6-Hinweis gefunden", + "explanation": "Es wurden allgemeine Whois-/Registry-Daten gefunden, jedoch kein klares route/route6-Objekt.", + "risk": "Ohne route/route6-Hinweis bleibt die Origin-Plausibilität eingeschränkt.", + "recommendations": [ + "Prüfe, ob ein passendes route/route6-Objekt in der IRR gepflegt ist.", + "Validiere die Origin-Zuordnung zusätzlich manuell.", + ], + "raw": whois_payload, + } + + if not origin_as: + return { + "status": CheckStatus.OK.value, + "summary": "Route/route6-Hinweise gefunden", + "explanation": "Es wurden route/route6-Objekte bzw. Origin-Hinweise zum Prefix gefunden. Ohne angegebenes Origin-AS erfolgt keine AS-Konsistenzprüfung.", + "risk": "Grundsätzliche Registry-Plausibilität ist gegeben, AS-Abgleich ist offen.", + "recommendations": [ + "Für eine strengere Prüfung optional ein Origin-AS mitgeben.", + ], + "raw": whois_payload, + } + + normalized_origin = origin_as.upper() + if normalized_origin in route_origins: + return { + "status": CheckStatus.OK.value, + "summary": "Plausibles route/route6-Origin gefunden", + "explanation": f"Mindestens ein route/route6-Hinweis enthält das erwartete Origin {normalized_origin}.", + "risk": "Keine offensichtliche Registry-Inkonsistenz erkannt.", + "recommendations": [ + "Registry-Daten regelmäßig aktuell halten.", + ], + "raw": whois_payload, + } + + return { + "status": CheckStatus.CRITICAL.value, + "summary": "Route/route6-Origin widerspricht dem angegebenen Origin-AS", + "explanation": f"Gefundene Origins: {', '.join(sorted(route_origins))}. Erwartet wurde {normalized_origin}.", + "risk": "Möglicher Konfigurations- oder Registry-Fehler mit Hijack-Risiko.", + "recommendations": [ + "Origin-AS und route/route6-Objekte in der zuständigen Registry abgleichen.", + "Fehlerhafte Registry-Einträge korrigieren.", + ], + "raw": whois_payload, + } + + def _extract_route_origins(self, payload: dict) -> set[str]: + objects = payload.get("data", {}).get("irr_records") or payload.get("data", {}).get("records") or [] + origins: set[str] = set() + + for obj in objects: + route_seen = False + current_origin: str | None = None + + for field in obj if isinstance(obj, list) else []: + key = str(field.get("key", "")).lower() + value = str(field.get("value", "")).strip().upper() + + if key in {"route", "route6"} and value: + route_seen = True + if key == "origin" and value.startswith("AS"): + current_origin = value + + if route_seen and current_origin: + origins.add(current_origin) + + return origins diff --git a/backend/app/templates/report.html.j2 b/backend/app/templates/report.html.j2 index 839a843..2e29c91 100644 --- a/backend/app/templates/report.html.j2 +++ b/backend/app/templates/report.html.j2 @@ -27,12 +27,21 @@
{{ report.details | tojson(indent=2) }}
{{ report.checks.rpki.raw | tojson(indent=2) if report.checks and report.checks.rpki else '{}' }}
+ {"rpki": {{ (report.checks.rpki.raw if report.checks and report.checks.rpki else {}) | tojson(indent=2) }},
+"registry": {{ (report.checks.registry.raw if report.checks and report.checks.registry else {}) | tojson(indent=2) }}}