"""Local web UI: pick a pack, fill parameters, walk the steps, record evidence. Localhost only. Server-rendered forms; the only script is small: copy button, fail-comment warning, scroll to current step. """ from __future__ import annotations import html import secrets from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from urllib.parse import parse_qs, urlparse from . import pack as P, run as R CSS = """body{font:15px/1.5 system-ui,sans-serif;max-width:52rem;margin:2rem auto;padding:0 1rem;color:#111;background:#fff} @media(prefers-color-scheme:dark){body{color:#eee;background:#111}pre,input,select{background:#1b1b1b;color:#eee}} h1,h2{font-weight:600}a{color:inherit}hr{border:0;border-top:1px solid #8886} .eyebrow{font:12px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.06em} .step{border:1px solid #8886;padding:.8rem 1rem;margin:.8rem 0}.step.running{border-color:currentColor} .pill{font:12px ui-monospace,monospace;border:1px solid #8886;padding:0 .4rem} pre{border:1px solid #8886;padding:.5rem;overflow:auto;margin:.4rem 0} button,input,select{font:inherit;padding:.25rem .5rem}.row{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}.grow{flex:1;min-width:12rem}[hidden]{display:none}#current{scroll-margin-top:1rem}.note{opacity:.75}.bad{font-weight:600}""" def page(title: str, body: str) -> bytes: return (f"" f"{html.escape(title)}" f"

runbook-tutorials

{body}" "").encode() def e(s) -> str: return html.escape(str(s)) class App: def __init__(self, roots: list[Path]): self.token = secrets.token_urlsafe(16) self.packs = [P.load(p) for r in roots for p in P.discover(r)] def hidden(self) -> str: return f"" def index(self) -> bytes: rows = [] for i, p in enumerate(self.packs): d = p.data if not p.ok: rows.append(f"
  • {e(p.id)}: invalid ({e(p.errors[0])})
  • ") elif d.get("spec") != "runbook-pack/v0.1" and d["engine"] == "native": rows.append(f"
  • {e(d['title'])} {e(d.get('spec'))} " "cannot be run yet (RBT-WP-0007)
  • ") elif d["engine"] != "native": rows.append(f"
  • {e(d['title'])} legacy console " f"start with: python -m rtut launch {e(p.path)}
  • ") else: rows.append(f"
  • {e(d['title'])} " f"{e(d['exercise_status'])}
  • ") return page("Runbooks", f"

    Runbooks

    ") def pack_page(self, i: int) -> bytes: p = self.packs[i]; d = p.data prereq = "".join(f"
  • [{e(x['owner'])}] {e(x['text'])}
  • " for x in d.get("prerequisites", [])) fields = "".join( f"


    {e(x.get('help', ''))} Never enter secrets.

    " for x in d.get("parameters", [])) return page(d["title"], f"

    {e(d['title'])}

    {e(d['exercise_status'])}

    " f"

    {e(d.get('outcome', ''))}

    Prerequisites

    " f"
    {self.hidden()}{fields}" "

    " "
    ") def run_page(self, i: int, rid: str, msg: str = "") -> bytes: p = self.packs[i]; r = R.load(p, rid); d = p.data out = [f"

    {e(d['title'])}

    {e(d['exercise_status'])}

    " f"

    run {e(rid)} ยท operator {e(r.data['operator'])}

    "] if msg: out.append(f"

    {e(msg)}

    ") blocked = False waiting_for = "" total = len(d["steps"]) done_n = sum(1 for x in d["steps"] if r.state(x["id"])["state"] in ("done", "skipped")) out.append(f"

    {done_n} of {total} steps done

    " f"") current = next((x["id"] for x in d["steps"] if r.state(x["id"])["state"] == "running"), None) if current is None: current = next((x["id"] for x in d["steps"] if r.state(x["id"])["state"] in ("todo", "failed")), None) for n, s in enumerate(d["steps"], 1): st = r.state(s["id"]); state = st["state"] act = "" cmd = R.command_for(r, s["id"]) block = "" if cmd and state == "running": # the command and Copy appear only once the step is started block = f"
    {e(cmd)}
    " elif cmd and state == "done": block = f"
    Command
    {e(cmd)}
    " url = f"/run/{i}/{e(rid)}/{e(s['id'])}" v = st.get("verification") if state == "todo" and not blocked: act = f"
    {self.hidden()}
    " elif state == "todo": act = f"

    Waiting for: {e(waiting_for)}

    " elif state == "running": ver = s["verify"] if ver.get("expect", "manual") == "manual": act = (f"
    {self.hidden()}" f"

    Is this true? {e(ver['done_when'])}

    " "
    ") else: act = (f"
    {self.hidden()}" f"

    Runs: {e(R.render(ver['command'], r.data['params'], p))}

    " f"
    ") if not v: act = "

    1. Run the command, then check  โ†’  2. Mark done to lock it in

    " + act elif v["passed"]: act += "

    โœ” Check passed. Press Mark done to lock it in; the next step opens after that.

    " else: act += "

    โœ˜ Check not passed. Fix the cause, then check again, or mark the step failed.

    " ev = "" if v and v["passed"]: ev = "".join(f" " for x in s.get("evidence", [])) ev = f"

    {ev}

    " if ev else "" done_btn = f" " if v and v["passed"] else "" act += (f"
    {self.hidden()}{ev}

    {done_btn}" f" " "" "

    ") elif state == "failed": act = f"

    Failed: {e(st.get('note') or '(no comment)')}

    {self.hidden()}
    " elif state == "done" and st.get("note"): act = f"

    Comment: {e(st['note'])}

    " if state not in ("done", "skipped") and not blocked: blocked = True waiting_for = f"step {n}, {s['title']}" rb = f"

    Rollback: {e(s['rollback'])}

    " if s.get("rollback") and state in ("running", "failed") else "" anchor = " id=current" if s["id"] == current else "" out.append(f"{state} {n}. {e(s['title'])} " f"[{e(s['owner'])}] {e(s.get('risk', 'read-only'))}{block}" f"

    Done when: {e(s['verify']['done_when'])}

    {rb}{act}") if r.complete: out.append("

    All steps done. To record this run as exercising the pack, review and apply: " f"python -m rtut run finish {e(p.path)} {e(rid)} --apply

    ") out.append("

    Threat checks

    ") return page(d["title"], "".join(out)) def make_handler(app: App, port_holder: list): class H(BaseHTTPRequestHandler): def log_message(self, *a): # quiet pass def _send(self, body: bytes, code=200): self.send_response(code) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; form-action 'self'") self.end_headers(); self.wfile.write(body) def _host_ok(self) -> bool: return (self.headers.get("Host", "").split(":")[0] in ("127.0.0.1", "localhost")) def do_GET(self): if not self._host_ok(): return self._send(b"forbidden", 403) parts = [x for x in urlparse(self.path).path.split("/") if x] try: if not parts: return self._send(app.index()) if parts[0] == "pack": return self._send(app.pack_page(int(parts[1]))) if parts[0] == "run": return self._send(app.run_page(int(parts[1]), parts[2])) except (IndexError, ValueError, R.RunError): pass self._send(page("Not found", "

    Not found.

    "), 404) def do_POST(self): if not self._host_ok(): return self._send(b"forbidden", 403) n = int(self.headers.get("Content-Length", 0)) form = {k: v[0] for k, v in parse_qs(self.rfile.read(n).decode()).items()} if form.get("_t") != app.token: return self._send(b"bad token", 403) parts = [x for x in urlparse(self.path).path.split("/") if x] msg = "" try: if parts[:2] == ["run", "new"]: i = int(parts[2]); p = app.packs[i] params = {k[2:]: v for k, v in form.items() if k.startswith("p_")} r = R.create(p, form.get("operator", ""), params) return self._redirect(f"/run/{i}/{r.data['run_id']}#current") i, rid, sid, action = int(parts[1]), parts[2], parts[3], parts[4] p = app.packs[i]; r = R.load(p, rid) if action == "start": R.start(r, sid) elif action == "verify": R.verify(r, sid, confirm=bool(form.get("yes")), manual_ok=(form.get("ok") == "1") if not form.get("yes") else None) elif action == "done": R.complete(r, sid, {k[2:]: v for k, v in form.items() if k.startswith("e_")}, form.get("note", "")) elif action == "fail": R.fail(r, sid, form.get("note", "")) elif action == "retry": R.retry(r, sid) return self._redirect(f"/run/{i}/{rid}#current") except (R.RunError, KeyError, ValueError, IndexError) as ex: msg = str(ex) try: if parts[:2] == ["run", "new"]: return self._send(app.pack_page(int(parts[2])).replace(b"

    ", f"

    {e(msg)}

    ".encode(), 1), 400) return self._send(app.run_page(int(parts[1]), parts[2], msg), 400) except Exception: return self._send(page("Error", f"

    {e(msg)}

    "), 400) def _redirect(self, to: str): self.send_response(303); self.send_header("Location", to); self.end_headers() return H def serve(roots: list[Path], port: int = 8877) -> ThreadingHTTPServer: app = App(roots) srv = ThreadingHTTPServer(("127.0.0.1", port), make_handler(app, [])) srv.app = app # type: ignore[attr-defined] return srv