diff --git a/bin/box-ctl.py b/bin/box-ctl.py index 840cd17..f027092 100755 --- a/bin/box-ctl.py +++ b/bin/box-ctl.py @@ -697,6 +697,21 @@ def act_relay_health(): out(all_healthy, relays=results, healthy=all_healthy) +def act_identity_audit(): + """Run identity-audit-check.sh and return drift as JSON.""" + audit("identity-audit") + script = BIN / "identity-audit-check.sh" + r = subprocess.run([str(script)], capture_output=True, text=True, timeout=60) + if r.returncode == 2: + fail("AUDIT_UNAVAILABLE", (r.stderr or r.stdout).strip()[:500]) + drift = [] + for line in r.stdout.strip().split("\n"): + if line.startswith("DRIFT: "): + drift.append(line[len("DRIFT: "):]) + clean = r.returncode == 0 + out(clean, drift=drift, clean=clean) + + def act_cdp_latency(): """Run cdp-latency-check.sh and return per-node latency JSON.""" @@ -1131,6 +1146,7 @@ fleet: watchdog-alerts relay-health cdp-latency + identity-audit VM identity audit drift check timer actions: timer-list @@ -1363,6 +1379,10 @@ def main(argv): act_watchdog_alerts() elif action == "relay-health": act_relay_health() + elif action == "identity-audit": + if rest: + fail("BAD_ARGS", "usage: identity-audit") + act_identity_audit() elif action == "cdp-latency": act_cdp_latency() elif action == "chrome-errors": diff --git a/bin/identity-audit-check.sh b/bin/identity-audit-check.sh new file mode 100755 index 0000000..141971c --- /dev/null +++ b/bin/identity-audit-check.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# identity-audit-check.sh - Check VM identity audit for drift +# +# Lives on bl (/home/super/Projects/NetVM/bin/). Reads a bl-local cached +# copy of the VM's identity audit JSON and reports drift. +# +# Why a cache: bl cannot SSH to the VM (VM only accepts the operator +# container's key). The VM's hourly audit cron (/srv/board/bin/identity-audit.py) +# should scp /srv/board/data/identity-audit.json to bl at: +# /home/super/Projects/NetVM/var/identity-audit.json +# after each run. Until that push is wired, the cache is refreshed manually +# or by the identity-audit-watch cron. +# +# Called by: +# - the identity-audit-watch cron (replaces inline SSH one-liner) +# - box-ctl.py `identity-audit` action +# +# Exit codes: +# 0 - clean (no drift; warnings are expected and silent) +# 1 - drift detected (items printed to stdout, one per line) +# 2 - audit cache missing/unreadable (needs attention) +# +# Output on drift: one drift item per line, prefixed with "DRIFT: " + +set -u + +CACHE_PATH="/home/super/Projects/NetVM/var/identity-audit.json" + +# Allow override for testing +if [ $# -ge 1 ] && [ -f "$1" ]; then + CACHE_PATH="$1" +fi + +if [ ! -f "$CACHE_PATH" ]; then + echo "ERROR: identity audit cache missing: $CACHE_PATH" >&2 + echo "The VM hourly audit should push /srv/board/data/identity-audit.json here after each run." >&2 + exit 2 +fi + +audit_json=$(cat "$CACHE_PATH" 2>/dev/null) +if [ -z "$audit_json" ]; then + echo "ERROR: identity audit cache unreadable: $CACHE_PATH" >&2 + exit 2 +fi + +# Parse with python3 +drift_output=$(printf '%s' "$audit_json" | python3 -c " +import json, sys +try: + data = json.load(sys.stdin) +except Exception as e: + print('ERROR: invalid JSON in audit cache: %s' % e, file=sys.stderr) + sys.exit(2) +for item in data.get('drift', []): + print('DRIFT: ' + str(item)) +" 2>&1) +parse_rc=$? + +if [ $parse_rc -eq 2 ]; then + echo "$drift_output" >&2 + exit 2 +fi + +if [ -n "$drift_output" ]; then + echo "$drift_output" + exit 1 +fi + +# Clean: no drift. Warnings are expected (agents not dialed in) — stay silent. +exit 0