aboutsummaryrefslogtreecommitdiff
path: root/scripts/tests/pre-commit-secret-scan.bats
blob: 46475567ab6b617b4059ffe11886f3bdb6d31ae0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
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
}