Restarting k3s took kube-state-metrics and alloy's log tailers down with it, so kube_pod_container_info went empty in prometheus. The derivation asked, got a clean response with zero rows, and counted it as success. Cluster targets went from 39 to none, the total from 54 to 12, and the scan loop deleted every report whose target had vanished. Coverage then read 1.0. None of the four rules fired, and each for a defensible reason: the set was not empty because the operating host still answered, and every timestamp was fresh because an empty success updates it. The gap sat exactly between them. A source that has delivered before and now delivers nothing is treated as a failure: its previous targets are kept, its timestamp ages, and the stale rule takes over. A fifth rule watches the total for a drop of more than 40 percent, and it deliberately also fires on a deliberate shrink — losing 40 percent of the checked estate is worth a line either way. The six rancher decisions are gone too. They described versions that no longer run: the k3s patch took all fifteen of their criticals with it.
290 lines
11 KiB
YAML
290 lines
11 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: []
|
|
|
|
# --- CveZielmengeEingebrochen (#0051, Vorfall 2026-08-23) ------------------
|
|
- interval: 1m
|
|
name: "Ein Einbruch der Zielmenge faellt auf"
|
|
input_series:
|
|
# Der echte Verlauf: 54 Ziele, dann faellt die Cluster-Quelle still aus.
|
|
- series: 'cve_targets_desired_total'
|
|
values: '54+0x120 12+0x120'
|
|
alert_rule_test:
|
|
- eval_time: 1h
|
|
alertname: CveZielmengeEingebrochen
|
|
exp_alerts: [] # noch bei 54
|
|
- eval_time: 2h5m
|
|
alertname: CveZielmengeEingebrochen
|
|
exp_alerts: [] # eingebrochen, aber noch in der Karenz
|
|
- eval_time: 2h30m
|
|
alertname: CveZielmengeEingebrochen
|
|
exp_alerts:
|
|
- exp_labels:
|
|
severity: warning
|
|
room: security
|
|
exp_annotations:
|
|
summary: "Die CVE-Zielmenge ist auf 12 Ziele eingebrochen — ueber 40 % weniger als in den letzten 24 h. Faellt eine Quelle still aus, sieht das Dashboard danach BESSER aus, nicht schlechter."
|
|
# ⚠️ Gegenprobe zum Zusammenspiel: die vier aelteren Regeln schweigen in
|
|
# genau dieser Lage. Deshalb brauchte es eine fuenfte.
|
|
- eval_time: 2h30m
|
|
alertname: CveTargetsNoneDesired
|
|
exp_alerts: []
|
|
- eval_time: 2h30m
|
|
alertname: CveTargetsMissing
|
|
exp_alerts: []
|
|
|
|
- interval: 1m
|
|
name: "Ein kleiner Rueckbau bleibt still"
|
|
input_series:
|
|
- series: 'cve_targets_desired_total'
|
|
values: '54+0x60 44+0x180' # -19 %, unter der Schwelle
|
|
alert_rule_test:
|
|
- eval_time: 3h
|
|
alertname: CveZielmengeEingebrochen
|
|
exp_alerts: []
|