security watch: remember a notice per group — a catalog pin and a DooPlex pin of one image no longer share one memory entry (old entries copied once, no re-mail); W13

This commit is contained in:
admin committed 2026-10-28 10:39:41 +01:00
1 parent 73c91eacce
commit 29e9cd36a6
3 files changed
+55 -2

No files matched your search

+9
View File
@@ -1,3 +1,12 @@
## 2026-10-28 — the watch remembers a notice per GROUP: a catalog pin and a DooPlex pin of one image are two entries
- **`security_watch.py`:** the memory key is `dooplex:<image>|<id>` for a DooPlex pin (`mem_image`), `<image>|<id>` as
before for the rest. Before, the catalog's RomM 5.4.1 and DooPlex's RomM 5.4.0 shared one key, `plan()` kept the later
item, and a new notice on both was mailed in ONE group's block only (the homelab session or the catalog work, never
both). A memory written before this is copied once to the DooPlex key, so the first night after the change mails
nothing again. Found while classifying the 20 runtime apps (R-1332); fixed in the session, no row.
- Tests: `test_security_watch.py` W13 (5 checks; 4 red on the image-only key) — 64 checks.
## 2026-10-28 — the security watch reads zstd layers in CI by itself (R-1327)
- **`image_runtime.py`:** zstd layers are decoded by `zstandard` 0.25.0 — an importable one, else the wheel pinned by
+15 -2
View File
@@ -630,8 +630,13 @@ def source_name(kind, arg):
# ── memory and mails (pure: the tests drive this directly) ──────────────────────────────────────────────────────────
def mem_image(it):
"""The image as the memory names it: a DooPlex pin of an image the catalog also ships is its own entry (R-1333)."""
return ("dooplex:" if it.get("group") == "dooplex" else "") + it["image"]
def adv_key(it, a):
return "%s|%s" % (it["image"], a["id"])
return "%s|%s" % (mem_image(it), a["id"])
def label(it):
@@ -649,9 +654,17 @@ def plan(items, state, today, seed=False):
open_now, checked_images = {}, set()
for it in items:
if not it.get("not_checked"):
checked_images.add(it["image"])
checked_images.add(mem_image(it))
for a in it.get("advisories", []):
open_now[adv_key(it, a)] = (it, a)
# A memory written before R-1333 keyed a DooPlex notice by the image alone: copy it to the DooPlex key, so the first
# night after the change mails nothing again; the shared key stays only while a catalog pin still has the notice.
for k in [k for k in open_now if k.startswith("dooplex:") and k not in known]:
shared = k[len("dooplex:"):]
if shared in known:
known[k] = dict(known[shared])
if shared not in open_now:
del known[shared]
# A seed that could not read some pins (no key: rotation; or a source down) leaves them PENDING: their first real
# read is recorded silently too, so the first nights after a seed are no mail storm. A pin added later is never
# pending, so its advisories mail normally.
+31
View File
@@ -20,11 +20,14 @@ What is pinned, each by its consequence (the mail or the class), not by its mech
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
W13 one image in the catalog AND on DooPlex (R-1333): a new notice is mailed for both pins; the memory keeps one
entry per group; an old shared key is copied, so nothing is mailed twice; a clean DooPlex pin forgets only its own
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
controls green).
W13 (2026-10-28, R-1333) was seen failing on the image-only key (4 of 5 checks red; the clean-DooPlex-pin check green).
"""
import datetime
import json
@@ -440,10 +443,38 @@ def t_dooplex():
check("W12 the weekly mail has its own DooPlex block", "DooPlex (homelab)" in wk["text"], wk["text"][:300])
def t_two_groups():
print("-- W13: one image in the catalog AND on DooPlex (R-1333)")
cat = dict(adv_item("rommapp/romm", "CVE-2026-111497", group="app"), tag="5.4.1")
dpx = dict(adv_item("rommapp/romm", "CVE-2026-111497", group="dooplex"), tag="5.4.0")
m1, st, _ = w.plan([cat, dpx], {"weekly_last": TODAY.isoformat()}, TODAY)
adv = [m for m in m1 if m["kind"] == "advisory"]
check("W13 a new notice on both pins is mailed for BOTH (the catalog's 5.4.1 and DooPlex's 5.4.0)",
len(adv) == 1 and "5.4.1" in adv[0]["text"] and "5.4.0" in adv[0]["text"] and "homelab" in adv[0]["text"],
adv[0]["text"][-500:] if adv else kinds(m1))
check("W13 the memory keeps one entry per group", sorted(st["advisories"]) ==
["dooplex:rommapp/romm|CVE-2026-111497", "rommapp/romm|CVE-2026-111497"], sorted(st["advisories"]))
# a memory written before R-1333 holds ONE shared key: the first night after the change mails nothing again
old = {"weekly_last": TODAY.isoformat(), "advisories": {"rommapp/romm|CVE-2026-111497": {
"first": "2026-10-27", "last_mailed": "2026-10-27", "program": "rommapp/romm 5.4.0 (x)", "summary": "s"}}}
m2, st2, _ = w.plan([cat, dpx], old, TODAY)
check("W13 the old shared key is copied to the DooPlex key: no mail again", kinds(m2, "advisory") == [] and
st2["advisories"].get("dooplex:rommapp/romm|CVE-2026-111497", {}).get("first") == "2026-10-27", (kinds(m2), st2["advisories"]))
m3, st3, _ = w.plan([dpx], dict(old), TODAY)
check("W13 a shared key whose catalog pin is gone moves to the DooPlex key (no stale twin, no mail)",
sorted(st3["advisories"]) == ["dooplex:rommapp/romm|CVE-2026-111497"] and kinds(m3, "advisory") == [],
(sorted(st3["advisories"]), kinds(m3)))
dclean = dict(clean("rommapp/romm"), group="dooplex")
_, st4, _ = w.plan([cat, dclean], st, TODAY)
check("W13 DooPlex's pin checked clean forgets only DooPlex's entry", sorted(st4["advisories"]) ==
["rommapp/romm|CVE-2026-111497"], sorted(st4["advisories"]))
def main():
t_ranges()
t_classes()
t_memory()
t_two_groups()
t_inventory()
for t in (t_runtime, t_ghcr_pages, t_dooplex):
try: