diff --git a/STATUS.md b/STATUS.md index 818fada..245d3a5 100644 --- a/STATUS.md +++ b/STATUS.md @@ -86,7 +86,7 @@ Bedeutung der Meilensteine: siehe [roadmap.md](roadmap.md). | Design | Gate | Title | |---|---|---| -| [2026-08-21-cve-ziele-ableiten](docs/design/2026-08-21-cve-ziele-ableiten.md) | gate-2 | Design: Die CVE-Zielmenge ableiten statt pflegen (#0106) | +| [2026-08-21-cve-ziele-ableiten](docs/design/2026-08-21-cve-ziele-ableiten.md) | gate-3 | Design: Die CVE-Zielmenge ableiten statt pflegen (#0106) | ## ADRs (26) diff --git a/docs/design/2026-08-21-cve-ziele-ableiten.md b/docs/design/2026-08-21-cve-ziele-ableiten.md index ecae937..f24161a 100644 --- a/docs/design/2026-08-21-cve-ziele-ableiten.md +++ b/docs/design/2026-08-21-cve-ziele-ableiten.md @@ -1,6 +1,6 @@ --- type: design -status: gate-2 +status: gate-3 date: 2026-08-21 size: L related: @@ -233,4 +233,138 @@ hinaus — es ist die allgemeine Form dessen, was hier schiefging. Als Ziele ableitet, muss das **Ausbleiben der Herleitung** alarmieren, weil eine leere Soll-Menge sonst wie vollständige Deckung aussieht. -> **STOP — Freigabe für Gate 2.** +> **Gate 2 freigegeben durch sorb, 2026-08-21.** + +## Gate 3 — Program Design + +### Dateien + +Alle im Repo `threadnet-operating`, alle unter `monitoring/`. + +| Pfad | Art | Was | +|---|---|---| +| `cve/targets.py` | **neu** | Herleitung der Soll-Menge; stdlib, importierbar | +| `cve/cve-exporter.py` | geändert | ruft die Herleitung, schreibt `targets.txt`, gibt die fünf neuen Metriken aus | +| `cve/scan-loop.sh` | geändert | liest `targets.txt` statt `images.txt`; löscht Berichte ausgefallener Ziele | +| `cve/images.txt` | **gelöscht** | Kriterium 1 | +| `docker-compose.yml` | geändert | Volume `cve_targets` (Exporter rw, Scanner ro); Umgebung für Quellen | +| `prometheus/alerts.yml` | geändert | drei Regeln in Gruppe `axion-cve` | +| `grafana/dashboards/security/cve-overview.json` | geändert | zwei Panels: Deckung, Frische je Quelle | +| `cve/test_targets.py` | **neu** | Zusicherungen, gegen aufgezeichnete Antworten | + +### Signaturen — `cve/targets.py` + +```python +NormalisiertesImage = str # ohne docker.io/ und library/, mit Tag + +def normalisiere(ref: str) -> NormalisiertesImage: ... + +def promql(basis_url: str, ausdruck: str, timeout: float = 10.0) -> list[dict]: ... + +def ziele_cluster(basis_url: str) -> set[NormalisiertesImage]: ... +def ziele_operating(basis_url: str) -> set[NormalisiertesImage]: ... # job=operating_cadvisor + +def registry_token(registry: str, scope: str) -> str: ... +def registry_repos(registry: str) -> list[str]: ... +def registry_tags(registry: str, repo: str) -> list[str]: ... +def tag_erstellt(registry: str, repo: str, tag: str) -> datetime | None: ... +def ziele_registry(registry: str, je_repo: int = 3) -> set[NormalisiertesImage]: ... + +class Herleitung(NamedTuple): + ziele: set[NormalisiertesImage] + je_quelle: dict[str, set[NormalisiertesImage]] # cluster | operating | registry + fehler: dict[str, str] # Quelle -> Meldung + stand: dict[str, float] # Quelle -> unix-ts des Erfolgs + +def herleiten(cfg: dict) -> Herleitung: ... +``` + +### Aufrufweg des Hauptflusses + +``` +cve-exporter: collect() alle 15 s (Scrape) + └─ ziel_cache.hole() gibt zwischengespeicherte Herleitung + └─ targets.herleiten(cfg) hoechstens alle SOLL_INTERVALL (Vorgabe 1 h) + ├─ ziele_cluster() → promql("count by (image) (kube_pod_container_info)") + ├─ ziele_operating() → promql('...{job="operating_cadvisor",image!=""}') + └─ ziele_registry() → registry_repos → registry_tags → tag_erstellt* → juengste 3 + ├─ schreibe targets.txt nur bei Erfolg mindestens einer Quelle + └─ metriken: cve_target_desired, _coverage_ratio, _missing, _orphaned, _source_stale + +cve-scan: scan-loop.sh alle 24 h + ├─ liest /targets/targets.txt fehlt sie → Runde aussetzen, NICHTS loeschen + ├─ je Ziel: trivy image → /results/.json + └─ loescht /results/*.json ohne Ziel in targets.txt +``` + +⚠️ **Die Herleitung läuft nicht je Scrape.** Bei 15 s Scrape-Intervall wären das +rund 21 000 Registry-Anfragen am Tag. Zwischenspeicher mit eigenem Intervall, +die Metriken werden aus dem Zwischenspeicher bedient. + +### Was die Tests zusichern + +**Normalisierung** — `docker.io/library/postgres:17-alpine`, `library/postgres:17-alpine` +und `postgres:17-alpine` ergeben denselben Wert; `rohana.axion1337.de/sorb/x:v1` +und `ghcr.io/a/b:v1` bleiben unangetastet; ein Ref ohne Tag wird verworfen, nicht +zu `:latest` ergänzt (sonst entstünde ein Ziel, das es nicht gibt). + +**Quellentrennung** — aus einer Antwort mit `operating_cadvisor` **und** +`gameserver_cadvisor` erscheint kein Ziel der zweiten Menge (Kriterium 6). + +**Registry-Auswahl** — aus Tags mit Zeitstempeln werden die jüngsten drei +gewählt, **nach Zeit, nicht nach Namen**: `v0.10.0` neuer als `v0.9.0` muss +gewinnen. `sha-*` und `latest*` fallen vorher raus. + +**Ausfallverhalten (der Kern)** — fällt eine Quelle aus, ist ihr Anteil leer, +`fehler` trägt die Meldung, `stand` bleibt auf dem alten Wert stehen, **und die +übrigen Quellen liefern weiter**. Fallen *alle* aus, ist `ziele` leer und +`targets.txt` wird **nicht** überschrieben. + +**Deckungsrechnung** — bei Soll = Ist ist `_missing` 0 und `ratio` 1,0; bei einem +fehlenden Ziel ist `_missing` 1; bei einem überzähligen Bericht ist `_orphaned` 1. +Gegenprobe mit einem absichtlich falschen Paar, damit die Rechnung nachweislich +anschlagen kann (AAR-Lehre: eine Prüfung schuldet den Nachweis, dass sie rot +werden kann). + +**Berichts-Löschung** — ein Bericht, dessen Ziel nicht mehr in `targets.txt` +steht, wird gelöscht; bei **fehlender** `targets.txt` wird nichts gelöscht. + +### Grenzen — DO NOT CHANGE + +- `alertmanager/matrix-alerts.py`, `alertmanager/alertmanager.yml` — der + Meldeweg ist Nicht-Ziel (ADR-0003). +- Die bestehenden fünf Regeln `TrivyCriticalVulns`, `TrivyHighVulns`, + `TrivyScanStale`, `TrivyReportUnreadable`, `TrivyNoReports` — es kommen + Regeln **dazu**, keine wird umformuliert. +- Das Metrik-Schema aus #0078 (`trivy_vuln_info`, `trivy_vuln_count`, + `trivy_vuln_first_seen_timestamp`, `trivy_last_scan_timestamp`) — nur + Ergänzungen. +- Die `first_seen`-Zustandsdatei und ihre Prune-Logik. +- Alles im Cluster-Repo `gitops`. Dieses Vorhaben fasst nur + `threadnet-operating` an. +- `game-operating`, `gameserver`, Homelab. + +### Wackeligste Annahmen + +1. ⚠️ **Dass `container_last_seen` den Betriebs-Stack vollständig zeigt.** + Gemessen wurden 12 Images — aber cAdvisor sieht nur, was **läuft**. Ein + Dienst, der gerade aus ist, fehlt in der Soll-Menge, und die Deckung sähe + trotzdem nach 100 % aus. Genau die Lücke, vor der ADR-0026 warnt. Gate 4 + muss das gegen `docker compose config` gegenprüfen, bevor Kriterium 2 als + erfüllt gilt. +2. ⚠️ **Dass „die jüngsten drei je Repo" das Richtige treffen.** Bei + `threadnet-web` (30 Tags) sind das `v0.6.0`, `v0.5.4`, `v0.5.3` — plausibel. + Bei `axion-backup` mit 2 Tags sind es beide, auch wenn `v1` niemand mehr + betreibt. Die Regel ist grob; sie ist nur besser als eine Liste. +3. **Dass zwei Anfragen je Tag genügen.** Bei einem Multi-Arch-Index sind es + drei (Index → Manifest → Config). Gemessen an `v0.6.0`, `v0.5.4`, `v0.1.0`; + nicht an allen 36. +4. **Dass der Exporter der richtige Ort bleibt.** Er bekommt eine zweite + Aufgabe und damit einen zweiten Grund auszufallen. Fällt er aus, fehlen + Metriken **und** die Zielmenge veraltet — beides sichtbar, aber gekoppelt. +5. **Dass die Löschung von Berichten nicht zu scharf ist.** Ein Ziel, das für + eine Runde aus der Soll-Menge fällt (etwa weil ein Pod gerade neu startet), + verliert seinen Bericht und damit seine `first_seen`-Historie. Ob das eine + Karenz braucht, entscheidet Gate 4 an der Messung. + +> **STOP — Freigabe für Gate 3.** diff --git a/docs/ledger/2026-08-21-cve-ziele-ableiten.md b/docs/ledger/2026-08-21-cve-ziele-ableiten.md index fea57ad..72fb22a 100644 --- a/docs/ledger/2026-08-21-cve-ziele-ableiten.md +++ b/docs/ledger/2026-08-21-cve-ziele-ableiten.md @@ -14,7 +14,8 @@ related: | gate | commit | approval | status | note | |---|---|---|---|---| -| 2 | | | OPEN | Herleitung im Exporter statt drittem Dienst; ADR-0026 vorgeschlagen. | +| 3 | | | OPEN | Dateien, Signaturen, Zusicherungen, Grenzen; fuenf wackelige Annahmen benannt. | +| 2 | 2693a2d | sorb | DONE | Herleitung im Exporter statt drittem Dienst; ADR-0026 vorgeschlagen. | | 1 | e10cd5d | sorb | DONE | Sechs Kriterien; Registry-Reichweite = Option C (Bestand + letzte drei je Repo). | ## Ladder @@ -26,6 +27,7 @@ related: | eine Quelle fuer die laufenden Images, statt eine zu bauen | `kube_pod_container_info` (39) und `container_last_seen{job="operating_cadvisor"}` (12) liegen in **derselben** Prometheus, die der Scanner ueber das vorhandene Compose-Netz erreicht | reused: kein neuer Dienst, keine Zugangsdaten, kein neuer Netzweg | | | einen Weg, game-operating auszuschliessen | `job="gameserver_cadvisor"` (20 Images) trennt sauber von `operating_cadvisor` | reused: ein Beschriftungsvergleich statt einer Ausschlussliste | | | ob die Registry ohne neue Zugangsdaten lesbar ist | anonymer Token-Tanz genuegt: `/v2/token` → Katalog und Tag-Listen; 6 Repos, 36 Tags | reused: die Standard-Registry-Authentifizierung, die der Scanner ohnehin schon fuer das Ziehen macht | | +| einen anonymen Weg an die Tag-Zeitstempel, bevor ein Token eingeplant wird | Giteas `/api/v1/packages` verlangt einen Token — die Registry-API dagegen nicht: Manifest → Config-Blob liefert `created`, anonym, ~90 Anfragen je Runde | reused: die Registry-Authentifizierung, die der Scanner ohnehin macht — Randbedingung "keine neuen Zugangsdaten" bleibt gewahrt | | | einen Ort fuer die Soll-Menge, bevor ein Dienst dafuer gebaut wird | der `cve-exporter` braucht sie fuer die Deckungsmetrik ohnehin und ist ein laufender stdlib-Python-Prozess im selben Netz | reused: bestehender Prozess statt drittem Container — kein `jq`, kein `pip`, keine zweite Wahrheit | | | eine AAR zu genau diesem Stack, bevor Randbedingungen geraten werden | AAR 2026-08-01: `up -d` aktiviert Configs nicht (im Container pruefen), und `TrivyScanStale` kann ein nie gescanntes Image nicht melden | reused: beide Funde als Randbedingung uebernommen statt neu zu entdecken — Befund 3 bestimmt, dass die Deckung aus der Soll-Menge kommen muss | | | ob die rohana-Berichte ueberhaupt frisch sind | alle 29 Berichte gleichmaessig 4,1 h alt — der Scanner zieht aus der Registry erfolgreich | reused: die bestehende Scan-Schleife bleibt, nur ihre Zielbeschaffung aendert sich | |