Files
felhom.eu/documentation/audits/night-fixes-2026-10-26/tools/r1187g_live.py
T
admin ae3db89ab3
gates / gates (push) Successful in 1m56s
night fixes 2026-10-26: live proofs (R-1220, R-1232, R-1187, R-1298), register states, design lines; R-1300 filed; R-528 design
Evidence audits/night-fixes-2026-10-26/ (scanned: 0 secret hits).
2026-10-26 22:17:36 +01:00

101 lines
5.2 KiB
Python

#!/usr/bin/env python3
"""r1187g_live.py — R-1187 (g) live on scratch guest 9202: a PostGIS app's database survives a `.fab` export → import.
A THROWAWAY AdventureLog (postgis/postgis). A superuser is made with the app's own `createsuperuser`; the app is exported
and imported through the endpoints the UI invokes; then the user is read back through the app's FRONT DOOR — the
Django admin login page — and through its own ORM. Before the fix the import did not recognise postgis as a database,
so the dump was never loaded. Teardown through the product. The throwaway password lives only in this process.
usage: SC=<0600 dir with .ctlpw> SWEEP_EV=<evidence dir> TARGET=9202 DRIVE_ROOT=/mnt/felhom-drives/scratch_hdd python3 -u r1187g_live.py
"""
import os
import re
import secrets
import signal
import urllib.parse
import sys
import time
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../catalog-sweep-2026-10-24/tools"))
import sweep as sw # noqa: E402
signal.signal(signal.SIGTERM, lambda *a: (_ for _ in ()).throw(SystemExit("SIGTERM — cleaning up")))
w = sw.w
APP, SUB, DRIVE = "adventurelog", "advd53", os.environ.get("DRIVE_ROOT", "/mnt/felhom-drives/scratch_hdd")
say = print
def g(cmd):
return sw.g(cmd).strip()
def job(path):
for _ in range(180):
time.sleep(5)
code, d = w.ctl("GET", path)
if isinstance(d, dict) and d.get("done"):
return d
return {"timeout": True}
def orm_has(user):
out = g(f"docker exec adventurelog python manage.py shell -c "
f"\"from django.contrib.auth import get_user_model; print('ANS=' + str(get_user_model().objects.filter(username='{user}').exists()))\" 2>&1")
m = re.search(r"ANS=(True|False)", out)
return m.group(1) if m else "unreadable: " + out[-160:]
def admin_login(user, pw):
"""The front door, the household's route (box_walk.app_curl carries the setup gate): GET /admin/login/ for the
form and its csrftoken cookie, POST the form (the password on stdin, never an argument). Success = 302 to /admin/."""
rc, code, body = w.app_curl(SUB, "/admin/login/", "-D", "-")
tok = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', body)
ck = re.search(r"(?im)^set-cookie:\s*csrftoken=([^;]+)", body)
if not tok or not ck:
return f"no login form ({code})"
form = urllib.parse.urlencode({"csrfmiddlewaretoken": tok.group(1), "username": user, "password": pw, "next": "/admin/"})
rc, code, body = w.app_curl(SUB, "/admin/login/?next=/admin/", "-H", f"Cookie: csrftoken={ck.group(1)}",
"-H", "Content-Type: application/x-www-form-urlencoded",
"-H", f"Referer: https://{SUB}.{w.DOMAIN}/admin/login/", method="POST", data=form)
return code + (" (logged in)" if code == "302" else " (refused)")
def main():
w.say = say
w.login()
user, pw = "cc" + secrets.token_hex(3), "Cc-" + secrets.token_hex(12)
try:
ok = w.deploy(APP, SUB) and w.wait_app(SUB, "/", tries=120, delay=5)
say("1. adventurelog installed and answering:", ok)
out = g(f"docker exec -e DJANGO_SUPERUSER_PASSWORD='{pw}' adventurelog python manage.py createsuperuser --noinput "
f"--username {user} --email {user}@cc.invalid 2>&1 | tail -1")
say("2. createsuperuser:", out[:120], "| in the ORM:", orm_has(user))
say(" front door before:", admin_login(user, pw))
code, d = w.ctl("POST", "/api/export/start", {"stack_name": APP, "dest_drive": DRIVE, "stop_app": False})
if code != "200":
raise SystemExit(f"INSTRUMENT: export start refused {code} {d}")
j = job("/api/export/status")
say("3. export:", j.get("error") or "ok", j.get("output_size"), j.get("output_path"))
fab = j.get("output_path", "")
say(" the bundle's database:", g(f"tar -tzf {fab} | grep -E 'database/|manifest' | head -4"))
g("docker exec adventurelog python manage.py shell -c \"from django.contrib.auth import get_user_model; "
f"get_user_model().objects.filter(username='{user}').delete()\" >/dev/null 2>&1")
say("4. the user deleted in the live app (so only the import can bring it back) — in the ORM:", orm_has(user))
code, d = w.ctl("POST", "/api/export/import", {"path": fab})
if code != "200":
raise SystemExit(f"INSTRUMENT: import start refused {code} {d}")
j = job("/api/export/import/status")
say("5. import:", j.get("error") or "ok", "| steps:", [(s.get("label"), s.get("status")) for s in j.get("steps", [])])
say(" app answers:", w.wait_app(SUB, "/", tries=120, delay=5))
say(" R-1187 g — the user back in the ORM:", orm_has(user))
say(" R-1187 g — the front door after import:", admin_login(user, pw))
say(" the import's DB line:", g("docker logs --since 15m felhom-controller 2>&1 | grep -E 'restoring database|found DB service|DB dump imported|psql' | tail -3 | cut -c1-200"))
finally:
sw.remove(APP, keep=False, say=say)
say("6. leftovers:", sw.leftovers(APP, SUB))
say(" bundles removed:", g(f"rm -f {DRIVE}/exports/adventurelog*.fab; ls {DRIVE}/exports 2>/dev/null | grep -c adventurelog"))
if __name__ == "__main__":
main()