cve: the stale-decision counter was blind to the case it exists for
I wrote 'and ziel in soll' into that condition this evening, and it means a decision only counts as stale while its image is still deployed. The moment an image leaves the inventory — a version bump, which is exactly when decisions go stale — the entry stops being checked and sits in the file forever. Grafana 12.0.0 and 12.4.9 proved it hours later: both entries survived the jump to 13.2.0 and the counter reported zero. The cost of removing the guard is that a decision made before the scanner reaches its image shows up here until the next round. The alert waits 24 hours, which covers it. The existing arithmetic test could not have caught this, because it reimplements the calculation and the faulty condition was never in the copy. New tests drive the real collect() instead, including one that the old behaviour fails. Both dead grafana entries removed: 28 decisions down to 26.
This commit is contained in:
@@ -261,7 +261,19 @@ def collect():
|
||||
# ⚠️ Eine Entscheidung ohne Befund ist Altpapier: das Image ist ersetzt
|
||||
# oder der Befund weg. Sie zaehlt hier, damit die Datei nicht zuwaechst
|
||||
# und irgendwann Befunde deckt, die niemand mehr geprueft hat.
|
||||
if (ziel, cve) not in laufend and ziel in soll:
|
||||
#
|
||||
# ⚠️ KEIN "und ziel in soll" mehr. Genau diese Bedingung stand hier bis
|
||||
# zum 2026-08-22 und machte den Zaehler blind fuer den haeufigsten Fall:
|
||||
# Ist das Image ganz aus dem Bestand gefallen (Fassungssprung), faellt
|
||||
# auch seine Entscheidung aus der Pruefung - und bleibt fuer immer
|
||||
# unbemerkt in der Datei stehen. Belegt am selben Tag: die Eintraege zu
|
||||
# grafana 12.0.0 und 12.4.9 ueberlebten den Sprung auf 13.2.0, und der
|
||||
# Zaehler meldete 0.
|
||||
#
|
||||
# Der Preis ist bekannt und tragbar: Wer eine Entscheidung trifft, BEVOR
|
||||
# der Scanner das Image erreicht, sieht sie bis zur naechsten Runde hier
|
||||
# auftauchen. Die Alarmregel wartet 24h und deckt das ab.
|
||||
if (ziel, cve) not in laufend:
|
||||
ohne_befund += 1
|
||||
lines.append(f"cve_entscheidungen_abgelaufen {abgelaufen}")
|
||||
lines.append(f"cve_entscheidungen_ohne_befund {ohne_befund}")
|
||||
|
||||
@@ -90,29 +90,6 @@
|
||||
"CVE-2026-8376"
|
||||
]
|
||||
},
|
||||
{
|
||||
"ziel": "grafana/grafana:12.0.0",
|
||||
"art": "geplant",
|
||||
"grund": "12.4.9 liegt im Repo (516641b) und ist GEMESSEN: 7 CRITICAL -> 1, 70 HIGH -> 3. Der Major 13.2.0 wurde nach Messung verworfen (HIGH 70 -> 162). Wirkt mit dem Ausrollen auf dem Betriebs-Host.",
|
||||
"pruefen_am": "2026-08-28",
|
||||
"cves": [
|
||||
"CVE-2025-68121",
|
||||
"CVE-2026-31789",
|
||||
"CVE-2026-33186",
|
||||
"CVE-2026-33815",
|
||||
"CVE-2026-33816",
|
||||
"GHSA-r277-6w6q-xmqw"
|
||||
]
|
||||
},
|
||||
{
|
||||
"ziel": "grafana/grafana:12.4.9",
|
||||
"art": "hingenommen",
|
||||
"grund": "Die Fassung, auf die wir heute gehoben haben (7 CRITICAL -> 1). Der eine Rest ist GHSA-r277-6w6q-xmqw in github.com/getkin/kin-openapi v0.133.0, einer mitgelieferten Go-Abhaengigkeit - behoben in 0.144.0, aber Grafana muss sie ziehen, wir koennen es nicht. Der Major 13.2.0 wurde gemessen verworfen (HIGH 70 -> 162). Beim naechsten 12er-Patch nachsehen.",
|
||||
"pruefen_am": "2026-09-21",
|
||||
"cves": [
|
||||
"GHSA-r277-6w6q-xmqw"
|
||||
]
|
||||
},
|
||||
{
|
||||
"ziel": "oci.element.io/synapse:v1.158.0",
|
||||
"art": "hingenommen",
|
||||
|
||||
@@ -138,6 +138,71 @@ class RechnungTest(unittest.TestCase):
|
||||
self.assertEqual(offen, 1)
|
||||
|
||||
|
||||
class CollectTest(unittest.TestCase):
|
||||
"""⚠️ Diese Klasse prueft die ECHTE collect(), nicht die nachgebaute Rechnung
|
||||
in RechnungTest. Der Unterschied ist an diesem Projekt schon einmal teuer
|
||||
geworden: Am 2026-08-22 stand in collect() ein `and ziel in soll`, das den
|
||||
Zaehler fuer verwaiste Entscheidungen blind machte, sobald ein Image ganz aus
|
||||
dem Bestand fiel - und RechnungTest konnte das nicht sehen, weil dort die
|
||||
Bedingung gar nicht stand.
|
||||
"""
|
||||
|
||||
def laufe(self, berichte, entscheidungen, soll):
|
||||
res = tempfile.mkdtemp()
|
||||
for i, (ziel, cves) in enumerate(berichte):
|
||||
json.dump({"ArtifactName": ziel, "ArtifactType": "container_image",
|
||||
"Results": [{"Vulnerabilities": [
|
||||
{"VulnerabilityID": c, "Severity": "CRITICAL", "PkgName": "p",
|
||||
"InstalledVersion": "1", "FixedVersion": ""} for c in cves]}]},
|
||||
open(os.path.join(res, f"b{i}.json"), "w"))
|
||||
pfad = schreibe({"eintraege": entscheidungen})
|
||||
alt_res, alt_ent = exporter.RESULTS, exporter.ENTSCHEIDUNGEN_FILE
|
||||
exporter.RESULTS, exporter.ENTSCHEIDUNGEN_FILE = res, pfad
|
||||
exporter.ziel_cache._letzte = exporter.ziel_herleitung.Herleitung(
|
||||
ziele=set(soll), je_quelle={"cluster": set(soll)}, fehler={},
|
||||
stand={"cluster": exporter.time.time()})
|
||||
exporter.ziel_cache._geholt_um = exporter.time.time()
|
||||
try:
|
||||
zeilen = exporter.collect().splitlines()
|
||||
finally:
|
||||
exporter.RESULTS, exporter.ENTSCHEIDUNGEN_FILE = alt_res, alt_ent
|
||||
self.zeilen = zeilen
|
||||
return {z.split(" ")[0]: z.split(" ")[1] for z in zeilen if " " in z and "{" not in z}
|
||||
|
||||
E = [{"ziel": "a:1", "art": "hingenommen", "grund": "x" * 70,
|
||||
"pruefen_am": "2099-01-01", "cves": ["CVE-1"]}]
|
||||
|
||||
def test_entschieden_und_offen_werden_getrennt_gezaehlt(self):
|
||||
m = self.laufe([("a:1", ["CVE-1", "CVE-2"])], self.E, ["a:1"])
|
||||
self.assertEqual(m["cve_critical_entschieden"], "1")
|
||||
self.assertEqual(m["cve_critical_offen"], "1")
|
||||
self.assertEqual(m["cve_entscheidungen_ohne_befund"], "0")
|
||||
|
||||
def test_entscheidung_zu_einem_VERSCHWUNDENEN_image_faellt_auf(self):
|
||||
# Der Fall vom 2026-08-22: das Image ist ersetzt, das Ziel steht nicht
|
||||
# mehr im Soll, der Bericht ist weg. Die Entscheidung ist Altpapier und
|
||||
# muss sich melden - vorher meldete sie sich NICHT.
|
||||
m = self.laufe([("b:1", [])], self.E, ["b:1"])
|
||||
self.assertEqual(m["cve_entscheidungen_ohne_befund"], "1")
|
||||
|
||||
def test_gegenprobe_eine_gebrauchte_entscheidung_meldet_sich_nicht(self):
|
||||
m = self.laufe([("a:1", ["CVE-1"])], self.E, ["a:1"])
|
||||
self.assertEqual(m["cve_entscheidungen_ohne_befund"], "0")
|
||||
self.assertEqual(m["cve_critical_offen"], "0")
|
||||
|
||||
def test_der_entschiedene_befund_bleibt_SICHTBAR(self):
|
||||
# Der Kern der Entscheidung gegen eine Trivy-Ignore-Datei: entschieden
|
||||
# heisst gezaehlt, nicht versteckt. Waere er unterdrueckt, fehlte diese
|
||||
# Serie - und "0, weil behoben" saehe aus wie "0, weil weggeschaut".
|
||||
m = self.laufe([("a:1", ["CVE-1"])], self.E, ["a:1"])
|
||||
self.assertEqual(m["cve_critical_entschieden"], "1")
|
||||
treffer = [z for z in self.zeilen if z.startswith("trivy_vuln_info{") and "CVE-1" in z]
|
||||
self.assertEqual(len(treffer), 1, "der entschiedene Befund fehlt in trivy_vuln_info")
|
||||
# Gegenprobe: ein Ziel ohne Befund erzeugt auch keine Serie.
|
||||
self.laufe([("a:1", [])], self.E, ["a:1"])
|
||||
self.assertFalse([z for z in self.zeilen if z.startswith("trivy_vuln_info{")])
|
||||
|
||||
|
||||
class EchteDateiTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.getroffen, self.fehler = exporter.lade_entscheidungen(ECHT)
|
||||
|
||||
Reference in New Issue
Block a user