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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
<img src="frontend/public/routeforge.png" alt="RouteForge Logo" width="420">
</p>
<p align="center">
<img src="https://img.shields.io/badge/version-v0.9.2--rc-blue" alt="Version">
<img src="https://img.shields.io/badge/version-v1.0.0-blue" alt="Version">
<img src="https://img.shields.io/badge/license-AGPL--3.0--or--later-orange" alt="License">
<img src="https://img.shields.io/badge/status-beta-yellow" alt="Status">
<img src="https://img.shields.io/badge/status-stable-brightgreen" alt="Status">
<img src="https://img.shields.io/badge/selfhosted-ready-success" alt="Selfhosted">
<img src="https://img.shields.io/badge/read--only-routing%20safe-informational" alt="Read-only">
</p>
Expand Down Expand Up @@ -41,9 +41,9 @@ It combines these checks into an explainable, read-only preflight workflow.

Routing changes often require fast but traceable checks across multiple external data views. RouteForge provides a single UI/API workflow so teams can run consistent preflight checks, share results, and keep a documented decision trail.

## Current Alpha Status
## Current Release

RouteForge is a **functional beta** release with production-like workflows for read-only validation and demo usage. Current release target: **v0.9.2-rc**.
RouteForge is a **stable selfhosted** release for production-grade read-only validation workflows. Current release: **v1.0.0**.

## Quickstart with Docker Compose

Expand Down Expand Up @@ -174,7 +174,7 @@ RouteForge is read-only by design:

## Known Limitations

- Alpha software, interfaces may evolve.
- Stable release with ongoing minor UX/documentation improvements.
- RIPEstat payloads can vary over time.
- No local RPKI validator yet.
- No full BGP monitoring replacement.
Expand Down
23 changes: 21 additions & 2 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
## v1.0.0 (2026-05-22)

### Summary
- Stable selfhosted RouteForge release.
- Read-only routing safety model kept as a strict product baseline.
- User management and role-based access control.
- Audit Log.
- Change Cases.
- ASN Check, Prefix Check, and Preflight checks.
- BGP Visibility.
- ROA Planner / ROA Preflight.
- Watch Mode and scheduled rechecks.
- Reports and exports.
- System status, security warnings, and upgrade documentation.
- English-only UI and output polish.

### Upgrade Notes
- Upgrade backend/frontend to `1.0.0` and API-visible version to `v1.0.0`.
- Keep Alembic migration discipline (`alembic current`, `alembic heads`, `alembic upgrade head`) before production use.
- No new database migration was introduced specifically for this stable cut.

## v0.9.2-rc (2026-05-21)

- UI Cleanup & English-only polish
Expand Down Expand Up @@ -91,8 +112,6 @@ BGP visibility checks remain read-only and do not modify RIPE DB, RPKI objects,
- Added audit events for Change Case and attachment operations.
- Security: Change Cases are local workflow metadata only; no writes to RIPE DB, RPKI, or routers are performed.

# Release Notes

## v0.8.1-beta

**Stabilization, UX Polish & Upgrade Safety**
Expand Down
43 changes: 23 additions & 20 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
# RouteForge Roadmap

## Current Status
v0.9.2-rc, BGP Visibility Details completed, read-only
RouteForge is now **v1.0.0 stable**: a selfhosted, read-only routing preflight release for production-oriented validation workflows.

## v0.9.2-rc
- projects/change cases
- grouped preflight reports
## v1.0.0 Completed Scope
The stable release includes:
- User Management
- Role-based access control
- Session authentication
- Audit Log
- Change Cases
- ASN Check
- Prefix Check
- Preflight
- BGP Visibility
- ROA Planner / ROA Preflight
- Watch Mode / Scheduled Rechecks
- Reports and exports
- System Status
- Security and upgrade documentation
- English-only UI and output cleanup
- Read-only safety model

## v0.9.2-rc
- bgp visibility details
## Historical Release Context
Recent release-candidate work (v0.9.x-rc) focused on stabilizing BGP visibility details, ROA preflight workflows, watch scheduling/rechecks, deployment QA, and upgrade discipline that culminated in v1.0.0.

## v0.9.2-rc
- roa planner / roa preflight

## v0.9.2-rc
- watch mode / scheduled rechecks

## v0.9.2-rc
- security review
- UX review
- API stability
- upgrade tests

## v1.0.0
- stable selfhosted RouteForge release
## Future Roadmap
Post-v1.0.0 work will focus on operational maturity, documentation quality, and maintainability without changing the core read-only safety model.
2 changes: 1 addition & 1 deletion backend/app/api/routes_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
def system_info():
return {
'name': 'RouteForge',
'version': 'v0.9.2-rc',
'version': 'v1.0.0',
'demo_mode': settings.demo_mode,
'read_only': True,
'data_sources': ['RIPEstat', 'RIPEstat Whois/Registry'],
Expand Down
34 changes: 17 additions & 17 deletions backend/app/core/prefix_evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,42 +20,42 @@ def evaluate_prefix_overall(

if CheckStatus.CRITICAL in statuses.values():
if statuses["routing"] == CheckStatus.CRITICAL:
return _critical("RPKI und Registry wirken plausibel, aber die sichtbare Routing-Origin weicht ab.", "Das Prefix ist sichtbar, aber nicht mit dem erwarteten Origin-AS.")
return _critical("RPKI and registry look plausible, but visible routing origin differs.", "The prefix is visible, but not with the expected origin AS.")
if statuses["rpki"] == CheckStatus.CRITICAL:
return _critical("Das Prefix ist zwar sichtbar, aber RPKI meldet ein kritisches Problem.", "RPKI bewertet das Prefix-Origin-Paar kritisch, obwohl andere Checks ggf. positive Hinweise liefern.")
return _critical("Registry/IRR-Origin widerspricht dem angegebenen Origin-AS.", "Ein gefundenes route/route6-Origin weicht vom geprüften Origin-AS ab.")
return _critical("Registry/IRR origin conflicts with the provided origin AS.", "A discovered route/route6 origin differs from the checked origin AS.")

if statuses["rpki"] == statuses["registry"] == statuses["routing"] == CheckStatus.UNKNOWN:
return _unknown("No reliable overall assessment possible.", "RPKI, Registry/IRR und Routing Visibility liefern keine verlässliche Aussage.")
return _unknown("No reliable overall assessment possible.", "RPKI, Registry/IRR, and routing visibility do not provide a reliable assessment.")

if statuses["rpki"] == CheckStatus.OK and statuses["registry"] == CheckStatus.OK and statuses["routing"] == CheckStatus.OK:
return {
"status": CheckStatus.OK.value,
"summary": "Prefix-Origin-Paar wirkt autorisiert, dokumentiert und sichtbar.",
"explanation": "RPKI, Registry/IRR und Routing Visibility zeigen ein konsistentes Ergebnis.",
"risk": "Derzeit keine offensichtliche Inkonsistenz erkennbar.",
"recommendations": ["Routing-Sichtbarkeit und RPKI/Registry-Daten weiter überwachen."],
"summary": "Prefix-origin pair appears authorized, documented, and visible.",
"explanation": "RPKI, Registry/IRR, and routing visibility show a consistent result.",
"risk": "No obvious inconsistency is currently visible.",
"recommendations": ["Continue monitoring routing visibility and RPKI/registry data."],
}

if statuses["rpki"] == CheckStatus.OK and statuses["registry"] == CheckStatus.OK and statuses["routing"] == CheckStatus.UNKNOWN:
return _warning("Routing visibility could not be determined reliably.", "RPKI und Registry/IRR sind plausibel, aber die Routing-Sichtbarkeit bleibt unklar.")
return _warning("Routing visibility could not be determined reliably.", "RPKI and Registry/IRR are plausible, but routing visibility remains unclear.")

if CheckStatus.WARNING in statuses.values():
return _warning("Kombinierte Prefix-Bewertung zeigt Warnhinweise.", "Mindestens eine Einzelprüfung meldet unvollständige oder unsichere Daten.")
return _warning("Combined prefix evaluation shows warnings.", "At least one individual check reports incomplete or uncertain data.")

if CheckStatus.UNKNOWN in statuses.values():
return _warning("Teilweise bestätigte Datenlage mit Unsicherheit.", "Mindestens eine Quelle ist unklar; die Gesamtbewertung bleibt daher konservativ WARNING.")
return _warning("Partially confirmed data with uncertainty.", "At least one source is unclear; the overall assessment remains conservatively WARNING.")

return _unknown("Kombinierte Prefix-Bewertung nicht eindeutig bestimmbar.", "Die vorliegenden Einzelergebnisse konnten nicht konsistent kombiniert werden.")
return _unknown("Combined prefix evaluation is not clearly determinable.", "The available individual results could not be combined consistently.")


def _warning(summary: str, explanation: str) -> dict:
return {
"status": CheckStatus.WARNING.value,
"summary": summary,
"explanation": explanation,
"risk": "Die Gesamtaussage bleibt eingeschränkt.",
"recommendations": ["Individual checks und Rohdaten gezielt nacharbeiten."],
"risk": "The overall conclusion remains limited.",
"recommendations": ["Review individual checks and raw data in detail."],
}


Expand All @@ -64,8 +64,8 @@ def _critical(summary: str, explanation: str) -> dict:
"status": CheckStatus.CRITICAL.value,
"summary": summary,
"explanation": explanation,
"risk": "Erhöhtes Risk für Fehlrouting, Erreichbarkeitsprobleme oder Sicherheitsvorfälle.",
"recommendations": ["Abweichung priorisiert prüfen und beheben."],
"risk": "Increased risk of misrouting, reachability issues, or security incidents.",
"recommendations": ["Prioritize checking and resolving the discrepancy."],
}


Expand All @@ -74,8 +74,8 @@ def _unknown(summary: str, explanation: str) -> dict:
"status": CheckStatus.UNKNOWN.value,
"summary": summary,
"explanation": explanation,
"risk": "Die Datenlage ist unzureichend für eine belastbare Routing-Sicherheitsbewertung.",
"recommendations": ["Prüfung später wiederholen und Rohdaten kontrollieren."],
"risk": "The data is insufficient for a reliable routing security assessment.",
"recommendations": ["Repeat the check later and verify raw data."],
}


Expand Down
20 changes: 10 additions & 10 deletions backend/app/core/recommendations.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def default_recommendations(status: CheckStatus) -> list[str]:
if status == CheckStatus.WARNING:
return ["Review RPKI/registry data and improve coverage."]
if status == CheckStatus.CRITICAL:
return ["Origin-AS und ROA sofort verifizieren, da Route verworfen werden kann."]
return ["Verify origin AS and ROA immediately, because the route may be rejected."]
return ["Check again; external data source was unreliable or unavailable."]


Expand Down Expand Up @@ -38,20 +38,20 @@ def evaluate_rpki_status(rpki_status: str | None, prefix: str, origin_as: str |
return {
"status": CheckStatus.CRITICAL.value,
"summary": "RPKI validation failed",
"explanation": "Das Prefix wird mit einem Origin-AS geprüft, das nicht durch einen passenden ROA gedeckt ist.",
"explanation": "The prefix is checked with an origin AS that is not covered by a matching ROA.",
"risk": "Validierende Netze können diese Route verwerfen. Dadurch kann Erreichbarkeit verloren gehen.",
"recommendations": [
"Check whether the origin AS is correct.",
"Review existing ROAs for the prefix.",
"Erstelle oder korrigiere den ROA nur, wenn du zur Verwaltung dieser Ressourcen berechtigt bist.",
"Create or correct the ROA only if you are authorized to manage these resources.",
],
}
if normalized == "invalid_asn":
return {
"status": CheckStatus.CRITICAL.value,
"summary": "RPKI invalid: unauthorized origin AS",
"explanation": "Für das Prefix existiert ein ROA, aber nicht für dieses Origin-AS.",
"risk": "Validierende Netze können diese Route verwerfen, weil das Origin-AS nicht autorisiert ist.",
"explanation": "A ROA exists for the prefix, but not for this origin AS.",
"risk": "Validating networks may reject this route because the origin AS is not authorized.",
"recommendations": [
"Check the origin AS.",
"Review the ROA.",
Expand All @@ -62,8 +62,8 @@ def evaluate_rpki_status(rpki_status: str | None, prefix: str, origin_as: str |
return {
"status": CheckStatus.CRITICAL.value,
"summary": "RPKI invalid: announced prefix too specific",
"explanation": "Für das Prefix existiert ein ROA, aber die angekündigte Prefix-Länge ist länger als die erlaubte maxLength.",
"risk": "Validierende Netze können diese Route verwerfen, obwohl das AS grundsätzlich passen kann.",
"explanation": "A ROA exists for the prefix, but the announced prefix length is longer than the allowed maxLength.",
"risk": "Validating networks may reject this route, even though the AS may otherwise match.",
"recommendations": [
"Check announced prefix length.",
"Check ROA maxLength.",
Expand All @@ -74,8 +74,8 @@ def evaluate_rpki_status(rpki_status: str | None, prefix: str, origin_as: str |
return {
"status": CheckStatus.WARNING.value,
"summary": "No matching ROA found",
"explanation": "Für dieses Prefix-Origin-Paar wurde kein passender ROA gefunden.",
"risk": "Das ist nicht automatisch ein Ausfall, schwächt aber die Routing-Sicherheit.",
"explanation": "No matching ROA was found for this prefix-origin pair.",
"risk": "This is not automatically an outage, but it weakens routing security.",
"recommendations": [
"Check whether a ROA should be created.",
"Nur anlegen, wenn man zur Verwaltung berechtigt ist.",
Expand All @@ -90,6 +90,6 @@ def evaluate_rpki_status(rpki_status: str | None, prefix: str, origin_as: str |
"recommendations": [
"Review the API raw data.",
"Repeat the check later.",
"Vergleiche bei Bedarf mit einer zweiten Quelle oder einem lokalen RPKI-Validator.",
"Compare with a secondary source or a local RPKI validator if needed.",
],
}
2 changes: 1 addition & 1 deletion backend/app/core/system_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def build_system_status(engine: Engine | None) -> dict:
return {
"status": "ok",
"name": settings.app_name,
"version": "v0.9.2-rc",
"version": "v1.0.0",
"read_only": True,
"mode": "demo" if settings.demo_mode else "live",
"demo_mode": settings.demo_mode,
Expand Down
2 changes: 1 addition & 1 deletion backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
logging.basicConfig(level=getattr(logging, settings.log_level.upper(), logging.INFO))
logger = logging.getLogger("routeforge")

app = FastAPI(title="RouteForge", version="0.9.2")
app = FastAPI(title="RouteForge", version="1.0.0")

app.add_middleware(
CORSMiddleware,
Expand Down
4 changes: 2 additions & 2 deletions backend/app/services/asn_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ def check(self, asn_input: str) -> dict:

errors = []
if "error" in overview:
errors.append("as-overview nicht erreichbar")
errors.append("as-overview not reachable")
if "error" in prefixes:
errors.append("announced-prefixes nicht erreichbar")
errors.append("announced-prefixes not reachable")
status = CheckStatus.UNKNOWN.value if errors else CheckStatus.OK.value
summary = f"ASN {resource} checked."
return {
Expand Down
2 changes: 1 addition & 1 deletion backend/app/services/bgp_visibility_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def check(self, prefix: str, expected_origin_as: str | None) -> dict:
return {
"status": status,
"summary": summary,
"explanation": "BGP Visibility basiert auf RIPEstat-Daten und ist read-only.",
"explanation": "BGP visibility is based on RIPEstat data and is read-only.",
"risk": "External visibility data may be delayed or incomplete.",
"recommendations": recommendations,
"input": {"prefix": normalized_prefix, "expected_origin_as": normalized_expected},
Expand Down
Loading
Loading