From 5bec4da37d78acbd46dfcfb9b48eb9d719950c34 Mon Sep 17 00:00:00 2001 From: SuperInstance Date: Mon, 28 Sep 2026 17:29:01 +0000 Subject: [PATCH] seed: fail-closed credential key-scan pre-commit (fleet-hooks, wave 53) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds .githooks/pre-commit + setup.sh from SuperInstance/fleet-hooks (byte-identical, commit bbc14cf). Makes the house practice — scanning staged files for credential-shaped strings before every push (wave-52 receipts) — self-enforcing at commit time. Arm once with: sh setup.sh (sets core.hooksPath .githooks, smoke-scans the index, refuses to arm a hooks path with no hook behind it). Scans staged INDEX blobs: ghp_/gho_/ghs_/ghu_ (36), github_pat_ (22+), sk-ant prefix, AKIA+16. Fail-closed (scan errors block, exit 2), never echoes matched content, FLEET_ALLOW= line-granular escape hatch for obviously- fake fixtures, no network, stdlib-only POSIX sh. 11/11 controls pass (tests live in SuperInstance/fleet-hooks tests/test_hooks.sh). --- .githooks/pre-commit | 122 +++++++++++++++++++++++++++++++++++++++++++ setup.sh | 43 +++++++++++++++ 2 files changed, 165 insertions(+) create mode 100755 .githooks/pre-commit create mode 100755 setup.sh diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..199165a --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,122 @@ +#!/bin/sh +# fleet pre-commit — fail-closed credential key-scan (fleet-hooks, wave 53) +# +# This productizes the fleet's house practice (wave-52 receipts): before every +# push, staged files were hand-scanned with `rg "ghp_|gho_|github_pat_"`. +# Now the repository enforces it at commit time. +# +# Design law: +# - scans STAGED content (the index — exactly what would be committed), +# not the worktree; a staged-then-deleted file cannot slip through +# - deletions: nothing new to leak, excluded; binary blobs: skipped +# (documented blind spot, receipted in tests as C4b) +# - .git/* and node_modules/* paths excluded defensively +# - NEVER echoes the matched line (the report must not leak the secret +# into terminals or CI logs) — reports class + file:line only +# - fail-closed: any scan error blocks the commit (exit 2) +# - no network, stdlib-only POSIX sh (sh, git, grep, awk, mktemp, wc, tr, sed) +# - this hook never needs your real token — never wire secrets into it +# +# Escape hatch: FLEET_ALLOW= — lines that ALSO match this regex are +# exempt (e.g. FLEET_ALLOW='FAKE' for obviously-fake fixtures). Per-invocation, +# visible in shell history/CI logs — deliberately loud. Use sparingly. +# +# Exit codes: 0 = pass (or nothing staged), 1 = credential-shaped string found, +# 2 = hook/scan error (fail-closed). + +set -u + +PATTERNS='ghp_[A-Za-z0-9]{36}|gho_[A-Za-z0-9]{36}|ghs_[A-Za-z0-9]{36}|ghu_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{22,}|[s]k-ant-|AKIA[0-9A-Z]{16}' +# ^ [s]k-ant- is spelled as a one-char class on purpose: the literal prefix +# would make this repo fail its own gate. The class matches the identical +# strings (standard self-scan idiom). All other pattern literals cannot +# self-match because the character following each prefix in this source is +# "[" or "|", which is outside the required character classes. + +fail() { printf 'fleet-pre-commit: FAIL: %s\n' "$*" >&2; exit 2; } + +command -v git >/dev/null 2>&1 || fail "git not found — refusing to run" +command -v awk >/dev/null 2>&1 || fail "awk not found — refusing to run" +git rev-parse --is-inside-work-tree >/dev/null 2>&1 || fail "not inside a git work tree" + +tmpdir=$(mktemp -d 2>/dev/null) || fail "mktemp failed — refusing to run unscanned" +trap 'rm -rf "$tmpdir"' EXIT INT TERM + +staged_list="$tmpdir/staged" +git -c core.quotePath=false diff --cached --name-only --diff-filter=ACMR >"$staged_list" 2>/dev/null \ + || fail "could not read the staged file list (git diff --cached failed)" + +# nothing staged -> nothing to scan -> pass +[ -s "$staged_list" ] || exit 0 + +# fail closed on pathological filenames: git C-quotes control characters +# (e.g. newline) in non -z output even with core.quotePath=false; such a path +# cannot be scanned reliably line-by-line and its quoted form would not match +# the index entry, so we refuse to guess instead. +if grep -q '^"' "$staged_list"; then + fail "a staged filename contains control characters (e.g. newline; git quoted it) — refusing to guess (fail-closed); rename the file and restage" +fi + +findings="$tmpdir/findings"; errs="$tmpdir/errs" +: >"$findings"; : >"$errs" + +while IFS= read -r f; do + [ -n "$f" ] || continue + # defensive excludes (git never stages .git/ contents, but stay strict) + case "$f" in + .git/*|*/.git/*|node_modules/*|*/node_modules/*) continue ;; + esac + # fast path: one combined grep over the staged blob (index is the truth) + git -c core.quotePath=false --no-pager grep --cached -nI -E "$PATTERNS" -- ":(literal)$f" >/dev/null 2>>"$errs" + rc=$? + if [ "$rc" -eq 0 ]; then + # classify per pattern family so the report names the class; + # FLEET_ALLOW exempts lines that ALSO match it (checked in awk) + for spec in \ + 'GHP:ghp_[A-Za-z0-9]{36}' \ + 'GHO:gho_[A-Za-z0-9]{36}' \ + 'GHS:ghs_[A-Za-z0-9]{36}' \ + 'GHU:ghu_[A-Za-z0-9]{36}' \ + 'GH_PAT:github_pat_[A-Za-z0-9_]{22,}' \ + 'SK_ANT:[s]k-ant-' \ + 'AWS_KEY:AKIA[0-9A-Z]{16}' + do + cls=${spec%%:*}; re=${spec#*:} + if git -c core.quotePath=false --no-pager grep --cached -nI -E "$re" -- ":(literal)$f" 2>>"$errs" \ + | awk -v cls="$cls" -v fp="$f" ' + { + i = index($0, ":"); rest = substr($0, i + 1) + j = index(rest, ":"); ln = substr(rest, 1, j - 1) + content = substr(rest, j + 1) + if (ENVIRON["FLEET_ALLOW"] != "" && content ~ ENVIRON["FLEET_ALLOW"]) next + printf "%s\t%s\t%s\n", cls, fp, ln + }' >>"$findings" + then : + else printf '%s\n' "classify error on: $f" >>"$errs" + fi + done + elif [ "$rc" -ne 1 ]; then + printf '%s\n' "scan error (git grep rc=$rc) on: $f" >>"$errs" + fi +done <"$staged_list" + +if [ -s "$errs" ]; then + printf 'fleet-pre-commit: BLOCKED (fail-closed) — the scan itself errored; commit refused.\n' >&2 + sed 's/^/ /' "$errs" >&2 + printf ' Fix the condition above and retry. (exit 2)\n' >&2 + exit 2 +fi + +if [ -s "$findings" ]; then + printf 'fleet-pre-commit: BLOCKED — credential-shaped string(s) in staged content.\n' >&2 + printf '\n class file:line\n' >&2 + awk -F'\t' '{ printf " %-10s %s:%s\n", $1, $2, $3 }' "$findings" >&2 + printf '\n The matched text is intentionally NOT shown (do not leak it into logs).\n' >&2 + printf ' Remove the secret or replace it with a placeholder. ONLY for obviously-fake\n' >&2 + printf ' fixtures, retry once with: FLEET_ALLOW= git commit ...\n' >&2 + printf ' Scan covers: ghp_/gho_/ghs_/ghu_ (36), github_pat_ (22+), sk-ant prefix, AKIA+16.\n' >&2 + printf ' Design + tests: https://github.com/SuperInstance/fleet-hooks (exit 1)\n' >&2 + exit 1 +fi + +exit 0 diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..d29a06d --- /dev/null +++ b/setup.sh @@ -0,0 +1,43 @@ +#!/bin/sh +# fleet-hooks setup — arm the fail-closed credential key-scan pre-commit +# in the repository you run it from (run at any depth inside that repo). +# +# Requires .githooks/pre-commit to exist at the repo root first (vendor it: +# see README "Install"). Fail-closed: if the hook file is missing we do NOT +# arm a hooksPath that would silently disable scanning. +# +# Exit codes: 0 armed (smoke pass), 1 setup failure, propagates the hook's +# own exit code (1/2) if the smoke run finds staged credential-shaped strings +# — that is the hook working correctly on a dirty index. + +set -eu + +git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { + echo "fleet-hooks setup: FAIL: not inside a git repository" >&2 + exit 1 +} + +root=$(git rev-parse --show-toplevel) + +if [ ! -f "$root/.githooks/pre-commit" ]; then + echo "fleet-hooks setup: FAIL: $root/.githooks/pre-commit not found." >&2 + echo " Vendor it first, e.g.:" >&2 + echo " git clone --depth 1 https://github.com/SuperInstance/fleet-hooks /tmp/fleet-hooks && cp -r /tmp/fleet-hooks/.githooks . && rm -rf /tmp/fleet-hooks" >&2 + echo " Refusing to arm core.hooksPath at a path with no hook (fail-closed)." >&2 + exit 1 +fi + +chmod +x "$root/.githooks/pre-commit" +git config core.hooksPath .githooks + +echo "fleet-hooks: armed." +echo " repo: $root" +echo " core.hooksPath $(git config core.hooksPath)" +echo " pre-commit: fail-closed credential key-scan (ghp_/gho_/ghs_/ghu_, github_pat_, sk-ant prefix, AKIA)" +echo " escape hatch: FLEET_ALLOW= (per-invocation, loud — only for obviously-fake fixtures)" +echo " verify: git config core.hooksPath # -> .githooks" + +# smoke: run the hook against the current index right now +printf 'fleet-hooks: smoke — scanning current index ...\n' +sh "$root/.githooks/pre-commit" +printf 'fleet-hooks: smoke PASS (index clean or nothing staged).\n'