diff --git a/STATUS.md b/STATUS.md index 4e3f7d5..818fada 100644 --- a/STATUS.md +++ b/STATUS.md @@ -86,9 +86,9 @@ 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-1 | Design: Die CVE-Zielmenge ableiten statt pflegen (#0106) | +| [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) | -## ADRs (25) +## ADRs (26) | ADR | Status | Title | |---|---|---| @@ -117,6 +117,7 @@ Bedeutung der Meilensteine: siehe [roadmap.md](roadmap.md). | [0023](docs/adr/0023-fremdhistorie-von-der-git-hygiene-ausnehmen.md) | accepted | ADR-0023: Fremde Historie von der Git-Hygiene ausnehmen — erklärt, nicht global | | [0024](docs/adr/0024-drei-framework-dateien-erklaert-erweitert.md) | accepted | ADR-0024: Drei Framework-Dateien sind erklärt erweitert — Diff-Referenz statt Byte-Vergleich | | [0025](docs/adr/0025-egress-ausnahmen-als-cidr-ausser-hinter-cdn.md) | accepted | ADR-0025: Egress-Ausnahmen werden als CIDR gepinnt — außer hinter einem CDN | +| [0026](docs/adr/0026-pruefziele-werden-abgeleitet-nicht-gepflegt.md) | proposed | ADR-0026: Prüfziele werden abgeleitet, nicht gepflegt — und das Ausbleiben der Herleitung alarmiert | ## Open AARs (6) diff --git a/docs/adr/0026-pruefziele-werden-abgeleitet-nicht-gepflegt.md b/docs/adr/0026-pruefziele-werden-abgeleitet-nicht-gepflegt.md new file mode 100644 index 0000000..ffe264a --- /dev/null +++ b/docs/adr/0026-pruefziele-werden-abgeleitet-nicht-gepflegt.md @@ -0,0 +1,85 @@ +--- +type: adr +id: "0026" +status: proposed +date: 2026-08-21 +supersedes: null +superseded_by: null +related: + - "docs/issues/0106-cve-scan-deckt-nur-die-haelfte-zielliste-ist-handgepflegt.md" + - "docs/design/2026-08-21-cve-ziele-ableiten.md" + - "docs/adr/0003-cve-meldeweg-aggregiert.md" +--- + +# ADR-0026: Prüfziele werden abgeleitet, nicht gepflegt — und das Ausbleiben der Herleitung alarmiert + +## Kontext + +Der CVE-Scan bezog seine Ziele aus einer von Hand gepflegten Liste. Am +2026-08-21 gemessen: **27 von 51 laufenden Images**, also 52 % Deckung. Nicht +gescannt wurden der Client, den jeder Nutzer lädt, beide Ingress-Schichten, die +Registry selbst und der gesamte Beobachtungs-Stack. Gleichzeitig meldete der +Scan CVEs für ein Image, das nirgends läuft. + +Niemand hat einen Fehler gemacht. Die Liste war beim Anlegen richtig und ist +danach liegengeblieben, weil ihre Pflege an keinem Schritt hängt, den ein +Ausrollen ohnehin durchläuft. + +Dieselbe Bauart findet sich anderswo: `release-watch` lebt von einer +handgepflegten Repo-Liste, die Sperrlisten der URL-Vorschau werden von Hand +nachgezogen. Die Frage ist also allgemeiner als dieser eine Scan. + +## Optionen + +**A: Liste behalten, Pflege einfordern.** Kostet nichts und funktioniert genau +so lange, wie jemand daran denkt. Der gemessene Verfall — drei Fassungen und ein +Upstream-Merge Rückstand — ist das Ergebnis dieser Option. + +**B: Liste behalten, Abweichung alarmieren.** Der Alarm meldet zuverlässig, was +niemand pflegen wollte, und erzeugt eine wiederkehrende Handarbeit mit einem +Ticket davor. Eine Krücke für eine Liste, die es nicht geben müsste. + +**C: Ziele aus dem Bestand ableiten.** Die Liste entfällt. Der Preis ist eine +neue Abhängigkeit: Die Herleitung selbst kann ausfallen. + +## Entscheidung + +**Option C — mit einer Auflage, ohne die sie schlechter wäre als B.** + +- **Ein Prüfziel wird aus dem abgeleitet, was tatsächlich vorhanden ist**, nicht + aus einer Aufzählung. Bevorzugt aus einer Quelle, die aus einem anderen Grund + ohnehin gepflegt wird — im Anlassfall die Metriken, die den Bestand ohnehin + beschreiben. +- **Eine abgeleitete Menge braucht genau einen Ort.** Wer die Ist-Menge kennt, + bildet auch die Soll-Menge; zwei Herleitungen sind zwei Wahrheiten. +- ⚠️ **Das Ausbleiben der Herleitung muss alarmieren.** Das ist der Kern: Eine + leere oder verkürzte Soll-Menge sieht aus wie **vollständige Deckung**, nicht + wie ein Ausfall. Deshalb gehört je Quelle ein Frischezeitstempel in die + Metriken, und eine Regel darauf. +- **Was aus der Soll-Menge fällt, verliert seinen Befund.** Sonst meldet ein + ausgemustertes Ziel weiter, und die Ableitung hat das Rauschen nur um eine + Ebene verschoben. +- **Was ausgeschlossen wird, wird über ein Merkmal ausgeschlossen, nicht über + eine zweite Liste.** Im Anlassfall trennt eine Beschriftung + (`job="gameserver_cadvisor"`) den fremden Bestand — nicht eine Aufzählung der + fremden Namen. + +## Konsequenzen + +*Einfacher.* Ein Ausrollen bringt sein Prüfziel mit; niemand muss daran denken. +Was gemeldet wird, bezieht sich auf Vorhandenes. Die Deckung wird zur Zahl und +ist damit überhaupt erst bestreitbar. + +*Schwerer.* Die Prüfung hängt jetzt an der Verfügbarkeit ihrer Quellen. Diese +Abhängigkeit ist neu, sie ist real, und sie ist der Grund für die Auflage oben. + +*Was offen bleibt.* Für `release-watch` (#0022) gilt derselbe Befund, dort ist +die Quelle aber weniger offensichtlich — ein Repo-Bestand ist nicht dasselbe +wie ein laufender Bestand. Diese ADR entscheidet die Richtung, nicht die +Umsetzung dort. + +⚠️ *Was diese Entscheidung nicht leistet.* Sie sagt nichts darüber, ob die +abgeleiteten Ziele **vollständig** sind. Eine Quelle, die einen Teil des +Bestands gar nicht kennt, liefert eine lückenlose Deckung über eine unvollständige +Grundmenge. Genau das war der Ausgangszustand, nur mit einer Liste statt einer +Metrik — der Betriebs-Host kam in der Liste überhaupt nicht vor. diff --git a/docs/design/2026-08-21-cve-ziele-ableiten.md b/docs/design/2026-08-21-cve-ziele-ableiten.md index f368fbe..ecae937 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-1 +status: gate-2 date: 2026-08-21 size: L related: @@ -111,4 +111,126 @@ lautlos zurückkehren kann. Kein UI-Anteil. Sichtbar wird das Ergebnis im bestehenden Grafana-Dashboard `security/cve-overview.json` und im Security-Raum. -> **STOP — Freigabe für Gate 1.** +> **Gate 1 freigegeben durch sorb, 2026-08-21.** Reichweite der Registry: +> **Option C** — Bestand plus die letzten drei Fassungen je Repo. + +## Gate 2 — Architecture + +### Gelesen zuerst + +`monitoring/cve/{scan-loop.sh,cve-exporter.py,images.txt}`, `docker-compose.yml`, +`prometheus/alerts.yml`, `alertmanager/{alertmanager.yml,matrix-alerts.py}`, +**ADR-0003** (Meldeweg, angenommen) und die **AAR vom 2026-08-01**. Drei Punkte +daraus bestimmen den Entwurf unmittelbar: + +- ⚠️ **AAR-Befund 3:** *„`TrivyScanStale` kann ein nie erfolgreich gescanntes + Image nicht melden — ohne ersten Report existiert keine Serie, an der + `time() - trivy_last_scan_timestamp` hängen könnte."* Eine Deckungszahl, die + aus vorhandenen Serien gebildet wird, kann die Lücke also nie sehen. Sie muss + aus der **Soll-Menge** kommen. +- ⚠️ **AAR-Befund 2:** `docker compose up -d` aktiviert geänderte Konfiguration + nicht zuverlässig; geprüft wird **im Container**, nicht auf der Platte. + Verfahren: `docs/wiki/deployment/deploy-uebergabe.md`. +- **AAR, Entwarnung:** Trivy zieht anonym aus rohana. Am 2026-08-21 erneut + bestätigt — der anonyme Token-Tanz trägt auch Katalog und Tag-Listen. + +### Wie es in das reale System passt + +```mermaid +flowchart LR + subgraph P["Prometheus (Betriebs-Host)"] + KSM["kube_pod_container_info
Cluster · 39"] + CAD["container_last_seen
job=operating_cadvisor · 12"] + end + REG["rohana /v2
Katalog + Tags
anonymer Token"] + + KSM --> EX + CAD --> EX + REG --> EX + + EX["cve-exporter
(bestehend, stdlib)
bildet die Soll-Menge"] + EX -->|"targets.txt"| VOL[("cve_targets")] + EX -->|"/metrics"| P + + VOL --> SC["cve-scan (Trivy)
scannt · loescht Fremdberichte"] + SC -->|"JSON"| RES[("cve_results")] + RES --> EX +``` + +**Warum der Exporter und nicht die Scan-Schleife.** Die Soll-Menge wird für +Kriterium 4 ohnehin im Exporter gebraucht: Nur wer beide Mengen kennt — was +gescannt werden *soll* und was gescannt *wurde* — kann die Deckung als Metrik +ausgeben. Ein zweiter Ort, der dasselbe herleitet, wäre eine zweite Wahrheit. +Der Exporter ist außerdem bereits ein laufender Python-stdlib-Prozess im selben +Netz; er braucht keine neue Abhängigkeit, um JSON zu lesen. + +**Neue Metriken** (Ergänzung des Schemas aus #0078, nichts wird ersetzt): + +| Metrik | Bedeutung | +|---|---| +| `cve_target_desired{target,quelle}` | Soll-Ziel; `quelle` ∈ `cluster`, `operating`, `registry` | +| `cve_target_coverage_ratio` | gescannt ÷ Soll | +| `cve_targets_missing` | `\|Soll \ gescannt\|` — **Kriterium 2**, heute 24 | +| `cve_targets_orphaned` | `\|gescannt \ Soll\|` — **Kriterium 3**, heute 2 | +| `cve_target_source_stale{quelle}` | Alter der Herleitung je Quelle | + +Die letzte Zeile ist die Absicherung gegen den eigenen Mechanismus: Fällt eine +Quelle aus, ist die Soll-Menge zu klein, und die Deckung sähe **besser** aus, +nicht schlechter. Ohne diese Metrik wäre der Ausfall der Herleitung nicht von +Erfolg zu unterscheiden — die Fehlerklasse, die dieses Projekt am häufigsten +trifft. + +### Randbedingungen + +- **Kein neuer Dienst, keine neuen Zugangsdaten, kein neuer Netzweg.** Alle drei + Quellen sind aus dem bestehenden Compose-Netz erreichbar. +- **Nur stdlib.** Machart der übrigen Monitoring-Helfer; kein `jq`, kein `pip`. +- **`job="gameserver_cadvisor"` bleibt ausgeschlossen** (Kriterium 6). +- **Normalisierung ist Pflicht** (`docker.io/`, `library/`, impliziter + Docker-Hub-Präfix), sonst meldet der Abgleich Scheinlücken. +- ⚠️ **Ein wegfallendes Ziel muss seinen Bericht verlieren.** Der Exporter liest + *jede* `*.json` in `/results` und nimmt das Ziel aus Trivys `ArtifactName`. + Ohne Löschen meldet ein ausgemustertes Image weiter — genau der heutige + `v0.3.0`-Fall. Das Löschen gehört in die Scan-Schleife, die die Berichte + schreibt. +- ⚠️ **Mehr Ziele heißt mehr Nachrichten.** Die Alarme aggregieren je Ziel + (ADR-0003), also erzeugt Option C rund 65 statt 29 mögliche Meldungen je + Runde. Am 2026-08-20 kamen 12 auf einmal. Das ist eine Folge dieses + Vorhabens, aber der Meldeweg ist Nicht-Ziel — deshalb hier nur benannt und + in Gate 5 nachzumessen, nicht vorab umgebaut. +- **Laufzeit unkritisch:** Die AAR maß 0,4 s für 29 Berichte je Scrape; bei 65 + bleibt das im Rahmen. + +### Optionen + +**Wo entsteht die Soll-Menge?** + +| | Pro | Contra | +|---|---|---| +| **A — in `scan-loop.sh`** | Ziel und Verbraucher an einem Ort | Das Trivy-Image bringt kein `jq` und kein Python; JSON in POSIX-Shell zu zerlegen ist die fehleranfälligste Variante. Und die Deckungsmetrik bräuchte die Menge trotzdem ein zweites Mal | +| **B — im `cve-exporter`** *(gewählt)* | Ein Ort kennt Soll und Ist, also kann er die Deckung ausgeben; stdlib-JSON; Prozess läuft bereits | Der Exporter bekommt eine zweite Aufgabe; Reihenfolge beim Kaltstart zu bedenken | +| **C — eigener Dienst `cve-targets`** | Sauberste Trennung | Ein Container mehr für ~80 Zeilen, und die Deckungsmetrik läge wieder woanders als die Ist-Menge | + +**Gewählt: B.** Die Leiter sagt *„braucht es das überhaupt?"* — ein dritter +Prozess für eine Liste, die der zweite ohnehin bilden muss, ist keiner. + +⚠️ **Kaltstart:** Existiert `targets.txt` noch nicht, scannt die Schleife nichts +und wartet. Sie darf dabei **keine** vorhandenen Berichte löschen, sonst +verliert ein Neustart bei ausgefallener Prometheus den gesamten Bestand. Das +Löschen greift nur gegen eine **erfolgreich gebildete** Soll-Menge. + +**Reichweite Registry: Option C** (Gate 1, sorb): Bestand plus die letzten drei +Fassungs-Tags je Repo. `sha-*` und `latest-ci` bleiben draußen — Bau-Artefakte, +die nie ausgeliefert werden. Sortierung nach Tag ist nicht verlässlich +lexikografisch (`v0.10.0` < `v0.9.0`); maßgeblich ist der Zeitstempel aus der +Registry, nicht der Name. + +### Richtungsentscheidung + +Dass Prüfziele **abgeleitet und nicht gepflegt** werden, gilt über diesen Fall +hinaus — es ist die allgemeine Form dessen, was hier schiefging. Als +**ADR-0026** vorgeschlagen, mit dem Zusatz, der diesen Entwurf trägt: Wer +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.** diff --git a/docs/ledger/2026-08-21-cve-ziele-ableiten.md b/docs/ledger/2026-08-21-cve-ziele-ableiten.md index 5e049ff..fea57ad 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 | |---|---|---|---|---| -| 1 | | | OPEN | Wartet auf Freigabe; die Registry-Reichweite ist die offene Frage. | +| 2 | | | OPEN | 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 @@ -25,6 +26,8 @@ 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 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 | | ## Notes