Files
felhom.eu/scripts/dooplex_inventory.py
T
admin f5a4995da5
gates / gates (push) Successful in 3m27s
security watch sees runtimes (R-1327) and DooPlex (R-1328); HTTP2Shell rows R-1329–R-1332; R-1326 closed
- 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).
2026-10-27 23:23:02 +01:00

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:]))