Files
felhom.eu/documentation/audits/visitors-2026-10-01/tools/sparky_box.py
T

122 lines
9.6 KiB
Python

#!/usr/bin/env python3
"""sparky_box.py — SparkyFitness's checklist rows on scratch 9202 (live catalog, controller 0.286.1), through the product's
own endpoints. Strangers through traefik with no session and no gate cookie; the tunnel SIMULATED by a curl container at
172.16.253.2 (Part A's method). Secrets never printed. Env: SC, EV."""
import json, os, re, subprocess, sys, time
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
import box_walk as w
import upgrade_fixtures_box as fixtures
APP, SUB = "sparkyfitness", "sparky"
HOST = f"{SUB}.{w.DOMAIN}"; FX = fixtures.FIXTURES[APP]
log = open(f"{w.EV}/sparky-box.txt", "a", buffering=1)
def say(*a):
w.say(*a); log.write(" ".join(map(str, a)) + "\n")
def lan(path, *a):
r = subprocess.run(["curl", "-sk", "--max-time", "10", "-o", "/dev/null", "-w", "%{http_code}", "-H", f"Host: {HOST}"] + list(a) + [f"{w.BASE}{path}"],
capture_output=True, text=True)
return r.stdout.strip()
def tun(visitor, forged, path, data):
cmd = (f"docker run --rm --network felhom-tunnel --ip 172.16.253.2 curlimages/curl:8.11.1 -sk -o /dev/null -w '%{{http_code}}' "
f"'https://traefik{path}' -H 'Host: {HOST}' -H 'X-Forwarded-For: {forged}, {visitor}' -H 'CF-Connecting-IP: {visitor}' "
f"-H 'Origin: https://{HOST}' -H 'Content-Type: application/json' --data '{data}'")
return w.guest(cmd).strip()[-3:]
def state(): return w.stack(APP).get("state")
w.login()
say(f"##### SparkyFitness on 9202 — controller {w.guest('docker inspect felhom-controller --format {{.Config.Image}}').strip()} | catalog "
+ w.guest("cd /var/lib/docker/volumes/felhom-controller-data/_data/data/catalog-cache && git log --oneline -1").strip()[:9]
+ f" | {time.strftime('%FT%TZ', time.gmtime())}")
# 3.5 / 4.3 — a stranger polls from the press; the install timed
w.guest(f"""cat > /root/sp-poll.sh <<'PEOF'
out=/root/sp-poll.txt; : > $out; t0=$(date +%s)
while [ $(( $(date +%s) - t0 )) -lt 240 ]; do
r=$(curl -sk --max-time 4 -o /root/sp-poll.body -w '%{{http_code}}' -H 'Host: {HOST}' -H 'Content-Type: application/json' -X POST --data '{{"email":"s@x.hu","password":"Stranger-1234","name":"s"}}' https://127.0.0.1/api/auth/sign-up/email)
echo "$(date +%H:%M:%S) $r $(head -c 60 /root/sp-poll.body | tr '\\n' ' ')" >> $out; sleep 1
done
PEOF
(nohup sh /root/sp-poll.sh >/dev/null 2>&1 &)""")
t0 = time.time(); ok = w.deploy(APP, SUB); say(f"deploy -> {ok} ({round(time.time()-t0)} s to `deployed`)")
healthy = None
for _ in range(150):
hs = w.guest("for c in sparkyfitness sparkyfitness-server sparkyfitness-db; do docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}nohc{{end}}' $c; done").split()
if hs and all(h in ("healthy", "nohc") for h in hs): healthy = time.time() - t0; break
time.sleep(2)
say(f"4.3 every container healthy {round(healthy) if healthy else 'NEVER'} s after the press; restarts:",
w.guest("for c in sparkyfitness sparkyfitness-server sparkyfitness-db; do echo -n \"$c=$(docker inspect -f '{{.RestartCount}}' $c) \"; done").strip(),
"| state:", state())
say("3.3 before the setup, a stranger (LAN) on / , /api/auth/sign-up/email:", lan("/", "-H", "Accept: text/html"), lan("/api/auth/sign-up/email", "-X", "POST", "-H", "Content-Type: application/json", "--data", "{}"))
tok = FX.seed(w, SUB, say) # 3.1: the household through the gate, the first account
say("3.1 the household's first account through the gate ->", "ok" if tok else "FAILED " + str(getattr(FX, "tried", "")))
time.sleep(3); w.guest("pkill -f sp-poll.sh || true")
poll = w.guest("cat /root/sp-poll.txt").strip().splitlines(); codes = {}
for l in poll: codes[l.split(" ")[1]] = codes.get(l.split(" ")[1], 0) + 1
say(f"3.5 a stranger's {len(poll)} sign-up tries (1/s) from the press until the household's account: codes {codes}; any 200: {any(' 200 ' in l for l in poll)}")
# the household's 'Done, I set it up' (no probe in the template)
csrf = open(f"{w.SC}/csrf{os.getpid()}.txt").read().strip(); sess = open(f"{w.SC}/sess{os.getpid()}.txt").read().strip()
r = subprocess.run(["curl", "-sk", "-o", "/dev/null", "-w", "%{http_code}", "-H", w.HOSTHDR, "-H", f"Cookie: {sess}", "-H", f"X-CSRF-Token: {csrf}",
"-X", "POST", f"{w.BASE}/apps/{APP}/setup-gate/open"], capture_output=True, text=True)
say("the household presses 'Done, I set it up' ->", r.stdout)
for _ in range(40):
if "native_lock: applied" in w.guest(f"grep -A8 '^setup_gate:' /opt/docker/stacks/{APP}/app.yaml"): break
time.sleep(5)
say("3.4 lock record:", " | ".join(x.strip() for x in w.guest(f"grep -A8 '^setup_gate:' /opt/docker/stacks/{APP}/app.yaml").splitlines()),
"| app env SPARKY_FITNESS_DISABLE_SIGNUP =", w.guest("docker exec sparkyfitness-server printenv SPARKY_FITNESS_DISABLE_SIGNUP").strip())
w.wait_app(SUB, "/", want=("200",), tries=40)
body = json.dumps({"email": "stranger@example.com", "password": "Stranger-pass-1234", "name": "S"})
for p in ("/api/auth/sign-up/email", "/API/Auth/Sign-Up/email", "//api//auth/sign-up/email"):
say(f"3.4 a stranger signs up at {p} (LAN) ->", lan(p, "-X", "POST", "-H", "Content-Type: application/json", "-H", f"Origin: https://{HOST}", "--data", body))
say("3.4 the same straight at the server (behind the block — the app's own switch):",
w.guest("docker run --rm --network traefik-public curlimages/curl:8.11.1 -s -o /dev/null -w '%{http_code}' http://sparkyfitness-server:3010/api/auth/sign-up/email "
f"-H 'Content-Type: application/json' -H 'Origin: https://{HOST}' --data '{body}' || true").strip()[-3:])
say("3.9 browser-shaped sign-in (Origin, JSON) right / wrong:", FX._signin(w, SUB, tok["email"], tok["pw"])[0], FX._signin(w, SUB, tok["email"], "wrong-x")[0])
# 3.6 — lock-out through the simulated tunnel
say("3.6 waiting 20 s (better-auth's own window)"); time.sleep(20)
seq = []
for i in range(1, 9):
seq.append(tun("198.51.100.66", f"10.6.0.{i}", "/api/auth/sign-in/email", json.dumps({"email": tok["email"], "password": f"wrong-{i}"})))
say(f"3.6 stranger 198.51.100.66, 8 wrong sign-ins for the household's e-mail, a new forged leftmost each: {seq}")
t1 = time.time(); hh = []
for _ in range(20):
c = tun("203.0.113.10", "203.0.113.10", "/api/auth/sign-in/email", json.dumps({"email": tok["email"], "password": tok["pw"]}))
hh.append((round(time.time() - t1), c))
if c == "200": break
time.sleep(3)
say(f"3.6 the household from 203.0.113.10, right password, from the stranger's last try on (s, code): {hh}")
# 1.5 / 1.8 / 0.4
say("1.5 processes:", w.guest("for c in sparkyfitness-server sparkyfitness; do echo -n \"$c: \"; docker exec $c sh -c 'cat /proc/1/cmdline | tr \"\\000\" \" \"'; echo; done").strip())
say("1.8 an unknown page ->", lan("/felhom-no-such-page-xyz/", "-H", "Accept: text/html"), "| an unknown API ->", lan("/api/felhom-no-such"))
say("0.4 outbound hosts named in the server's first-start log:", sorted(set(re.findall(r"https?://([a-z0-9.-]+\.[a-z]{2,})", w.guest("docker logs sparkyfitness-server 2>&1 | head -400"))))[:12])
# 4.4 — negative control
w.guest("docker stop sparkyfitness-server >/dev/null"); seen = []
for i in range(9):
time.sleep(5); s = state()
if not seen or seen[-1][1] != s: seen.append((5 * (i + 1), s))
say(f"4.4 sparkyfitness-server STOPPED — states (s, state): {seen}; the front door / -> {lan('/')} ; /api/health -> {lan('/api/health')}")
w.guest("docker start sparkyfitness-server >/dev/null")
for i in range(40):
time.sleep(5)
if state() == "running": say(f" running again {5*(i+1)} s after docker start"); break
# 2.7 size
say("2.7 volumes after the seed:", w.guest("for v in $(docker volume ls -q | grep sparkyfitness); do echo -n \"$v=$(du -sh /var/lib/docker/volumes/$v/_data | cut -f1) \"; done").strip())
# 2.5 backup → remove keeping backups → restore → read back
code, d = w.ctl("POST", "/api/debug/backup/night-chain"); say("2.5 the night chain (debug action) ->", code)
for _ in range(60):
if "finished in" in w.guest("docker logs --since 3m felhom-controller 2>&1 | grep 'night-chain\\] finished' | tail -1"): break
time.sleep(5)
say(" restore points:", [(s.get("id") or s.get("snapshot_id"), s.get("time") or s.get("created")) for s in w.snapshots(APP)][:2])
w.ctl("POST", f"/api/stacks/{APP}/stop"); time.sleep(10)
code, d = w.ctl("POST", f"/api/stacks/{APP}/remove", {"remove_hdd_data": False, "remove_backups": False})
say(f"2.6 remove KEEPING backups -> {code} {str(d)[:220]}")
time.sleep(6); say(" left after:", w.guest(f"docker ps -a --format '{{{{.Names}}}}' | grep -E '^{APP}'; docker volume ls -q | grep -i {APP}").strip() or "nothing")
r = w.restore(APP); say("2.5 restore:", {k: r.get(k) for k in ("ok", "snapshot_id", "seconds", "state_after", "hold_after")})
w.wait_app(SUB, "/", want=("200",), tries=60)
say("2.5 the seed reads back after remove + restore:", FX.verify(w, SUB, tok, say))
say(" R-773 on a restored sparkyfitness: record", " | ".join(x.strip() for x in w.guest(f"grep -A8 '^setup_gate:' /opt/docker/stacks/{APP}/app.yaml").splitlines()[:7]),
"| a stranger signs up ->", lan("/api/auth/sign-up/email", "-X", "POST", "-H", "Content-Type: application/json", "-H", f"Origin: https://{HOST}", "--data", body))
# 2.6 remove with data
w.ctl("POST", f"/api/stacks/{APP}/stop"); time.sleep(10)
code, d = w.ctl("POST", f"/api/stacks/{APP}/remove", {"remove_hdd_data": True, "remove_backups": True})
say(f"2.6 remove WITH data and backups -> {code} {str(d)[:220]}")
time.sleep(6); say(" left after:", w.guest(f"ls -d /opt/docker/stacks/{APP} 2>/dev/null; docker ps -a --format '{{{{.Names}}}}' | grep -E '^{APP}'; docker volume ls -q | grep -i {APP}").strip() or "nothing")
w.guest("rm -f /root/sp-poll.sh /root/sp-poll.txt /root/sp-poll.body")