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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 13 additions & 2 deletions backend/app/services/prefix_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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.")
Expand All @@ -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,
Expand All @@ -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,
Expand Down
106 changes: 106 additions & 0 deletions backend/app/services/registry_checker.py
Original file line number Diff line number Diff line change
@@ -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
13 changes: 11 additions & 2 deletions backend/app/templates/report.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,21 @@
<li><strong>Risiko:</strong> {{ report.checks.rpki.risk if report.checks and report.checks.rpki else '-' }}</li>
</ul>

<h3>Registry/IRR</h3>
<ul>
<li><strong>Status:</strong> {{ report.checks.registry.status if report.checks and report.checks.registry else '-' }}</li>
<li><strong>Summary:</strong> {{ report.checks.registry.summary if report.checks and report.checks.registry else '-' }}</li>
<li><strong>Erklärung:</strong> {{ report.checks.registry.explanation if report.checks and report.checks.registry else '-' }}</li>
<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 API Rohdaten anzeigen</summary>
<pre>{{ report.checks.rpki.raw | tojson(indent=2) if report.checks and report.checks.rpki else '{}' }}</pre>
<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>

Expand Down
8 changes: 7 additions & 1 deletion backend/app/templates/report.md.j2
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,20 @@
- Erklärung: {{ report.checks.rpki.explanation if report.checks and report.checks.rpki else '-' }}
- Risiko: {{ report.checks.rpki.risk if report.checks and report.checks.rpki else '-' }}

### Registry/IRR
- Status: **{{ report.checks.registry.status if report.checks and report.checks.registry else '-' }}**
- Summary: {{ report.checks.registry.summary if report.checks and report.checks.registry else '-' }}
- 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
```json
{{ report.details | tojson(indent=2) }}
```

## Rohdaten
```json
{{ report.checks.rpki.raw | tojson(indent=2) if report.checks and report.checks.rpki else '{}' }}
{{ (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) }}
```


Expand Down
42 changes: 42 additions & 0 deletions backend/tests/test_registry_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from app.services.registry_checker import RegistryChecker


def test_route_object_matching_origin_is_ok():
payload = {
"data": {
"records": [
[
{"key": "route", "value": "193.0.6.0/24"},
{"key": "origin", "value": "AS3333"},
]
]
}
}
result = RegistryChecker().check("193.0.6.0/24", "AS3333", payload)
assert result["status"] == "OK"


def test_no_route_object_is_warning():
payload = {"data": {"records": [[{"key": "descr", "value": "example"}]]}}
result = RegistryChecker().check("193.0.6.0/24", "AS3333", payload)
assert result["status"] == "WARNING"


def test_route_object_origin_mismatch_is_critical():
payload = {
"data": {
"records": [
[
{"key": "route", "value": "193.0.6.0/24"},
{"key": "origin", "value": "AS64500"},
]
]
}
}
result = RegistryChecker().check("193.0.6.0/24", "AS3333", payload)
assert result["status"] == "CRITICAL"


def test_empty_data_is_unknown():
result = RegistryChecker().check("193.0.6.0/24", "AS3333", {})
assert result["status"] == "UNKNOWN"
17 changes: 15 additions & 2 deletions frontend/src/components/ReportView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const order = { CRITICAL: 0, WARNING: 1, UNKNOWN: 2, OK: 3 }
export function ReportView({ report }: { report: CheckResponse }) {
const recs = report.recommendations ?? []
const rpki = report.checks?.rpki
const hasRpkiCheck = Boolean(rpki)
const registry = report.checks?.registry
const hasAnyChecks = Boolean(rpki || registry)
const rpkiExplanation = report.details?.rpki_explanation
const extractedPrefixes = Array.isArray(report.details?.extracted_prefixes) ? report.details.extracted_prefixes : []
const rpkiSummary = report.details?.rpki_summary
Expand All @@ -30,7 +31,8 @@ export function ReportView({ report }: { report: CheckResponse }) {
{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>}

<h4 className='font-semibold mt-4'>Einzelprüfungen</h4>
{hasRpkiCheck ? <div className='border rounded p-3 mt-2'>
{hasAnyChecks ? <div className='mt-2 space-y-3'>
{rpki && <div className='border rounded p-3'>
<div className='flex items-center gap-2'><span className='font-semibold'>RPKI</span><StatusBadge status={rpki?.status || 'UNKNOWN'} /></div>
<p className='mt-2'><strong>Summary:</strong> {rpki?.summary || '-'}</p>
<p className='mt-1'><strong>Erklärung:</strong> {rpki?.explanation || '-'}</p>
Expand All @@ -39,6 +41,17 @@ export function ReportView({ report }: { report: CheckResponse }) {
<summary>RPKI Rohdaten</summary>
<pre className='mt-2 text-xs overflow-auto'>{JSON.stringify(rpki?.raw ?? {}, null, 2)}</pre>
</details>
</div>}
{registry && <div className='border rounded p-3'>
<div className='flex items-center gap-2'><span className='font-semibold'>Registry/IRR</span><StatusBadge status={registry?.status || 'UNKNOWN'} /></div>
<p className='mt-2'><strong>Summary:</strong> {registry?.summary || '-'}</p>
<p className='mt-1'><strong>Erklärung:</strong> {registry?.explanation || '-'}</p>
<p className='mt-1'><strong>Risiko:</strong> {registry?.risk || '-'}</p>
<details className='mt-2'>
<summary>Registry/IRR Rohdaten</summary>
<pre className='mt-2 text-xs overflow-auto'>{JSON.stringify(registry?.raw ?? {}, null, 2)}</pre>
</details>
</div>}
</div> : <p className='mt-2'>Für diesen Check-Typ sind keine Einzelprüfungen verfügbar.</p>}

{rpkiSummary && <div className='mt-4 border rounded p-3'><h4 className='font-semibold'>ASN-RPKI Summary</h4>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export type CheckResponse = {
risk?: string
recommendations: string[]
input?: { prefix?: string; origin_as?: string | null; asn?: string; limit?: number }
checks?: { rpki?: CheckSection } | null
checks?: { rpki?: CheckSection; registry?: CheckSection } | null
details?: {
rpki_explanation?: string
extracted_prefixes?: string[]
Expand Down
Loading