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
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:
1 parent
503692a1da
commit
f5a4995da5
21 files changed
+3498
-29
No files matched your search
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user