Files
felhom.eu/documentation/audits/SPIKE-cc-creds-2026-10-17/probes/pre.py
T
adminandClaude Opus 5.5 b9c4f73bfa
gates / gates (push) Failing after 6m36s
SPIKE cc-creds: secret broker + CC transcript guards measured with canaries (R-1118..R-1122, 168 -> 173)
Mask PASS, PreToolUse refuse PASS, PostToolUse replace PARTIAL (crash and
large-output disk leaks, both with a measured fix), SOPS+age store PARTIAL
(private repo could not be created), transcript sweep PASS. Fake secrets
only; ~/.config/credentials not read; no CC settings changed. Files use
"creds": .gitignore *secret* ignores the brief's names. STATUS D10: the
store choice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
2026-10-17 11:57:24 +02:00

39 lines
2.0 KiB
Python

#!/usr/bin/env python3
"""SPIKE probe — PreToolUse(Bash) refuse. stdin: hook JSON. Denies the known leak shapes with a reason
that names the safe form. Logs one line per decision to evidence/q2/pre-hook.log (rule name only).
Unknown input shape -> exit 1 (a crash), so `onFailure: block` decides, never a silent allow."""
import json, os, re, sys, time
RULES = [
("source-secret-file",
re.compile(r"(^|[;&|(]\s*|\s)(source|\.)\s+\S*(credentials|secret|canary|\.env)\b", re.I),
"Sourcing a secrets file prints values (bash echoes un-assignable lines). Use: sec run --env NAME -- <cmd>"),
("dump-environment",
re.compile(r"(^|[;&|(]\s*)\s*(printenv|env|export\s+-p|declare\s+-[px]+|set)\s*($|[;&|>)])"),
"Dumping the environment prints every secret in it. Print one NAME only: printenv NAME | wc -c"),
("curl-url-format-var",
re.compile(r"\bcurl\b.*%\{(redirect_url|url_effective|referer|url)\}"),
"curl -w '%{redirect_url}' re-prints -u/--netrc credentials inside the URL. Use -w '%{http_code}' and read Location from -D-"),
("kubectl-read-secret",
re.compile(r"\bkubectl\b.*\bget\s+(secrets?|sec)\b(?!.*--show-managed-fields-only)"),
"kubectl get secret prints base64 values. Use: kubectl get secret NAME -o jsonpath='{.data}' | jq 'keys' (names only)"),
]
def log(rule, decision):
d = os.path.join(os.path.dirname(os.path.abspath(__file__)), "evidence", "q2")
os.makedirs(d, exist_ok=True)
with open(os.path.join(d, "pre-hook.log"), "a") as fh:
fh.write("%s %s %s\n" % (time.strftime("%H:%M:%S"), decision, rule))
ev = json.load(sys.stdin)
cmd = ev["tool_input"]["command"] # KeyError = crash = onFailure decides
for name, rx, reason in RULES:
if rx.search(cmd):
log(name, "deny")
print(json.dumps({"hookSpecificOutput": {"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "SPIKE-GUARD[%s]: %s" % (name, reason)}}))
sys.exit(0)
log("-", "pass")
sys.exit(0)