Files
felhom.eu/documentation/audits/website-refresh-2026-10-08/shots/tools/wr.py
T
admin 67b3fd23d1
gates / gates (push) Failing after 13m1s
website refresh: true numbers (56 apps), claims checked against the capability map, ten missing apps, an English twin of every public page (public, choice B), 404 page; site gates 13-18 + 11 decoys; R-902 opened
Part A: claim table documentation/audits/website-refresh-2026-10-08/claims.md; "100% open source" corrected
from the licence read; unbacked claims cut (firewall, RAID, snapshots, new-machine restore, self-managed
mode, household VPN, "never lost"). Part B: dawarich, docmost, grimmory, homebox, karakeep, mealie, metube,
radicale, recipe-importer, sparkyfitness cards; plant-it and wger cut (not offered). Part C: /en/ twins,
nav language switch, hreflang both ways, sitemap with xhtml:link, og-image-en.png. Part D: site_gates.py
twins/lang/same-apps/no-Hungarian/FAQ-JSON-LD/tail. Operator ruling 9 + choice B in 10-localisation.md §11,
§10.7. Register: R-902 (contact mailer source in no repo); R-813, R-784, R-559 annotated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
2026-10-08 08:29:17 +02:00

129 lines
5.4 KiB
Python

"""wr.py — website-refresh 2026-10-08: install ONE catalog app on scratch guest 9202 through the product, take its
screenshots with headless Chrome inside 9202, copy the raw PNGs off the box, remove the app through the product,
verify it is gone.
python3 wr.py baseline
python3 wr.py deploy <app> <sub>
python3 wr.py shoot <app> <sub> <script.js> [KEY=VAL ...] # secrets.json in the guest, 0600, removed after
python3 wr.py fetch <app> <guest-path> <local-path> # one file off the guest
python3 wr.py remove <app>
python3 wr.py verify <app>
Env: SC (0600 scratch with .ctlpw), EV (evidence dir). No secret is ever printed: the dashboard session and the
invented demo account's password travel only inside files (SC on DooPlex, /root/wr-shots/<app>/secrets.json in the
guest, removed by `shoot` when Chrome exits)."""
import base64, json, os, secrets, sys, time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import sw # noqa: E402
b = sw.b
SC, EV = os.environ["SC"], os.environ["EV"]
IMG = "ghcr.io/puppeteer/puppeteer:latest"
TRAEFIK = "172.18.0.5"
APPS = ["docmost", "homebox", "mealie", "recipe-importer", "sparkyfitness"]
def log(app, *a):
b.say(*a)
os.makedirs(f"{EV}/{app}", exist_ok=True)
with open(f"{EV}/{app}/run.log", "a") as f:
f.write(" ".join(map(str, a)) + "\n")
def secfile(app):
p = f"{SC}/sec-{app}.json"
if os.path.exists(p):
return json.load(open(p))
d = {"user": "demo@example.com", "pw": "Demo-" + secrets.token_hex(10), "name": "Demo Csalad"}
fd = os.open(p, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
os.write(fd, json.dumps(d).encode()); os.close(fd)
return d
def baseline():
b.login()
c, d = b.ctl("GET", "/api/stacks")
dep = [(s.get("name"), s.get("state")) for s in (d.get("data") or []) if isinstance(s, dict) and s.get("deployed")]
out = b.guest("df -h / | tail -1; docker ps --format '{{.Names}} {{.Image}}'; "
"docker volume ls --format '{{.Name}}' | grep -E 'docmost|homebox|mealie|recipe|sparky' || echo 'no volumes of the five'; "
+ "; ".join(f"echo {a}: $(ls -A /opt/docker/stacks/{a})" for a in APPS)
+ f"; docker inspect -f '{{{{.RepoDigests}}}} {{{{.Created}}}}' {IMG}")
txt = f"deployed (dashboard API): {dep}\n{out}"
os.makedirs(EV, exist_ok=True)
open(f"{EV}/baseline-9202.txt", "w").write(txt + "\n")
print(txt)
def deploy(app, sub):
b.login()
ok = b.deploy(app, sub)
if b.GENERATED.get(app):
p = f"{SC}/gen-{app}.json"
fd = os.open(p, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
os.write(fd, json.dumps(b.GENERATED[app]).encode()); os.close(fd)
log(app, f"generated deploy secrets kept in scratch (keys {list(b.GENERATED[app])})")
log(app, f"deploy {app} -> {ok}")
if ok:
log(app, f"app answers: {b.wait_app(sub)}")
def shoot(app, sub, script, extra):
b.login()
sess = open(f"{SC}/sess{os.getpid()}.txt").read().strip().split("=", 1)[1]
sec = dict(secfile(app))
gp = f"{SC}/gen-{app}.json"
if os.path.exists(gp):
sec["gen"] = json.load(open(gp))
sec["session"] = sess
for kv in extra:
k, v = kv.split("=", 1); sec[k] = v
js = open(script).read()
gdir = f"/root/wr-shots/{app}"
payload = base64.b64encode(json.dumps(sec).encode()).decode()
jsb = base64.b64encode(js.encode()).decode()
cmd = f"""set -e
umask 077; mkdir -p {gdir}; chmod 700 {gdir}
echo {payload} | base64 -d > {gdir}/secrets.json; chmod 600 {gdir}/secrets.json
echo {jsb} | base64 -d > {gdir}/shot.js
set +e
timeout 600 docker run --rm --network traefik-public --user root -e PUPPETEER_CACHE_DIR=/home/pptruser/.cache/puppeteer \
-e NODE_PATH=/home/pptruser/node_modules -e DOMAIN=enkisfelhom.hu -e SUB={sub} -e TRAEFIK={TRAEFIK} -e STOP=${{STOP:-}} \
-v {gdir}:/out {IMG} node /out/shot.js 2>&1 | sed -E "s/[A-Za-z0-9_.+/=-]{{28,}}/<long-value-redacted>/g" | tail -40
rc=${{PIPESTATUS[0]}}
shred -u {gdir}/secrets.json
echo "chrome rc=$rc"; ls -la {gdir}
"""
out = b.guest(cmd, timeout=900)
log(app, out)
def fetch(app, gpath, lpath):
out = b.guest(f"base64 -w0 {gpath}")
data = base64.b64decode(out.strip().split("\n")[0])
os.makedirs(os.path.dirname(lpath), exist_ok=True)
open(lpath, "wb").write(data)
log(app, f"fetched {gpath} -> {lpath} ({len(data)} B)")
def remove(app):
b.login()
b.remove(app)
verify(app)
def verify(app):
b.login()
st = b.stack(app)
out = b.guest(f"echo containers: $(docker ps -a --format '{{{{.Names}}}}' | grep -i '{app.split('-')[0]}' || echo none); "
f"echo volumes: $(docker volume ls --format '{{{{.Name}}}}' | grep -i '{app.split('-')[0]}' || echo none); "
f"echo networks: $(docker network ls --format '{{{{.Name}}}}' | grep -i '{app.split('-')[0]}' || echo none); "
f"echo stackdir: $(ls -A /opt/docker/stacks/{app}); "
f"echo drive: $(ls -d /mnt/felhom-drives/scratch_hdd/userdata/{app} 2>/dev/null || echo none)")
log(app, f"VERIFY {app}: deployed={st.get('deployed')} state={st.get('state')}\n{out}")
if __name__ == "__main__":
a = sys.argv[1:]
{"baseline": lambda: baseline(), "deploy": lambda: deploy(a[1], a[2]),
"shoot": lambda: shoot(a[1], a[2], a[3], a[4:]), "fetch": lambda: fetch(a[1], a[2], a[3]),
"remove": lambda: remove(a[1]), "verify": lambda: verify(a[1])}[a[0]]()