Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
125 changes: 125 additions & 0 deletions backend/app/core/prefix_evaluation.py
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 2 additions & 4 deletions backend/app/services/prefix_checker.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"],
Expand Down
28 changes: 14 additions & 14 deletions backend/app/templates/report.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
<li><strong>Origin-AS:</strong> {{ report.input.origin_as if report.input and report.input.origin_as else '-' }}</li>
</ul>

<h2>Gesamtstatus</h2>
<p><strong>{{ report.status }}</strong></p>

<h2>Kurzfassung</h2><p>{{ report.summary }}</p>
<h2>Erklärung</h2><p>{{ report.explanation or '-' }}</p>
<h2>Risiko</h2><p>{{ report.risk or '-' }}</p>
<h2>Gesamtbewertung</h2>
<ul>
<li><strong>Status:</strong> {{ report.status }}</li>
<li><strong>Kurzfassung:</strong> {{ report.summary }}</li>
<li><strong>Erklärung:</strong> {{ report.explanation or '-' }}</li>
<li><strong>Risiko:</strong> {{ report.risk or '-' }}</li>
</ul>

<h2>Empfehlungen</h2><ul>{% for item in report.recommendations %}<li>{{ item }}</li>{% endfor %}</ul>

Expand All @@ -35,15 +36,14 @@
<li><strong>Risiko:</strong> {{ report.checks.registry.risk if report.checks and report.checks.registry else '-' }}</li>
</ul>

<h2>Technische Details</h2><pre>{{ report.details | tojson(indent=2) }}</pre>

<h2>Rohdaten</h2>
<details>
<summary>RPKI/Registry API Rohdaten anzeigen</summary>
<pre>{"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) }}}</pre>
</details>
</body></html>
<h3>RPKI Rohdaten</h3>
<pre>{{ (report.checks.rpki.raw if report.checks and report.checks.rpki else {}) | tojson(indent=2) }}</pre>
<h3>Registry/IRR Rohdaten</h3>
<pre>{{ (report.checks.registry.raw if report.checks and report.checks.registry else {}) | tojson(indent=2) }}</pre>

<h2>Technische Details</h2><pre>{{ report.details | tojson(indent=2) }}</pre>

{% if report.details and report.details.rpki_summary %}<h2>RPKI Batch Summary</h2><pre>{{ report.details.rpki_summary | tojson(indent=2) }}</pre>{% endif %}
{% if report.details and report.details.results %}<h2>RPKI Batch Ergebnisse</h2><pre>{{ report.details.results | tojson(indent=2) }}</pre>{% endif %}
</body></html>
29 changes: 14 additions & 15 deletions backend/app/templates/report.md.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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
Expand Down
45 changes: 45 additions & 0 deletions backend/tests/test_prefix_overall_evaluation.py
Original file line number Diff line number Diff line change
@@ -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"
3 changes: 3 additions & 0 deletions frontend/src/components/ReportView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export function ReportView({ report }: { report: CheckResponse }) {
<h4 className='font-semibold mt-4'>Empfehlungen</h4>
{recs.length > 0 ? <ul className='list-disc pl-5'>{recs.map((r, i) => <li key={`${r}-${i}`}>{r}</li>)}</ul> : <p>Keine Empfehlungen verfügbar.</p>}

{hasAnyChecks && <h4 className='font-semibold mt-4'>Gesamtbewertung</h4>}
{hasAnyChecks && <p className='mt-1 text-sm text-gray-700'>Oben siehst du den kombinierten Status aus RPKI und Registry/IRR.</p>}

<h4 className='font-semibold mt-4'>Einzelprüfungen</h4>
{hasAnyChecks ? <div className='mt-2 space-y-3'>
{rpki && <div className='border rounded p-3'>
Expand Down
Loading