diff --git a/documentation/audits/rehearsal-2026-10-20/B5-night/00-expected.txt b/documentation/audits/rehearsal-2026-10-20/B5-night/00-expected.txt index 5a481c96..66678b48 100644 --- a/documentation/audits/rehearsal-2026-10-20/B5-night/00-expected.txt +++ b/documentation/audits/rehearsal-2026-10-20/B5-night/00-expected.txt @@ -24,3 +24,7 @@ Baseline taken 20:59–21:09 UTC (before the night, read-only, through the apps' (tool `tools/snapshot.py`). The morning snapshot must equal it line for line. The four known Nextcloud gaps are from the day (the 1.5 GB upload that never finished, a symlink WebDAV cannot hold, the NFC/NFD pair Nextcloud folds into one name) and the four 0-byte `feltoltes_*.jpg` are R-1196. + +Added 21:39 UTC, before the night: a THIRD passive reader, `tools/logwatch.py` — every 10 min it copies the box's own +log ring (the „Hibakeresés" page's data: controller INFO+ and the host agent's lines) off the box, because a restart +erases it. Read-only, like the other two. The copies are scanned for secrets before they are committed. diff --git a/documentation/audits/rehearsal-2026-10-20/tools/logwatch.py b/documentation/audits/rehearsal-2026-10-20/tools/logwatch.py new file mode 100644 index 00000000..d1a10d9c --- /dev/null +++ b/documentation/audits/rehearsal-2026-10-20/tools/logwatch.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +"""logwatch.py OUTDIR — every 10 min, copy the box's own log ring (the „Hibakeresés" page's data: controller INFO+ and +the host agent's lines) and append the NEW lines to OUTDIR/controller.log and OUTDIR/agent.log. Read-only. A night +restart erases the ring; this keeps the night's evidence off the box (rule 5). Run under sec (dashboard password).""" +import json, os, sys, time +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))); import ctl +out = sys.argv[1]; os.makedirs(out, exist_ok=True) +seen = {"c": set(), "a": set()} +while True: + for key, path, fn in (("c", "/api/debug/logs?level=INFO", "controller.log"), ("a", "/api/debug/agent-logs", "agent.log")): + try: + c, d = ctl.call("GET", path, timeout=60) + new = 0 + with open(os.path.join(out, fn), "a") as f: + for e in (d.get("data") or {}).get("entries") or []: + k = (e.get("timestamp"), e.get("message")) + if k not in seen[key]: + seen[key].add(k); new += 1 + f.write(f"{e.get('timestamp')} {e.get('level')} {e.get('message')}\n") + print(time.strftime("%H:%M:%SZ", time.gmtime()), fn, "http", c, "new", new, flush=True) + except Exception as ex: # noqa: BLE001 + print(time.strftime("%H:%M:%SZ", time.gmtime()), fn, "NO ANSWER", type(ex).__name__, flush=True) + time.sleep(600)