aboutsummaryrefslogtreecommitdiff
path: root/scripts/tests/pre-commit-secret-scan.bats
blob: 013129e37ca0268b4922e9a2078aa01ceea187f3 (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
#!/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.

VARIANTS="elisp bash go"

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
}