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).
87 lines
4.2 KiB
Python
87 lines
4.2 KiB
Python
#!/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:]))
|