security watch sees runtimes (R-1327) and DooPlex (R-1328); HTTP2Shell rows R-1329–R-1332; R-1326 closed
gates / gates (push) Successful in 3m27s

- scripts/image_runtime.py: Node / Python Brotli / brotlicffi / apk / dpkg read from an image's registry config and
  layers (no pull; HEAD for the nightly digest; zstd via the zstd program where present; quay.io, index.docker.io).
- security_watch.py: a „runtime" source (security_watch_runtimes.json), cached by digest in state.json (154 images,
  filled on DooPlex tonight); ghcr.io tag lists are paged; the DooPlex group (own block, homelab next-step).
- dooplex_inventory.py + documentation/security-watch/dooplex-workloads.txt (89 images, homelab-manifests 5aeec8d).
- Tests W10–W12 (58 checks) + test_image_runtime.py (13), each red-proved.
- audits/http2shell-2026-10-27: the Home Assistant lane walk (proven), HOMELAB-LIST.md (19 to update), the run log.
- 08 §11 design line; 09 §3 decision 276 (HA monthly = same main version; decided by CC unattended).
This commit is contained in:
admin committed 2026-10-27 23:23:02 +01:00
1 parent 503692a1da
commit f5a4995da5
21 files changed
+3498 -29

No files matched your search

+118
View File
@@ -14,6 +14,13 @@ What is pinned, each by its consequence (the mail or the class), not by its mech
would have mailed
W8 the inventory: an image with no upstream line refuses (the gate), a gap needs a reason, the grammar
W9 a GitHub Actions advisory is not listed; an empty fix with a range ending at X reads „after X", not „no fix"
W10 the RUNTIME inside an image (R-1327, HTTP2Shell CVE-2026-111497): Node 24.18.1 -> ADVISORY from „runtime" with
the fixed 24.22.1; 24.22.1 -> none (negative control); Python Brotli 1.2.0 -> ADVISORY, 1.2.1 -> none; Node 20 (no
fix on its line) -> ADVISORY; a layer read error -> NOT CHECKED; a registry with no route -> a note, not a
failure; the result is cached by digest (a second night reads nothing); main() wires the reader
W12 the DooPlex (homelab) group (R-1328): read from its list file; a DooPlex advisory names the homelab session, never
the catalog's lane; a DooPlex image in a private registry is NOT CHECKED with the reason; its own weekly block
W11 ghcr.io tag lists are PAGED (Link: rel="next"): every page is read, so a newer tag on page 2 is seen
Red-proofs (2026-10-26): W5 with the `k not in known` test removed (every night mails) -> W5 fails; W6 with the streak
threshold at 1 -> W6 fails; W2 with FEED_DAYS check removed -> W2's old-item case fails; W4 with SourceError swallowed
-> W4 fails. W9 (2026-10-27, R-1310) was seen failing before its two rules existed (2 of 4 checks red, the two
@@ -323,11 +330,122 @@ def t_ranges():
a is True and fixed == "no fixed version named yet", (a, fixed))
def t_runtime():
print("-- W10: the runtime inside an image")
reads = []
def reader_for(answers):
def read(ref):
reads.append(ref)
return dict(answers[ref], ref=ref)
return read
answers = {
"ex/node-old:1.0.0": {"digest": "sha256:a", "node": "24.18.1", "unchecked": []},
"ex/node-new:1.0.0": {"digest": "sha256:b", "node": "24.22.1", "unchecked": []},
"ex/py-old:1.0.0": {"digest": "sha256:c", "brotli": "1.2.0", "unchecked": []},
"ex/py-new:1.0.0": {"digest": "sha256:d", "brotli": "1.2.1", "unchecked": []},
"ex/node-eol:1.0.0": {"digest": "sha256:e", "node": "20.19.5", "unchecked": []},
"ex/broken:1.0.0": {"digest": "sha256:f", "unchecked": ["layer sha256:1234 unreadable: ReadError"]},
"ex/private:1.0.0": {"unchecked": ["registry gitea.dooplex.hu: no anonymous token route"]},
}
items = [item(r.split(":")[0], "1.0.0", "self", group="app") for r in answers]
for it in items:
it["sources"] = []
routes = {"https://hub.docker.com/v2/repositories/": TAGS("1.0.0")}
w.run(items, FakeNet(routes), TODAY, False, runtime=reader_for(answers))
by = {it["image"]: it for it in items}
a = by["ex/node-old"]["advisories"]
check("W10 Node 24.18.1 -> ADVISORY from runtime, CVE-2026-111497, fixed 24.22.1",
by["ex/node-old"]["class"] == "ADVISORY" and a and a[0]["source"] == "runtime" and
a[0]["id"] == "CVE-2026-111497" and "24.22.1" in a[0]["fixed"], (by["ex/node-old"]["class"], a))
check("W10 negative control: Node 24.22.1 -> no advisory", by["ex/node-new"]["class"] == "CURRENT",
(by["ex/node-new"]["class"], by["ex/node-new"]["advisories"]))
check("W10 Python Brotli 1.2.0 -> ADVISORY, fixed 1.2.1", by["ex/py-old"]["class"] == "ADVISORY" and
"1.2.1" in by["ex/py-old"]["advisories"][0]["fixed"], by["ex/py-old"]["advisories"])
check("W10 negative control: Brotli 1.2.1 -> no advisory", by["ex/py-new"]["class"] == "CURRENT", by["ex/py-new"]["class"])
check("W10 Node 20 (no fixed release on its line) -> ADVISORY naming the end of life",
by["ex/node-eol"]["class"] == "ADVISORY" and "end of life" in by["ex/node-eol"]["advisories"][0]["summary"],
by["ex/node-eol"]["advisories"])
check("W10 a layer read error -> NOT CHECKED with source runtime", by["ex/broken"]["class"] == "NOT CHECKED" and
by["ex/broken"]["not_checked"][0]["source"] == "runtime", (by["ex/broken"]["class"], by["ex/broken"]["not_checked"]))
check("W10 a registry with no route -> a note, not a failure", by["ex/private"]["class"] == "CURRENT" and
any("runtime" in n for n in by["ex/private"]["notes"]), (by["ex/private"]["class"], by["ex/private"]["notes"]))
# the cache: a second night with the same digests reads no layer
cache, resolved, full = {}, [], []
def resolve(ref):
resolved.append(ref)
return answers[ref].get("digest")
def read_full(ref):
full.append(ref)
return dict(answers[ref], ref=ref)
r = w.cached_runtime_reader(cache, resolve=resolve, read=read_full)
r("ex/node-old:1.0.0")
r("ex/node-old:1.0.0")
check("W10 the result is cached by digest: one full read for two nights", full == ["ex/node-old:1.0.0"] and
len(resolved) == 2 and "sha256:a" in cache, (full, resolved, list(cache)))
r("ex/broken:1.0.0")
check("W10 an UNCHECKED result is not cached (tomorrow tries again)", "sha256:f" not in cache, list(cache))
src = open(os.path.join(HERE, "security_watch.py"), encoding="utf-8").read()
check("W10 main() hands the reader to run() (off only with --no-runtime)",
"runtime=None if \"no-runtime\" in opts else cached_runtime_reader(rcache))" in src)
def t_ghcr_pages():
print("-- W11: ghcr.io tags are paged")
routes = {
"https://ghcr.io/token": (200, {}, {"token": "t"}),
"https://ghcr.io/v2/ex/app/tags/list?n=10000": (200, {"Link": '</v2/ex/app/tags/list?last=1.0.0&n=10000>; rel="next"'},
{"tags": ["0.9.0", "1.0.0"]}),
"https://ghcr.io/v2/ex/app/tags/list?last=1.0.0": (200, {}, {"tags": ["1.0.1", "1.1.0"]}),
}
src = w.Sources(FakeNet(routes), TODAY, False)
names = src.tags("ghcr.io/ex/app")
check("W11 both pages are read (1.1.0 is on page 2)", names == ["0.9.0", "1.0.0", "1.0.1", "1.1.0"], names)
def t_dooplex():
print("-- W12: the DooPlex group")
d = tempfile.mkdtemp()
try:
f = os.path.join(d, "dooplex-workloads.txt")
open(f, "w").write("# header\nrommapp/romm:5.4.0\tromm-system/romm.yaml\ngitea.dooplex.hu/admin/jarr:latest\tjarr/x.yaml\n")
items = inv.dooplex_items(f, catalog_sources={"rommapp/romm": "github:rommapp/romm"})
by = {i["image"]: i for i in items}
check("W12 the list file is read, group dooplex, the catalog's sources reused",
len(items) == 2 and by["rommapp/romm"]["group"] == "dooplex" and
by["rommapp/romm"]["sources_text"] == "github:rommapp/romm", items)
check("W12 an image the catalog does not ship is a stated gap (runtime + registry only)",
(by["gitea.dooplex.hu/admin/jarr"]["sources_text"] or "").startswith("gap:"), by["gitea.dooplex.hu/admin/jarr"])
finally:
shutil.rmtree(d)
it = {"image": "rommapp/romm", "tag": "5.4.0", "owner": "romm-system/romm.yaml", "where": "dooplex", "group": "dooplex"}
a = {"id": "CVE-2026-111497", "summary": "Node.js 24.18.1 inside the image", "fixed": "24.22.1", "severity": "critical",
"source": "runtime", "link": "x", "unsure": False}
m = w.advisory_mail([(it, a)], [], TODAY)
check("W12 a DooPlex advisory names the homelab session and not the lane", "homelab" in m["text"] and
"security-fix lane" not in m["text"].split("rommapp/romm")[1], m["text"][-400:])
priv = [item("gitea.dooplex.hu/admin/jarr", "latest", "gap:DooPlex workload", group="dooplex")]
priv[0]["sources"] = [("gap", "DooPlex workload")]
w.run(priv, FakeNet({}), TODAY, False, runtime=lambda ref: {"ref": ref, "unchecked": [
"registry gitea.dooplex.hu: no anonymous token route"]})
check("W12 a DooPlex image in a private registry -> NOT CHECKED with the reason", priv[0]["class"] == "NOT CHECKED" and
"gitea.dooplex.hu" in priv[0]["not_checked"][0]["why"], (priv[0]["class"], priv[0]["not_checked"]))
wk = w.weekly_mail([dict(it, behind="5.4.1", **{"class": "BEHIND"})], TODAY)
check("W12 the weekly mail has its own DooPlex block", "DooPlex (homelab)" in wk["text"], wk["text"][:300])
def main():
t_ranges()
t_classes()
t_memory()
t_inventory()
for t in (t_runtime, t_ghcr_pages, t_dooplex):
try:
t()
except Exception as e: # noqa: BLE001 — a missing feature is a red check, not a crash
check("%s ran" % t.__name__, False, "%s: %s" % (type(e).__name__, e))
print()
if fails:
for f in fails: