153 lines
5.7 KiB
Python
153 lines
5.7 KiB
Python
"""Anchor script: exit-code contract for tools/ledger_anchor_check.py.
|
|
|
|
Runs the script as a subprocess against throwaway SQLite chains
|
|
in a TemporaryDirectory (deleted with the test). Pins only what
|
|
the operator depends on: 0 pass, 1 verification failure with the
|
|
failure named, 2 usage error, and --write-anchor refused on a
|
|
failing chain. The script itself never writes the chain; the
|
|
tamper/tail cases simulate privileged tampering by dropping the
|
|
SQLite append-only triggers first (a normal UPDATE is already
|
|
blocked by the trigger, which is itself asserted here).
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
SCRIPT = REPO_ROOT / "tools" / "ledger_anchor_check.py"
|
|
|
|
try:
|
|
from cloud.audit_wiring import record_audit_verdict
|
|
from cloud.ledger import LedgerStore
|
|
except ImportError:
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
from cloud.audit_wiring import record_audit_verdict
|
|
from cloud.ledger import LedgerStore
|
|
|
|
|
|
def _run(*args):
|
|
proc = subprocess.run(
|
|
[sys.executable, str(SCRIPT), *args],
|
|
capture_output=True, text=True, timeout=60)
|
|
try:
|
|
report = json.loads(proc.stdout) if proc.stdout.strip() else None
|
|
except ValueError:
|
|
report = None
|
|
return proc.returncode, report, proc.stderr
|
|
|
|
|
|
class LedgerAnchorCheckTestCase(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.db_path = os.path.join(self.tmp.name, "control.db")
|
|
store = LedgerStore(self.db_path)
|
|
record_audit_verdict(store, "t1", "p1", "apps/web", "v2",
|
|
"fail", "audit", failures=["ci_green"])
|
|
record_audit_verdict(store, "t1", "p1", "apps/web", "v2",
|
|
"pass", "audit")
|
|
head = store.export()["head_hash"]
|
|
store.close()
|
|
self.anchor_path = os.path.join(self.tmp.name, "anchor.json")
|
|
with open(self.anchor_path, "w", encoding="utf-8") as handle:
|
|
json.dump({"version": 1, "head_hash": head, "count": 2},
|
|
handle)
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def _copy_db(self, name):
|
|
import shutil
|
|
path = os.path.join(self.tmp.name, name)
|
|
shutil.copyfile(self.db_path, path)
|
|
return path
|
|
|
|
def test_pass_without_and_with_anchor(self):
|
|
code, report, _ = _run("--db", self.db_path)
|
|
self.assertEqual(code, 0)
|
|
self.assertTrue(report["ok"])
|
|
code, report, _ = _run("--db", self.db_path,
|
|
"--anchor", self.anchor_path)
|
|
self.assertEqual(code, 0)
|
|
self.assertTrue(report["ok"])
|
|
self.assertEqual(report["head_advanced_by"], 0)
|
|
|
|
def test_normal_update_blocked_by_trigger_then_privileged_tamper_fails(self):
|
|
path = self._copy_db("tamper.db")
|
|
conn = sqlite3.connect(path)
|
|
with self.assertRaises(sqlite3.IntegrityError):
|
|
conn.execute(
|
|
"UPDATE audit_ledger SET detail='x' WHERE id=1")
|
|
conn.rollback()
|
|
conn.execute("DROP TRIGGER audit_ledger_no_update")
|
|
conn.execute("UPDATE audit_ledger SET detail='tampered'"
|
|
" WHERE id=1")
|
|
conn.commit()
|
|
conn.close()
|
|
code, report, _ = _run("--db", path,
|
|
"--anchor", self.anchor_path)
|
|
self.assertEqual(code, 1)
|
|
self.assertFalse(report["ok"])
|
|
self.assertTrue(any("entry_hash mismatch" in f
|
|
for f in report["failures"]))
|
|
|
|
def test_tail_deletion_fails_against_old_anchor(self):
|
|
path = self._copy_db("tail.db")
|
|
conn = sqlite3.connect(path)
|
|
conn.execute("DROP TRIGGER audit_ledger_no_delete")
|
|
conn.execute("DELETE FROM audit_ledger WHERE id=2")
|
|
conn.commit()
|
|
conn.close()
|
|
code, report, _ = _run("--db", path,
|
|
"--anchor", self.anchor_path)
|
|
self.assertEqual(code, 1)
|
|
self.assertFalse(report["ok"])
|
|
self.assertTrue(any("shorter than anchor" in f
|
|
for f in report["failures"]))
|
|
|
|
def test_extended_chain_still_passes_anchor(self):
|
|
store = LedgerStore(self.db_path)
|
|
record_audit_verdict(store, "t1", "p1", "apps/web", "v2",
|
|
"pass", "audit")
|
|
store.close()
|
|
code, report, _ = _run("--db", self.db_path,
|
|
"--anchor", self.anchor_path)
|
|
self.assertEqual(code, 0)
|
|
self.assertTrue(report["ok"])
|
|
self.assertEqual(report["head_advanced_by"], 1)
|
|
|
|
def test_write_anchor_refused_on_failing_export(self):
|
|
path = self._copy_db("tail2.db")
|
|
conn = sqlite3.connect(path)
|
|
conn.execute("DROP TRIGGER audit_ledger_no_delete")
|
|
conn.execute("DELETE FROM audit_ledger WHERE id=2")
|
|
conn.commit()
|
|
conn.close()
|
|
store = LedgerStore(path)
|
|
export_path = os.path.join(self.tmp.name, "export.json")
|
|
with open(export_path, "w", encoding="utf-8") as handle:
|
|
json.dump(store.export(), handle)
|
|
store.close()
|
|
out_path = os.path.join(self.tmp.name, "new-anchor.json")
|
|
code, _report, _ = _run("--export", export_path,
|
|
"--anchor", self.anchor_path,
|
|
"--write-anchor", out_path)
|
|
self.assertEqual(code, 1)
|
|
self.assertFalse(os.path.exists(out_path))
|
|
|
|
def test_usage_errors_exit_2(self):
|
|
code, _report, _ = _run(
|
|
"--db", os.path.join(self.tmp.name, "nope.db"))
|
|
self.assertEqual(code, 2)
|
|
code, _report, _ = _run("--db", "postgres://host/db")
|
|
self.assertEqual(code, 2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|