ac0ff8e2af
ci / build-test (push) Has been cancelled
- untrack CLAUDE.md (tailnet leak) + stale HANDOFF.md; keep local via .gitignore - add source-available LICENSE (attribution on fork, royalty on commercial use) - add lab/ reproduction kit (fake-cred LIVE-VM/mock rotation POCs) + lab/README - rewrite README to current status (22 drivers, 8 LIVE-VM/14 MOCK-ONLY/0 LIVE-REAL, 247 tests) and carry the credential-handling safety rules Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
95 lines
3.1 KiB
Python
95 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
# Drives `incredigo passwords ... commit --allow-csv` inside a real pty so the command
|
|
# sees a TTY and PAUSES at its "press Enter to shred" prompt. While the child is blocked,
|
|
# we inspect the tmpfs CSV it staged (location, header, contents), then release it and
|
|
# confirm the file was shredded. This is the only way to observe the otherwise-ephemeral
|
|
# tmpfs CSV, since headless commit shreds it immediately.
|
|
#
|
|
# argv: mgr export stage backup audit old1 old2 want_header pwcol
|
|
import os, pty, sys, select, re, subprocess, time
|
|
|
|
mgr, export, stage, backup, audit, old1, old2, want_header, pwcol = sys.argv[1:10]
|
|
pwcol = int(pwcol)
|
|
fail = 0
|
|
def out(m): print(" " + m, flush=True)
|
|
|
|
cmd = ["incredigo", "passwords", "--manager", mgr, "--export-path", export,
|
|
"--allow-csv", "commit", "--stage-in", stage, "--verified", "all",
|
|
"--backup-out", backup, "--audit-log", audit]
|
|
|
|
master, slave = pty.openpty()
|
|
proc = subprocess.Popen(cmd, stdin=slave, stdout=slave, stderr=slave, close_fds=True)
|
|
os.close(slave)
|
|
|
|
buf = b""
|
|
csv_path = None
|
|
deadline = time.time() + 30
|
|
while time.time() < deadline:
|
|
r, _, _ = select.select([master], [], [], 0.5)
|
|
if master in r:
|
|
try:
|
|
chunk = os.read(master, 4096)
|
|
except OSError:
|
|
break
|
|
if not chunk:
|
|
break
|
|
buf += chunk
|
|
m = re.search(rb'(/dev/shm/incredigo/incredigo-pw-[0-9]+\.csv)', buf)
|
|
if m and b'press Enter to shred' in buf:
|
|
csv_path = m.group(1).decode()
|
|
break
|
|
if proc.poll() is not None:
|
|
break
|
|
|
|
if not csv_path:
|
|
out("FAIL: no tmpfs CSV / shred prompt seen")
|
|
sys.stdout.write(buf.decode(errors="replace"))
|
|
sys.exit(1)
|
|
|
|
out("staged CSV: " + csv_path)
|
|
|
|
# (a) on tmpfs?
|
|
fst = subprocess.run(["stat", "-f", "-c", "%T", csv_path],
|
|
capture_output=True, text=True).stdout.strip()
|
|
if fst == "tmpfs":
|
|
out("OK: CSV is on tmpfs (%s)" % fst)
|
|
else:
|
|
out("FAIL: CSV not on tmpfs (%s)" % fst); fail = 1
|
|
|
|
# (b) correct per-browser header + (c) NEW pw present, OLD gone
|
|
with open(csv_path) as f:
|
|
lines = f.read().splitlines()
|
|
hdr = lines[0] if lines else ""
|
|
if hdr == want_header:
|
|
out("OK: header = " + hdr)
|
|
else:
|
|
out("FAIL: header=%s want=%s" % (hdr, want_header)); fail = 1
|
|
|
|
body = "\n".join(lines[1:])
|
|
for label, old in (("github", old1), ("gitlab", old2)):
|
|
if old in body:
|
|
out("FAIL: OLD %s pw present in CSV" % label); fail = 1
|
|
else:
|
|
out("OK: OLD %s pw absent from CSV" % label)
|
|
|
|
row1 = lines[1].split(",") if len(lines) > 1 else []
|
|
newpw = row1[pwcol - 1] if len(row1) >= pwcol else ""
|
|
if len(newpw) >= 16:
|
|
out("OK: row1 new pw is strong (len %d)" % len(newpw))
|
|
else:
|
|
out("FAIL: new pw too short (%d)" % len(newpw)); fail = 1
|
|
|
|
# (d) release the prompt -> shred
|
|
os.write(master, b"\n")
|
|
try:
|
|
proc.wait(timeout=15)
|
|
except subprocess.TimeoutExpired:
|
|
proc.kill(); out("FAIL: commit did not exit after release"); fail = 1
|
|
|
|
if not os.path.exists(csv_path):
|
|
out("OK: tmpfs CSV shredded (file gone)")
|
|
else:
|
|
out("FAIL: CSV still present after shred"); fail = 1
|
|
|
|
sys.exit(1 if fail else 0)
|