From 4758e35150597999fe0d31f0a0905d2b25dc6aa8 Mon Sep 17 00:00:00 2001 From: Thore Cimbal Date: Fri, 21 Aug 2026 12:00:00 +0000 Subject: [PATCH] cve: only scan registry repositories that are actually running (#0051) Decision by sorb: narrow the target set to what is operated rather than tidying the registry. A repository now counts only while at least one of its tags is running, which drops element-desktop-build and windows-vm and 61 of their 62 critical findings with them. Of those 62, exactly two had a fix available; they are build artefacts nobody runs, so remediation was never the right answer. The narrowing stays derived rather than maintained: the running set is the one the derivation already builds, so there is no second list to keep in step (ADR-0026). Order now matters. The registry selection needs the running estate to tell a rollback target from a build artefact, so it runs after the other two sources and is skipped when they yield nothing. That last part changes behaviour deliberately. A derivation where both estate sources answer successfully but empty used to count as complete and merely unusable; it now reports the registry as failed. If nothing is running at all, that is an outage rather than a normal state, and it should say so instead of hanging on a single boolean. The test carries the new contract with that reasoning written next to it, plus a counter-case proving the registry does run when the estate is known. --- monitoring/cve/targets.py | 50 ++++++++++++++++++++++++++++------ monitoring/cve/test_targets.py | 22 +++++++++++++-- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/monitoring/cve/targets.py b/monitoring/cve/targets.py index 4b24e30..d272e43 100644 --- a/monitoring/cve/targets.py +++ b/monitoring/cve/targets.py @@ -196,16 +196,31 @@ def tag_erstellt( return None -def ziele_registry(registry: str, je_repo: int = 3, timeout: float = 15.0) -> set: +def ziele_registry(registry: str, je_repo: int = 3, laufend: Optional[set] = None, + timeout: float = 15.0) -> set: """Die juengsten `je_repo` Fassungen jedes Repos. Reichweite laut Gate 1 (Entscheidung sorb, Option C): Bestand plus das, was ein Rollback realistisch treffen kann. Ein Rollback-Ziel mit bekannter Luecke ist der einzige Fall, in dem ein nicht laufendes Image betrieblich zaehlt - bei v0.1.0 rollt niemand mehr zurueck. + + ⚠️ EINENGUNG (#0051, Entscheidung sorb 2026-08-21): Beruecksichtigt werden nur + Repos, von denen gerade MINDESTENS EIN Tag laeuft. Ohne diese Bedingung + kamen reine Bau-Artefakte in die Zielmenge - `element-desktop-build` und + `windows-vm` trugen zusammen 61 CRITICAL, davon 1 behebbar. Befunde ueber + Images, die niemand betreibt, sind Rauschen, das wie Signal aussieht. + + Die Einengung ist ABGELEITET, nicht gepflegt: `laufend` ist dieselbe Menge, + die die Herleitung ohnehin bildet. Keine zweite Liste (ADR-0026). """ ziele = set() for repo in registry_repos(registry, timeout): + if laufend is not None and not any( + z.startswith(f"{registry}/{repo}:") for z in laufend + ): + # Kein Tag dieses Repos laeuft - Bau-Artefakt, kein Rollback-Ziel. + continue token = registry_token(registry, f"repository:{repo}:pull", timeout) kandidaten = [ t @@ -252,20 +267,22 @@ class Herleitung(NamedTuple): def herleiten(cfg: dict, vorheriger_stand: Optional[dict] = None) -> Herleitung: - """Fragt alle drei Quellen. Ein Ausfall kostet nur die eigene Quelle.""" + """Fragt alle drei Quellen. Ein Ausfall kostet nur die eigene Quelle. + + ⚠️ REIHENFOLGE IST BEDEUTSAM: Die Registry-Auswahl braucht den laufenden + Bestand, um Bau-Artefakte auszuschliessen (#0051). Faellt der Bestand aus, + faellt die Registry-Auswahl mit - und das ist richtig so: Eine + Registry-Auswahl ohne Bestandswissen koennte nicht unterscheiden, was ein + Rollback-Ziel ist und was ein Bau-Artefakt. + """ stand = dict(vorheriger_stand or {}) je_quelle, fehler = {}, {} jetzt = time.time() - aufgaben = ( - ("cluster", lambda: ziele_cluster(cfg["prometheus"], cfg["timeout"])), - ("operating", lambda: ziele_operating(cfg["prometheus"], cfg["timeout"])), - ("registry", lambda: ziele_registry(cfg["registry"], cfg["je_repo"], cfg["timeout"])), - ) - for name, hole in aufgaben: + def versuche(name, hole): if not cfg.get(f"{name}_an", True): je_quelle[name] = set() - continue + return try: je_quelle[name] = hole() stand[name] = jetzt @@ -273,6 +290,21 @@ def herleiten(cfg: dict, vorheriger_stand: Optional[dict] = None) -> Herleitung: je_quelle[name] = set() fehler[name] = f"{type(e).__name__}: {e}" + versuche("cluster", lambda: ziele_cluster(cfg["prometheus"], cfg["timeout"])) + versuche("operating", lambda: ziele_operating(cfg["prometheus"], cfg["timeout"])) + + laufend = je_quelle.get("cluster", set()) | je_quelle.get("operating", set()) + if laufend: + versuche("registry", lambda: ziele_registry( + cfg["registry"], cfg["je_repo"], laufend, cfg["timeout"])) + else: + # Ohne Bestandswissen keine Registry-Auswahl - lieber eine kleinere + # Menge als eine, die Bau-Artefakte mitschleppt. Als Fehler gemeldet, + # damit es nicht wie Erfolg aussieht. + je_quelle["registry"] = set() + if "registry" not in fehler: + fehler["registry"] = "uebersprungen: kein laufender Bestand bekannt" + ziele = set().union(*je_quelle.values()) if je_quelle else set() return Herleitung(ziele=ziele, je_quelle=je_quelle, fehler=fehler, stand=stand) diff --git a/monitoring/cve/test_targets.py b/monitoring/cve/test_targets.py index ab9f98a..a82ba1f 100644 --- a/monitoring/cve/test_targets.py +++ b/monitoring/cve/test_targets.py @@ -266,11 +266,29 @@ class Ausfallverhalten(unittest.TestCase): def test_gegenprobe_leere_menge_ist_NICHT_dasselbe_wie_erfolg(self): # Der Fall, der ohne diese Unterscheidung wie 100 % Deckung aussaehe. + # + # ⚠️ VERHALTEN GEAENDERT am 2026-08-21 (#0051): Antworten Cluster UND + # Betriebs-Host erfolgreich LEER, wird die Registry-Auswahl jetzt + # uebersprungen und als Fehler gemeldet. Frueher galt das als + # "vollstaendig, nur unbrauchbar". Der Grund fuer die Aenderung: Ohne + # laufenden Bestand kann die Registry-Auswahl Rollback-Ziele nicht von + # Bau-Artefakten unterscheiden - und laeuft wirklich nichts, ist das ein + # Ausfall und kein Normalzustand. Er soll deshalb als Fehler sichtbar + # sein, nicht nur an `brauchbar` haengen. self._stelle(set(), set(), set()) h = targets.herleiten(self.CFG) self.assertEqual(h.ziele, set()) - self.assertTrue(h.vollstaendig) # kein Fehler gemeldet ... - self.assertFalse(h.brauchbar) # ... aber trotzdem nicht verwertbar + self.assertFalse(h.brauchbar) # nicht verwertbar ... + self.assertFalse(h.vollstaendig) # ... UND es wird gesagt + self.assertIn("registry", h.fehler) + self.assertIn("kein laufender Bestand", h.fehler["registry"]) + + def test_registry_ohne_laufenden_bestand_wird_uebersprungen(self): + # Gegenprobe zur Zeile darueber: Mit Bestand laeuft die Registry sehr wohl. + self._stelle({"a:1"}, set(), {"reg/x:v1"}) + h = targets.herleiten(self.CFG) + self.assertIn("reg/x:v1", h.ziele) + self.assertNotIn("registry", h.fehler) class Deckungsrechnung(unittest.TestCase):