Sixty-three criticals on running images have no fix to take. Writing them into a trivy ignore file would have been the obvious move and the wrong one: trivy drops ignored findings from its output, so afterwards 'zero because fixed' and 'zero because we looked away' render identically. Every finding stays in trivy_vuln_info. The decisions sit beside them in entscheidungen.json and are counted, not subtracted. Each entry names its CVEs one by one. A blanket entry per image would also swallow the next finding that shows up there, which is the finding you would most want to see. The loader rejects an entry without ids, and rejects a review date it cannot parse — rejecting the whole file, because a half-read decision list is worse than none. An unreadable file leaves everything counted as open. Getting that direction backwards would mean a typo reads as 'all decided', and nobody would notice. The end-to-end run caught the same mistake in the other half: when the derived target set is empty the set is unknown, not empty, so the open count now falls back to every report rather than to zero. Three python services move off the debian base while we are here — 3.13-slim carried four criticals with no fix, 3.13-alpine none. All three run on the stdlib alone and TLS was checked inside the image before the switch.
249 lines
9.6 KiB
YAML
249 lines
9.6 KiB
YAML
# Regeltests fuer die Deckungs-Alarme (#0106) und die Entscheidungs-Alarme (#0051).
|
|
# docker run --rm --entrypoint promtool -v "$PWD/prometheus:/p:ro" \
|
|
# prom/prometheus:v3.14.0 test rules /p/alerts_test.yml
|
|
#
|
|
# ⚠️ ZWECK: Eine Regel, die nur je gruen war, ist eine Vermutung. In diesem
|
|
# Projekt ist "meldet Erfolg, ist aber blind" die haeufigste Fehlerklasse
|
|
# (AAR 2026-08-01, Befund 1 und 3). Deshalb steht zu jeder Regel ein Fall, in
|
|
# dem sie feuern MUSS, und einer, in dem sie schweigen MUSS - sonst laesst sich
|
|
# eine Regel, die immer feuert, von einer richtigen nicht unterscheiden.
|
|
#
|
|
# Erste Regeltests in diesem Stack ueberhaupt; die uebrigen 16 Regeln sind
|
|
# bewusst nicht mit abgedeckt (Nicht-Ziel von #0106).
|
|
rule_files:
|
|
- alerts.yml
|
|
|
|
evaluation_interval: 1m
|
|
|
|
tests:
|
|
# --- CveTargetsMissing ----------------------------------------------------
|
|
- interval: 1m
|
|
name: "Luecke in der Deckung feuert nach 6h"
|
|
input_series:
|
|
- series: 'cve_targets_missing'
|
|
values: '24+0x480' # 8h lang 24 fehlende Images
|
|
alert_rule_test:
|
|
- eval_time: 5h
|
|
alertname: CveTargetsMissing
|
|
exp_alerts: [] # noch in der Karenz - eine volle Runde darf dauern
|
|
- eval_time: 7h
|
|
alertname: CveTargetsMissing
|
|
exp_alerts:
|
|
- exp_labels:
|
|
severity: warning
|
|
room: security
|
|
exp_annotations:
|
|
summary: "24 laufende Images werden NICHT auf CVEs geprueft — die Schwachstellen-Sicht hat ein Loch, das sie selbst nicht zeigt. Details: https://selendis.axion1337.de/d/cve-overview"
|
|
|
|
- interval: 1m
|
|
name: "Volle Deckung schweigt"
|
|
input_series:
|
|
- series: 'cve_targets_missing'
|
|
values: '0+0x480'
|
|
alert_rule_test:
|
|
- eval_time: 7h
|
|
alertname: CveTargetsMissing
|
|
exp_alerts: []
|
|
|
|
# --- CveTargetsOrphaned ---------------------------------------------------
|
|
- interval: 1m
|
|
name: "Berichte ohne laufendes Image feuern"
|
|
input_series:
|
|
- series: 'cve_targets_orphaned'
|
|
values: '2+0x480'
|
|
alert_rule_test:
|
|
- eval_time: 7h
|
|
alertname: CveTargetsOrphaned
|
|
exp_alerts:
|
|
- exp_labels:
|
|
severity: info
|
|
room: security
|
|
exp_annotations:
|
|
summary: "2 CVE-Bericht(e) betreffen Images, die nirgends laufen — Befunde, auf die niemand handeln kann (Rauschen, das wie Signal aussieht)"
|
|
|
|
# --- CveTargetSourceStale (die wichtigste) --------------------------------
|
|
- interval: 1m
|
|
name: "Eine eingefrorene Quelle faellt auf"
|
|
input_series:
|
|
# Die Registry-Quelle altert, die anderen beiden bleiben frisch.
|
|
- series: 'cve_target_source_stale{quelle="registry"}'
|
|
values: '0+60x400' # waechst um 60s je Minute -> nach 180 min > 10800
|
|
- series: 'cve_target_source_stale{quelle="cluster"}'
|
|
values: '30+0x400'
|
|
- series: 'cve_target_source_stale{quelle="operating"}'
|
|
values: '30+0x400'
|
|
alert_rule_test:
|
|
- eval_time: 2h
|
|
alertname: CveTargetSourceStale
|
|
exp_alerts: [] # 7200s < 10800s - noch in Ordnung
|
|
- eval_time: 5h
|
|
alertname: CveTargetSourceStale
|
|
exp_alerts:
|
|
- exp_labels:
|
|
severity: warning
|
|
room: security
|
|
quelle: registry # ⚠️ NUR die eingefrorene, nicht alle drei
|
|
exp_annotations:
|
|
summary: "Die Quelle 'registry' der CVE-Zielmenge ist seit 18000s nicht erneuert worden — die Zielmenge schrumpft still, und die Deckung sieht dadurch BESSER aus statt schlechter"
|
|
|
|
# --- CveTargetsNoneDesired ------------------------------------------------
|
|
# Der Fall, den die drei anderen NICHT sehen: alle Quellen antworten sauber,
|
|
# aber leer. missing = 0, stale frisch - und es wird nichts mehr geprueft.
|
|
- interval: 1m
|
|
name: "Leere Zielmenge trotz gesunder Quellen"
|
|
input_series:
|
|
- series: 'cve_targets_desired_total'
|
|
values: '0+0x120'
|
|
- series: 'cve_targets_missing'
|
|
values: '0+0x120'
|
|
- series: 'cve_target_source_stale{quelle="cluster"}'
|
|
values: '5+0x120'
|
|
alert_rule_test:
|
|
- eval_time: 20m
|
|
alertname: CveTargetsNoneDesired
|
|
exp_alerts: [] # Karenz 30m
|
|
- eval_time: 40m
|
|
alertname: CveTargetsNoneDesired
|
|
exp_alerts:
|
|
- exp_labels:
|
|
severity: critical
|
|
room: security
|
|
exp_annotations:
|
|
summary: "Die CVE-Zielmenge ist LEER — es wird nichts mehr geprueft, obwohl keine Quelle einen Fehler meldet"
|
|
# Gegenprobe im selben Lauf: die anderen drei schweigen hier, obwohl die
|
|
# Lage schlecht ist. Genau deshalb braucht es die vierte Regel.
|
|
- eval_time: 40m
|
|
alertname: CveTargetsMissing
|
|
exp_alerts: []
|
|
- eval_time: 40m
|
|
alertname: CveTargetSourceStale
|
|
exp_alerts: []
|
|
|
|
# --- Gegenprobe: gesunder Zustand loest KEINEN der vier Alarme aus --------
|
|
- interval: 1m
|
|
name: "Alles in Ordnung bleibt still"
|
|
input_series:
|
|
- series: 'cve_targets_missing'
|
|
values: '0+0x480'
|
|
- series: 'cve_targets_orphaned'
|
|
values: '0+0x480'
|
|
- series: 'cve_targets_desired_total'
|
|
values: '65+0x480'
|
|
- series: 'cve_target_source_stale{quelle="cluster"}'
|
|
values: '120+0x480'
|
|
- series: 'cve_target_source_stale{quelle="operating"}'
|
|
values: '120+0x480'
|
|
- series: 'cve_target_source_stale{quelle="registry"}'
|
|
values: '900+0x480'
|
|
alert_rule_test:
|
|
- eval_time: 7h
|
|
alertname: CveTargetsMissing
|
|
exp_alerts: []
|
|
- eval_time: 7h
|
|
alertname: CveTargetsOrphaned
|
|
exp_alerts: []
|
|
- eval_time: 7h
|
|
alertname: CveTargetsNoneDesired
|
|
exp_alerts: []
|
|
- eval_time: 7h
|
|
alertname: CveTargetSourceStale
|
|
exp_alerts: []
|
|
|
|
# --- CveCriticalOffen (#0051) ---------------------------------------------
|
|
- interval: 1m
|
|
name: "Ein CRITICAL ohne Entscheidung faellt nach 24h auf"
|
|
input_series:
|
|
- series: 'cve_critical_offen'
|
|
values: '3+0x1800'
|
|
alert_rule_test:
|
|
- eval_time: 20h
|
|
alertname: CveCriticalOffen
|
|
exp_alerts: [] # Karenz: ein frisch entdeckter Befund darf
|
|
# einen Arbeitstag lang unentschieden sein
|
|
- eval_time: 26h
|
|
alertname: CveCriticalOffen
|
|
exp_alerts:
|
|
- exp_labels:
|
|
severity: warning
|
|
room: security
|
|
exp_annotations:
|
|
summary: "3 CRITICAL auf laufenden Images ohne Entscheidung — weder behoben noch mit Begruendung und Pruefdatum in cve/entscheidungen.json. Details: https://selendis.axion1337.de/d/cve-overview"
|
|
|
|
# --- CveEntscheidungAbgelaufen --------------------------------------------
|
|
# ⚠️ Der eigentliche Zweck der Entscheidungsdatei. Ohne diese Regel wuerde
|
|
# aus "wir haben uns das angesehen und es ist vertretbar" nach ein paar
|
|
# Monaten still "das steht halt so in der Datei".
|
|
- interval: 1m
|
|
name: "Ein ueberschrittenes Pruefdatum meldet sich"
|
|
input_series:
|
|
- series: 'cve_entscheidungen_abgelaufen'
|
|
values: '0+0x60 1+0x600' # erst alles gueltig, dann laeuft eine ab
|
|
alert_rule_test:
|
|
- eval_time: 1h
|
|
alertname: CveEntscheidungAbgelaufen
|
|
exp_alerts: [] # noch nichts abgelaufen
|
|
- eval_time: 8h
|
|
alertname: CveEntscheidungAbgelaufen
|
|
exp_alerts:
|
|
- exp_labels:
|
|
severity: warning
|
|
room: security
|
|
exp_annotations:
|
|
summary: "1 CVE-Entscheidung(en) haben ihr Pruefdatum ueberschritten — die Begruendung von damals gilt bis zum Gegenbeweis nicht mehr"
|
|
|
|
# --- CveEntscheidungenUnlesbar --------------------------------------------
|
|
- interval: 1m
|
|
name: "Eine kaputte Entscheidungsdatei nennt sich selbst"
|
|
input_series:
|
|
- series: 'cve_entscheidungen_lesefehler'
|
|
values: '1+0x120'
|
|
# Gleichzeitig zaehlt alles als offen - so ist der Exporter gebaut.
|
|
- series: 'cve_critical_offen'
|
|
values: '63+0x120'
|
|
alert_rule_test:
|
|
- eval_time: 15m
|
|
alertname: CveEntscheidungenUnlesbar
|
|
exp_alerts: [] # Karenz 30m
|
|
- eval_time: 45m
|
|
alertname: CveEntscheidungenUnlesbar
|
|
exp_alerts:
|
|
- exp_labels:
|
|
severity: warning
|
|
room: security
|
|
exp_annotations:
|
|
summary: "cve/entscheidungen.json ist nicht lesbar — bis das behoben ist, zaehlt jeder CRITICAL als unentschieden"
|
|
# ⚠️ Gegenprobe zum Zusammenspiel: CveCriticalOffen schweigt hier noch
|
|
# (Karenz 24h). Ohne die Regel darueber bliebe der wahre Grund also
|
|
# einen ganzen Tag lang unsichtbar.
|
|
- eval_time: 45m
|
|
alertname: CveCriticalOffen
|
|
exp_alerts: []
|
|
|
|
# --- Gegenprobe: entschiedener Bestand bleibt still ------------------------
|
|
- interval: 1m
|
|
name: "Alles entschieden und gueltig bleibt still"
|
|
input_series:
|
|
- series: 'cve_critical_offen'
|
|
values: '0+0x1800'
|
|
- series: 'cve_critical_entschieden'
|
|
values: '63+0x1800'
|
|
- series: 'cve_entscheidungen_abgelaufen'
|
|
values: '0+0x1800'
|
|
- series: 'cve_entscheidungen_ohne_befund'
|
|
values: '0+0x1800'
|
|
- series: 'cve_entscheidungen_lesefehler'
|
|
values: '0+0x1800'
|
|
alert_rule_test:
|
|
- eval_time: 26h
|
|
alertname: CveCriticalOffen
|
|
exp_alerts: []
|
|
- eval_time: 26h
|
|
alertname: CveEntscheidungAbgelaufen
|
|
exp_alerts: []
|
|
- eval_time: 26h
|
|
alertname: CveEntscheidungOhneBefund
|
|
exp_alerts: []
|
|
- eval_time: 26h
|
|
alertname: CveEntscheidungenUnlesbar
|
|
exp_alerts: []
|