diff options
Diffstat (limited to 'scripts/tests/pre-commit-secret-scan.bats')
| -rw-r--r-- | scripts/tests/pre-commit-secret-scan.bats | 197 |
1 files changed, 197 insertions, 0 deletions
diff --git a/scripts/tests/pre-commit-secret-scan.bats b/scripts/tests/pre-commit-secret-scan.bats new file mode 100644 index 0000000..4647556 --- /dev/null +++ b/scripts/tests/pre-commit-secret-scan.bats @@ -0,0 +1,197 @@ +#!/usr/bin/env bats +# Tests for the secret-scan block shared by the elisp, bash, and go pre-commit +# hooks. The block greps added lines in the staged diff for credential +# patterns; a hit blocks the commit (exit 1), a clean scan falls through to the +# variant's language check (exit 0). +# +# Every case stages a .txt file, so the language checks that follow the scan +# (check-parens, shellcheck, gofmt) all skip and the scan is what's under test. +# +# The two boundary cases exist because of a live false-positive in a downstream +# project: an embedded PNG sprite data URI blocked a real commit and forced +# --no-verify. Root cause was `grep -iE` applying case-insensitivity to the +# fixed-case AWS token AKIA[0-9A-Z]{16}, so any mixed-case 20-char run inside a +# random base64 blob matched. Measured at ~6% of 100KB blobs; case-sensitive +# matching drops it to 0 across ~10MB. + +# Discovered, never enumerated. This list read "elisp bash go" while python and +# typescript also shipped pre-commit hooks, so every "in every variant" test +# below silently skipped two bundles from the day they were added — the same +# enumerate-instead-of-discover failure these tests exist to catch. A new bundle +# is now covered the moment it has a hook. +VARIANTS="$(cd "${BATS_TEST_DIRNAME}/../../languages" && \ + for d in */githooks/pre-commit; do [ -f "$d" ] && printf '%s ' "${d%%/*}"; done)" + +setup() { + REPO="$(mktemp -d)" + cd "$REPO" || return 1 + git init -q . + git config user.email t@example.com + git config user.name Test +} + +teardown() { + cd /tmp || true + [ -n "${REPO:-}" ] && rm -rf "$REPO" +} + +# Stage $2 as the content of file $1 (default staged.txt). +stage() { + local file="${2:-staged.txt}" + printf '%s\n' "$1" > "$file" + git add "$file" +} + +# Run a variant's hook in the temp repo. $1 = variant name. +run_hook() { + bash "${BATS_TEST_DIRNAME}/../../languages/$1/githooks/pre-commit" +} + +# ---- Normal: real secrets still block, clean content still passes ---- + +@test "secret-scan: clean content passes in every variant" { + stage 'const greeting = "hello world";' + for v in $VARIANTS; do + run run_hook "$v" + [ "$status" -eq 0 ] || { echo "$v blocked clean content: $output"; return 1; } + done +} + +@test "secret-scan: a real uppercase AWS access key blocks in every variant" { + stage 'aws_key = "AKIAIOSFODNN7EXAMPLE"' + for v in $VARIANTS; do + run run_hook "$v" + [ "$status" -eq 1 ] || { echo "$v missed an AWS key"; return 1; } + [[ "$output" == *"potential secret"* ]] + done +} + +@test "secret-scan: a keyword=value credential blocks in every variant" { + stage 'api_key: "sk_live_9f3b2a7c1e4d8f0a6b5"' + for v in $VARIANTS; do + run run_hook "$v" + [ "$status" -eq 1 ] || { echo "$v missed an api_key assignment"; return 1; } + done +} + +@test "secret-scan: a PEM private-key header blocks in every variant" { + stage '-----BEGIN RSA PRIVATE KEY-----' + for v in $VARIANTS; do + run run_hook "$v" + [ "$status" -eq 1 ] || { echo "$v missed a PEM header"; return 1; } + done +} + +# ---- Boundary: the case-sensitivity fix ---- + +@test "secret-scan: a lowercase akia-like run does not block (AWS keys are uppercase)" { + # Under `grep -iE` this matched the AKIA token and blocked a legitimate commit. + stage 'const blob = "akiaiosfodnn7examplexyz";' + for v in $VARIANTS; do + run run_hook "$v" + [ "$status" -eq 0 ] || { echo "$v false-positived on a lowercase run: $output"; return 1; } + done +} + +@test "secret-scan: an embedded base64 data URI carrying a mixed-case akia run does not block" { + # The live failure: a sprite blob whose random base64 contained a mixed-case + # 20-char run. Case-sensitive matching is what clears it. + stage 'const SPRITE = "data:image/png;base64,iVBORw0KGgoAkIaIOSFODNN7ExAMPLEqQmCC";' + for v in $VARIANTS; do + run run_hook "$v" + [ "$status" -eq 0 ] || { echo "$v false-positived on a sprite data URI: $output"; return 1; } + done +} + +# ---- Boundary: the scan must not go blind on data-URI lines ---- + +@test "secret-scan: a real credential sharing a line with a base64 data URI still blocks" { + # Minified bundles put a whole file on one line, so a data URI and a real key + # can share it. Skipping any line containing ';base64,' would hide the key. + stage 'const S="data:image/png;base64,iVBORw0KGgoAAAANS";const c={api_key:"sk_live_9f3b2a7c1e4d8f0a6b5"};' + for v in $VARIANTS; do + run run_hook "$v" + [ "$status" -eq 1 ] || { echo "$v went blind on a data-URI line and missed the key"; return 1; } + done +} + +@test "secret-scan: a line matching both passes is reported once, not twice" { + # The scan runs a case-sensitive and a case-insensitive pass. A line carrying + # both an AWS key and a keyword=value credential hits both; reporting it twice + # reads as two separate leaks. + stage 'api_key = "AKIAIOSFODNN7EXAMPLE_padding"' + for v in $VARIANTS; do + run run_hook "$v" + [ "$status" -eq 1 ] + hits="$(printf '%s\n' "$output" | grep -c 'AKIAIOSFODNN7EXAMPLE_padding')" + [ "$hits" -eq 1 ] || { echo "$v reported the line $hits times, want 1"; return 1; } + done +} + +# ---- Error / edge ---- + +@test "secret-scan: an empty staged diff passes" { + for v in $VARIANTS; do + run run_hook "$v" + [ "$status" -eq 0 ] || { echo "$v failed on an empty diff: $output"; return 1; } + done +} + +@test "secret-scan: a secret only on a removed line does not block" { + # The scan reads added lines. Deleting a key should never block the deletion. + stage 'aws_key = "AKIAIOSFODNN7EXAMPLE"' + git commit -qm "seed" --no-verify + printf 'clean\n' > staged.txt + git add staged.txt + for v in $VARIANTS; do + run run_hook "$v" + [ "$status" -eq 0 ] || { echo "$v blocked a removal: $output"; return 1; } + done +} + +# ---- Fail-closed: a broken git must never read as "nothing to scan" ---- + +# Put a stub `git` ahead of the real one that fails only the staged-diff call +# and delegates everything else, so just the pipeline under test breaks. +break_git() { + mkdir -p "$REPO/bin" + cat > "$REPO/bin/git" <<'STUB' +#!/usr/bin/env bash +if [ "${1:-}" = "diff" ] && [ "${2:-}" = "--cached" ]; then + echo "simulated git failure" >&2 + exit 128 +fi +exec /usr/bin/git "$@" +STUB + chmod +x "$REPO/bin/git" +} + +@test "secret-scan: a broken git refuses rather than passing blind, in every variant" { + # The scan built its input as `git diff ... | grep ... || true`. With no + # pipefail, a git failure yielded an empty string, so the scan searched + # nothing, found nothing, and reported clean with a real secret staged. + # Found by .emacs.d in elisp 2026-07-24; all five variants had it. + stage 'aws_key = "AKIAIOSFODNN7EXAMPLE"' + break_git + for v in $VARIANTS; do + run env PATH="$REPO/bin:$PATH" bash \ + "${BATS_TEST_DIRNAME}/../../languages/$v/githooks/pre-commit" + [ "$status" -ne 0 ] || { + echo "$v FAILED OPEN: exited 0 with a secret staged and git broken" + return 1 + } + done +} + +@test "secret-scan: the refusal says why, in every variant" { + stage 'aws_key = "AKIAIOSFODNN7EXAMPLE"' + break_git + for v in $VARIANTS; do + run env PATH="$REPO/bin:$PATH" bash \ + "${BATS_TEST_DIRNAME}/../../languages/$v/githooks/pre-commit" + [[ "$output" == *"cannot read"* ]] || { + echo "$v refused without naming the cause: $output" + return 1 + } + done +} |
