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.
This commit is contained in:
Thore Cimbal
2026-08-21 12:00:00 +00:00
parent c5b20a1e47
commit 4758e35150
2 changed files with 61 additions and 11 deletions
+41 -9
View File
@@ -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)
+20 -2
View File
@@ -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):