diff --git a/README.md b/README.md index 7c74879..11f70ac 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,12 @@ Der Prefix Check enthält zusätzlich einen read-only Registry/IRR-Plausibilitä 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. + +## Kombinierte Prefix-Bewertung + +RouteForge bewertet Prefix-Checks nicht nur anhand einer einzelnen Quelle. RPKI und Registry/IRR werden getrennt angezeigt, aber zusätzlich zu einer Gesamtbewertung zusammengeführt. +Ein `CRITICAL` aus einer Einzelprüfung bleibt `CRITICAL` in der Gesamtbewertung. `WARNING` weist auf unvollständige oder unsichere Datenlage hin. + ## 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/core/prefix_evaluation.py b/backend/app/core/prefix_evaluation.py new file mode 100644 index 0000000..db14cbd --- /dev/null +++ b/backend/app/core/prefix_evaluation.py @@ -0,0 +1,125 @@ +from app.core.status import CheckStatus + + +def evaluate_prefix_overall( + rpki_check: dict, + registry_check: dict, + prefix: str, + origin_as: str | None, +) -> dict: + _ = prefix + rpki_status = _normalize_status(rpki_check.get("status")) + registry_status = _normalize_status(registry_check.get("status")) + + if not origin_as: + return { + "status": CheckStatus.WARNING.value, + "summary": "Keine vollständige Prefix-Origin-Prüfung möglich", + "explanation": "Ohne Origin-AS ist keine vollständige kombinierte Bewertung von RPKI und Registry-Origin möglich.", + "risk": "Die Gesamtaussage bleibt eingeschränkt, weil die Origin-Autorisierung nicht vollständig geprüft werden kann.", + "recommendations": [ + "Origin-AS ergänzen, um die kombinierte Prefix-Bewertung zu vervollständigen.", + "RPKI- und Registry/IRR-Hinweise danach erneut gemeinsam prüfen.", + ], + } + + if CheckStatus.CRITICAL in {rpki_status, registry_status}: + if registry_status == CheckStatus.CRITICAL: + return { + "status": CheckStatus.CRITICAL.value, + "summary": "Registry/IRR-Origin widerspricht dem angegebenen Origin-AS.", + "explanation": "Ein gefundenes route/route6-Origin weicht vom geprüften Origin-AS ab.", + "risk": "Hohe Wahrscheinlichkeit für Konfigurations- oder Dokumentationsfehler mit Hijack-/Erreichbarkeitsrisiko.", + "recommendations": [ + "Origin-AS und route/route6-Objekte in der zuständigen Registry sofort abgleichen.", + "Fehlerhafte Registry-Einträge korrigieren.", + ], + } + return { + "status": CheckStatus.CRITICAL.value, + "summary": "RPKI meldet ein kritisches Problem trotz plausibler Registry-Daten.", + "explanation": "Registry/IRR-Daten wirken plausibel, aber RPKI würde diese Route als problematisch bewerten. Validierende Netze können die Route verwerfen.", + "risk": "Akute Erreichbarkeitsrisiken durch mögliche Routenverwerfung in ROV-validierenden Netzen.", + "recommendations": [ + "Origin-AS und ROA-Zuordnung priorisiert prüfen.", + "RPKI-Inkonsistenz beheben und Prüfung erneut durchführen.", + ], + } + + if rpki_status == CheckStatus.UNKNOWN and registry_status == CheckStatus.UNKNOWN: + return { + "status": CheckStatus.UNKNOWN.value, + "summary": "Keine belastbare Gesamtbewertung möglich.", + "explanation": "Sowohl RPKI als auch Registry/IRR liefern keine verlässliche Aussage.", + "risk": "Die Datenlage ist unzureichend für eine belastbare Routing-Sicherheitsbewertung.", + "recommendations": [ + "Prüfung später wiederholen.", + "Rohdaten und Quellfehler in beiden Checks kontrollieren.", + ], + } + + if rpki_status == CheckStatus.OK and registry_status == CheckStatus.OK: + return { + "status": CheckStatus.OK.value, + "summary": "Prefix-Origin-Paar wirkt plausibel und RPKI-valid.", + "explanation": "RPKI bestätigt die Origin-Autorisierung und Registry/IRR-Daten enthalten passende Hinweise.", + "risk": "Derzeit kein akutes Risiko aus RPKI- oder Registry-Sicht erkennbar.", + "recommendations": ["Weiterhin regelmäßig überwachen und Dokumentation aktuell halten."], + } + + if rpki_status == CheckStatus.OK and registry_status == CheckStatus.WARNING: + return { + "status": CheckStatus.WARNING.value, + "summary": "RPKI ist gültig, Registry/IRR-Dokumentation ist unvollständig.", + "explanation": "Das Prefix-Origin-Paar ist RPKI-valid, aber in den Registry-/IRR-Daten wurde kein klares passendes route/route6-Objekt gefunden.", + "risk": "Technisch aktuell stabil möglich, aber mit Dokumentations- und Nachvollziehbarkeitslücke.", + "recommendations": [ + "Registry-/IRR-Daten auf vollständige route/route6-Dokumentation prüfen.", + ], + } + + if rpki_status == CheckStatus.WARNING and registry_status == CheckStatus.WARNING: + return { + "status": CheckStatus.WARNING.value, + "summary": "Routing-Sicherheitslage ist unvollständig dokumentiert.", + "explanation": "Weder RPKI noch Registry/IRR liefern eine vollständig belastbare Bestätigung.", + "risk": "Erhöhtes operatives Risiko durch fehlende oder unvollständige Absicherung.", + "recommendations": ["RPKI- und Registry-Daten gemeinsam vervollständigen und erneut prüfen."], + } + + # Konservativ: Teilweise positive Ergebnisse mit UNKNOWN bleiben WARNING. + if (rpki_status, registry_status) in { + (CheckStatus.OK, CheckStatus.UNKNOWN), + (CheckStatus.UNKNOWN, CheckStatus.OK), + }: + return { + "status": CheckStatus.WARNING.value, + "summary": "Teilweise bestätigte Datenlage mit Unsicherheit.", + "explanation": "Eine Quelle bestätigt die Plausibilität, die andere bleibt unklar. Die Gesamtbewertung ist daher konservativ WARNING.", + "risk": "Verbleibende Unsicherheit kann zu Fehlannahmen in der Routing-Bewertung führen.", + "recommendations": [ + "Unklare Quelle gezielt nachprüfen und Datenlage vervollständigen.", + ], + } + + if CheckStatus.WARNING in {rpki_status, registry_status}: + return { + "status": CheckStatus.WARNING.value, + "summary": "Kombinierte Prefix-Bewertung zeigt Warnhinweise.", + "explanation": "Mindestens eine Einzelprüfung meldet eine unvollständige oder unsichere Datenlage.", + "risk": "Routing-Sicherheitsbewertung ist nicht vollständig belastbar.", + "recommendations": ["Hinweise aus RPKI und Registry/IRR gezielt nacharbeiten."], + } + + return { + "status": CheckStatus.UNKNOWN.value, + "summary": "Kombinierte Prefix-Bewertung nicht eindeutig bestimmbar.", + "explanation": "Die vorliegenden Einzelergebnisse konnten nicht konsistent kombiniert werden.", + "risk": "Die Gesamtaussage bleibt unklar.", + "recommendations": ["Einzelprüfungen und Rohdaten manuell verifizieren."], + } + + +def _normalize_status(value: str | None) -> CheckStatus: + mapping = {s.value: s for s in CheckStatus} + return mapping.get((value or "UNKNOWN").upper(), CheckStatus.UNKNOWN) diff --git a/backend/app/services/prefix_checker.py b/backend/app/services/prefix_checker.py index dfd6acf..d7bafb4 100644 --- a/backend/app/services/prefix_checker.py +++ b/backend/app/services/prefix_checker.py @@ -1,5 +1,5 @@ from app.core.normalize import format_asn, normalize_asn, validate_prefix -from app.core.recommendations import evaluate_rpki_status +from app.core.prefix_evaluation import evaluate_prefix_overall from app.core.status import CheckStatus from app.config import settings from app.services.registry_checker import RegistryChecker @@ -30,9 +30,7 @@ def check(self, prefix: str, origin_as: str | None) -> dict: if rpki_check.get("status") == CheckStatus.UNKNOWN.value and rpki_check.get("raw", {}).get("error"): warnings.append("RPKI-Quelle nicht erreichbar oder unvollständig.") - overall = evaluate_rpki_status(rpki_check.get("raw_status"), normalized_prefix, normalized_origin) - if rpki_check.get("status") == CheckStatus.UNKNOWN.value and rpki_check.get("explanation"): - overall["explanation"] = rpki_check["explanation"] + overall = evaluate_prefix_overall(rpki_check, registry_check, normalized_prefix, normalized_origin) return { "status": overall["status"], diff --git a/backend/app/templates/report.html.j2 b/backend/app/templates/report.html.j2 index 2e29c91..beb526a 100644 --- a/backend/app/templates/report.html.j2 +++ b/backend/app/templates/report.html.j2 @@ -9,12 +9,13 @@
  • Origin-AS: {{ report.input.origin_as if report.input and report.input.origin_as else '-' }}
  • -

    Gesamtstatus

    -

    {{ report.status }}

    - -

    Kurzfassung

    {{ report.summary }}

    -

    Erklärung

    {{ report.explanation or '-' }}

    -

    Risiko

    {{ report.risk or '-' }}

    +

    Gesamtbewertung

    +

    Empfehlungen

    @@ -35,15 +36,14 @@
  • Risiko: {{ report.checks.registry.risk if report.checks and report.checks.registry else '-' }}
  • -

    Technische Details

    {{ report.details | tojson(indent=2) }}
    -

    Rohdaten

    -
    - RPKI/Registry API Rohdaten anzeigen -
    {"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) }}}
    -
    - +

    RPKI Rohdaten

    +
    {{ (report.checks.rpki.raw if report.checks and report.checks.rpki else {}) | tojson(indent=2) }}
    +

    Registry/IRR Rohdaten

    +
    {{ (report.checks.registry.raw if report.checks and report.checks.registry else {}) | tojson(indent=2) }}
    + +

    Technische Details

    {{ report.details | tojson(indent=2) }}
    {% if report.details and report.details.rpki_summary %}

    RPKI Batch Summary

    {{ report.details.rpki_summary | tojson(indent=2) }}
    {% endif %} {% if report.details and report.details.results %}

    RPKI Batch Ergebnisse

    {{ report.details.results | tojson(indent=2) }}
    {% endif %} + diff --git a/backend/app/templates/report.md.j2 b/backend/app/templates/report.md.j2 index a782df7..193f0b5 100644 --- a/backend/app/templates/report.md.j2 +++ b/backend/app/templates/report.md.j2 @@ -7,17 +7,11 @@ - Prefix: {{ report.input.prefix if report.input else '-' }} - Origin-AS: {{ report.input.origin_as if report.input and report.input.origin_as else '-' }} -## Gesamtstatus -**{{ report.status }}** - -## Kurzfassung -{{ report.summary }} - -## Erklärung -{{ report.explanation or '-' }} - -## Risiko -{{ report.risk or '-' }} +## Gesamtbewertung +- Status: **{{ report.status }}** +- Kurzfassung: {{ report.summary }} +- Erklärung: {{ report.explanation or '-' }} +- Risiko: {{ report.risk or '-' }} ## Empfehlungen {% for item in report.recommendations %}- {{ item }} @@ -36,16 +30,21 @@ - Erklärung: {{ report.checks.registry.explanation if report.checks and report.checks.registry else '-' }} - Risiko: {{ report.checks.registry.risk if report.checks and report.checks.registry else '-' }} -## Technische Details +## Rohdaten +### RPKI Rohdaten ```json -{{ report.details | tojson(indent=2) }} +{{ (report.checks.rpki.raw if report.checks and report.checks.rpki else {}) | tojson(indent=2) }} ``` -## Rohdaten +### Registry/IRR Rohdaten ```json -{{ (report.checks.rpki.raw if report.checks and report.checks.rpki else {}) | tojson(indent=2) }}\n\nRegistry/IRR:\n{{ (report.checks.registry.raw if report.checks and report.checks.registry else {}) | tojson(indent=2) }} +{{ (report.checks.registry.raw if report.checks and report.checks.registry else {}) | tojson(indent=2) }} ``` +## Technische Details +```json +{{ report.details | tojson(indent=2) }} +``` {% if report.details and report.details.rpki_summary %} ## RPKI Batch Summary diff --git a/backend/tests/test_prefix_overall_evaluation.py b/backend/tests/test_prefix_overall_evaluation.py new file mode 100644 index 0000000..00a46ed --- /dev/null +++ b/backend/tests/test_prefix_overall_evaluation.py @@ -0,0 +1,45 @@ +from app.core.prefix_evaluation import evaluate_prefix_overall + + +def _check(status: str) -> dict: + return {"status": status} + + +def test_rpki_ok_registry_ok_is_ok(): + result = evaluate_prefix_overall(_check("OK"), _check("OK"), "193.0.6.0/24", "AS3333") + assert result["status"] == "OK" + + +def test_rpki_ok_registry_warning_is_warning(): + result = evaluate_prefix_overall(_check("OK"), _check("WARNING"), "193.0.6.0/24", "AS3333") + assert result["status"] == "WARNING" + + +def test_rpki_critical_registry_ok_is_critical(): + result = evaluate_prefix_overall(_check("CRITICAL"), _check("OK"), "193.0.6.0/24", "AS3333") + assert result["status"] == "CRITICAL" + + +def test_rpki_warning_registry_warning_is_warning(): + result = evaluate_prefix_overall(_check("WARNING"), _check("WARNING"), "193.0.6.0/24", "AS3333") + assert result["status"] == "WARNING" + + +def test_rpki_unknown_registry_ok_is_warning(): + result = evaluate_prefix_overall(_check("UNKNOWN"), _check("OK"), "193.0.6.0/24", "AS3333") + assert result["status"] == "WARNING" + + +def test_rpki_unknown_registry_unknown_is_unknown(): + result = evaluate_prefix_overall(_check("UNKNOWN"), _check("UNKNOWN"), "193.0.6.0/24", "AS3333") + assert result["status"] == "UNKNOWN" + + +def test_registry_critical_rpki_ok_is_critical(): + result = evaluate_prefix_overall(_check("OK"), _check("CRITICAL"), "193.0.6.0/24", "AS3333") + assert result["status"] == "CRITICAL" + + +def test_missing_origin_as_is_warning(): + result = evaluate_prefix_overall(_check("OK"), _check("OK"), "193.0.6.0/24", None) + assert result["status"] == "WARNING" diff --git a/frontend/src/components/ReportView.tsx b/frontend/src/components/ReportView.tsx index ae6c02f..19c20ee 100644 --- a/frontend/src/components/ReportView.tsx +++ b/frontend/src/components/ReportView.tsx @@ -30,6 +30,9 @@ export function ReportView({ report }: { report: CheckResponse }) {

    Empfehlungen

    {recs.length > 0 ? :

    Keine Empfehlungen verfügbar.

    } + {hasAnyChecks &&

    Gesamtbewertung

    } + {hasAnyChecks &&

    Oben siehst du den kombinierten Status aus RPKI und Registry/IRR.

    } +

    Einzelprüfungen

    {hasAnyChecks ?
    {rpki &&