docs: Gate 2 for #0106 — the exporter owns the target set, ADR-0026 proposed
The set of targets that should be scanned is built where the set that was scanned is already known: in the existing exporter. Anything else needs the same derivation twice, and two derivations are two truths. That also settles where the coverage metric comes from. The AAR of 2026-08-01 decided one thing outright. Its third finding says a stale-scan rule cannot report an image that never scanned, because no series exists to hang the expression on. A coverage figure built from existing series is therefore blind to exactly the gap it is meant to show, so it has to come from the desired set instead. Two prices are written down rather than discovered later: a target that leaves the set must lose its report, or an image no one runs keeps reporting; and more targets mean more messages, roughly 65 instead of 29 per round, which Gate 5 has to measure since the reporting path itself is out of scope. ADR-0026 generalises it — derive targets, never maintain them — with the condition that makes it safe: a derivation that fails looks like full coverage, not like an outage, so its freshness is itself alerted.
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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<br/>Cluster · 39"]
|
||||
CAD["container_last_seen<br/>job=operating_cadvisor · 12"]
|
||||
end
|
||||
REG["rohana /v2<br/>Katalog + Tags<br/>anonymer Token"]
|
||||
|
||||
KSM --> EX
|
||||
CAD --> EX
|
||||
REG --> EX
|
||||
|
||||
EX["cve-exporter<br/>(bestehend, stdlib)<br/><b>bildet die Soll-Menge</b>"]
|
||||
EX -->|"targets.txt"| VOL[("cve_targets")]
|
||||
EX -->|"/metrics"| P
|
||||
|
||||
VOL --> SC["cve-scan (Trivy)<br/>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.**
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user