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
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""dooplex_inventory.py — the DooPlex (homelab) workload images, as a LIST for the security watch (R-1328). Read only.
|
||||
|
||||
WHY. HTTP2Shell (CVE-2026-111497) put a bad Brotli/Node inside about ten public apps on DooPlex, the host that runs the
|
||||
hub, Gitea, the registry and the backups — and nothing watched DooPlex's apps at all. Felhom does not run DooPlex's
|
||||
workloads (the homelab session does), so this is a LIST: the watch reads each image (registry tags + the runtime
|
||||
inside it) and mails the operator; nothing on DooPlex is ever changed from here.
|
||||
|
||||
WHAT IT READS. Every `image:` line in the homelab-manifests clone (default ~/git/homelab-manifests; HOMELAB_MANIFESTS
|
||||
overrides), one line per distinct image:tag, with the files that name it. NOT read: images a Helm chart chooses inside
|
||||
its own templates (Longhorn, ArgoCD, cert-manager, the ingress controller, …) — they appear in no `image:` line of the
|
||||
repo; the list file says so in its header.
|
||||
|
||||
OUTPUT: documentation/security-watch/dooplex-workloads.txt — `IMAGE:TAG<TAB>WHERE` lines under a dated header. The watch
|
||||
reads the FILE (CI cannot read the homelab repo); re-run this to refresh it.
|
||||
|
||||
USAGE: python3 scripts/dooplex_inventory.py [--check] (--check: exit 1 when the committed list differs from the repo)
|
||||
"""
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
OUT = os.path.join(os.path.dirname(HERE), "documentation", "security-watch", "dooplex-workloads.txt")
|
||||
SRC = os.environ.get("HOMELAB_MANIFESTS") or os.path.expanduser("~/git/homelab-manifests")
|
||||
IMAGE_RX = re.compile(r"^\s*-?\s*image:\s*[\"']?([^\"'\s#]+)")
|
||||
|
||||
|
||||
def collect(root):
|
||||
found = {}
|
||||
for d, dirs, files in os.walk(root):
|
||||
dirs[:] = [x for x in dirs if x != ".git"]
|
||||
for f in files:
|
||||
if not f.endswith((".yaml", ".yml")):
|
||||
continue
|
||||
p = os.path.join(d, f)
|
||||
try:
|
||||
lines = open(p, encoding="utf-8", errors="replace").read().splitlines()
|
||||
except OSError:
|
||||
continue
|
||||
for ln in lines:
|
||||
m = IMAGE_RX.match(ln)
|
||||
if not m or "{{" in m.group(1) or "$" in m.group(1):
|
||||
continue
|
||||
ref = m.group(1).split("@", 1)[0] # a digest pin is kept as its tag (the watch reads the tag)
|
||||
if ":" not in ref.rsplit("/", 1)[-1]:
|
||||
ref += ":latest"
|
||||
found.setdefault(ref, set()).add(os.path.relpath(p, root))
|
||||
return found
|
||||
|
||||
|
||||
def render(found, head):
|
||||
out = ["# DooPlex (homelab) workload images — the security watch's „dooplex\" group (R-1328). GENERATED by",
|
||||
"# scripts/dooplex_inventory.py from homelab-manifests %s — read only; nothing on DooPlex is changed from" % head,
|
||||
"# here. NOT in this list: images a Helm chart picks inside its own templates (Longhorn, ArgoCD, cert-manager,",
|
||||
"# the ingress controller and the like) — they are in no `image:` line of the repo."]
|
||||
for ref in sorted(found):
|
||||
out.append("%s\t%s" % (ref, ", ".join(sorted(found[ref]))[:300]))
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
def main(argv):
|
||||
if not os.path.isdir(SRC):
|
||||
print("homelab-manifests not found at %s" % SRC)
|
||||
return 2
|
||||
head = subprocess.run(["git", "-C", SRC, "log", "-1", "--format=%h (%cd)", "--date=short"], capture_output=True,
|
||||
text=True).stdout.strip() or "(no git)"
|
||||
text = render(collect(SRC), head)
|
||||
if "--check" in argv:
|
||||
old = open(OUT, encoding="utf-8").read() if os.path.isfile(OUT) else ""
|
||||
strip = lambda t: [l for l in t.splitlines() if not l.startswith("#")] # noqa: E731
|
||||
same = strip(old) == strip(text)
|
||||
print("dooplex list %s" % ("is current" if same else "DIFFERS from homelab-manifests %s" % head))
|
||||
return 0 if same else 1
|
||||
os.makedirs(os.path.dirname(OUT), exist_ok=True)
|
||||
with open(OUT, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
print("wrote %s: %d images (homelab-manifests %s, %s)" % (OUT, text.count("\n") - 4, head,
|
||||
datetime.date.today().isoformat()))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
Reference in new issue
Block a user