Setting the file. One moment.
Security Scan Test · Reviewing Claude Config · bitwarden/ai-plugins · Skills Docs
ContentsBack to the top of the page 19
Running Work Transitions
61
Reviewing Command Definitions
scripts/security-scan.test.sh
scripts/ security-scan.test.sh
Shell · 201 lines · 9 KB
15
16 set -uo pipefail
17
18 SCRIPT_DIR = "$( cd "$( dirname "${ BASH_SOURCE [0]}")" && pwd )"
19 SCAN = "${ SCRIPT_DIR }/security-scan.sh"
20
21 if [ ! -x "${ SCAN }" ]; then
22 echo "Error: ${ SCAN } is missing or not executable"
23 exit 1
24 fi
25
26 WORK_DIR = "$( mktemp -d )"
27 trap 'rm -rf "${WORK_DIR}"' EXIT
28
29 PASSED = 0
30 FAILED = 0
31
32 # Assembled so the literal never appears in this file.
33 OPENAI_KEY = "sk-$( printf 'A%.0s' $( seq 1 36 ))"
34 GITHUB_TOKEN = "ghp_$( printf 'B%.0s' $( seq 1 36 ))"
35
36 # fixture <name> — makes ${WORK_DIR}/<name>/.claude and echoes its path
37 fixture () {
38 local dir = "${ WORK_DIR }/ $1 /.claude"
39 mkdir -p "${ dir }"
40 echo "${ dir }"
41 }
42
43 # git_fixture <dir> — turns the parent of <dir> into a repository with one commit
44 git_fixture () {
45 (
46 cd " $1 /.." || exit 1
47 git init -q .
48 git add -A -f > /dev/null 2>&1
49 git -c user.email=t@t -c user.name=t commit -qm fixture > /dev/null 2>&1
50 )
51 }
52
53 # assert <case> <expect|reject> <pattern> <output>
54 assert () {
55 local name = " $1 " mode = " $2 " pattern = " $3 " output = " $4 "
56 local hit = 0
57 grep -qF -- "${ pattern }" <<< "${ output }" && hit = 1
58
59 if { [ "${ mode }" = "expect" ] && [ "${ hit }" -eq 1 ]; } ||
60 { [ "${ mode }" = "reject" ] && [ "${ hit }" -eq 0 ]; }; then
61 echo " ✅ ${ name }"
62 PASSED = $(( PASSED + 1 ))
63 else
64 echo " ❌ ${ name }"
65 echo " ${ mode }ed: ${ pattern }"
66 echo " got:"
67 sed 's/^/ /' <<< "${ output }"
68 FAILED = $(( FAILED + 1 ))
69 fi
70 }
71
72 # assert_status <case> <expected> <actual>
73 assert_status () {
74 if [ " $2 " -eq " $3 " ]; then
75 echo " ✅ $1 "
76 PASSED = $(( PASSED + 1 ))
77 else
78 echo " ❌ $1 (expected exit $2 , got $3 )"
79 FAILED = $(( FAILED + 1 ))
80 fi
81 }
82
83 echo "=== security-scan.sh regression tests ==="
84 echo ""
85
86 # ---------------------------------------------------------------------------
87 # Check 1 must report a committed settings.local.json even when git's output is
88 # long enough that grep exits first. Piping git into `grep -q` under `pipefail`
89 # lost the match: grep exited, git took SIGPIPE with 141, and pipefail surfaced
90 # that as pipeline failure. The fixture name sorts early on purpose.
91 # ---------------------------------------------------------------------------
92 echo "[1] committed settings.local.json, large repository"
93 CLAUDE_DIR = "$( fixture large-repo)"
94 printf 'x\n' > "${ CLAUDE_DIR }/aaa-settings.local.json"
95 # The filler has to match the pathspec and sit under the scanned directory, or git
96 # writes a single line and the pipe never fills. 20k paths is roughly 500 KB, well
97 # past the 64 KB buffer, so a reintroduced `| grep -q` takes SIGPIPE here.
98 ( cd "${ CLAUDE_DIR }" && seq 1 20000 | sed 's|$|-settings.local.json|' | xargs touch )
99 git_fixture "${ CLAUDE_DIR }"
100 OUT = "$( bash "${ SCAN }" "${ CLAUDE_DIR }" 2>&1 )"
101 assert "detects the committed file" expect "CRITICAL: settings.local.json is committed to git" "${ OUT }"
102 assert "does not claim it is absent" reject "OK: settings.local.json not in git" "${ OUT }"
103 echo ""
104
105 # ---------------------------------------------------------------------------
106 # Check 1 covers the repository, not just the scanned subtree. Scoping git to
107 # CLAUDE_DIR would list only paths under it, missing a committed file elsewhere
108 # in the same repository and printing paths that do not match the remediation.
109 # ---------------------------------------------------------------------------
110 echo "[2] committed settings.local.json elsewhere in the repository"
111 CLAUDE_DIR = "$( fixture monorepo)"
112 mkdir -p "${ CLAUDE_DIR }/../apps/foo/.claude"
113 printf 'x\n' > "${ CLAUDE_DIR }/../apps/foo/.claude/settings.local.json"
114 git_fixture "${ CLAUDE_DIR }"
115 OUT = "$( bash "${ SCAN }" "${ CLAUDE_DIR }" 2>&1 )"
116 assert "detects it outside the scanned directory" expect "CRITICAL: settings.local.json is committed to git" "${ OUT }"
117 assert "reports a repository-relative path" expect "apps/foo/.claude/settings.local.json" "${ OUT }"
118 echo ""
119
120 # ---------------------------------------------------------------------------
121 # Outside a git repository the check cannot run. It used to report a pass,
122 # because git ran against whatever repository the shell was in and its error
123 # was discarded.
124 # ---------------------------------------------------------------------------
125 echo "[3] target outside any git repository"
126 CLAUDE_DIR = "$( fixture no-repo)"
127 OUT = "$( bash "${ SCAN }" "${ CLAUDE_DIR }" 2>&1 )"
128 assert "records the check as skipped" expect "SKIPPED: ${ CLAUDE_DIR } is not inside a git repository" "${ OUT }"
129 assert "does not vouch for local settings" reject "OK: settings.local.json not in git" "${ OUT }"
130 assert "verdict reports the run as incomplete" expect "INCOMPLETE" "${ OUT }"
131 echo ""
132
133 # ---------------------------------------------------------------------------
134 # An absent settings.json is a determinate answer, not a check that could not run: no file
135 # means no rules are set in the scanned directory. It must not drive the run to
136 # INCOMPLETE, which would overstate the doubt.
137 # ---------------------------------------------------------------------------
138 echo "[4] no settings.json to inspect"
139 CLAUDE_DIR = "$( fixture no-settings)"
140 git_fixture "${ CLAUDE_DIR }"
141 OUT = "$( bash "${ SCAN }" "${ CLAUDE_DIR }" 2>&1 )"
142 STATUS = $?
143 assert "says no rules are set here" expect "No settings.json at ${ CLAUDE_DIR }" "${ OUT }"
144 assert "does not record it as skipped" reject "SKIPPED: no settings.json" "${ OUT }"
145 assert "verdict is not incomplete on this account" reject "INCOMPLETE" "${ OUT }"
146 assert_status "exits 0" 0 "${ STATUS }"
147 echo ""
148
149 # ---------------------------------------------------------------------------
150 # Real credentials are reported wherever they sit in the scanned tree. Earlier
151 # filters matched bare substrings over grep's path:line output and discarded
152 # findings by path or by a placeholder fragment inside a real value.
153 # ---------------------------------------------------------------------------
154 echo "[5] real credentials in the scanned tree"
155 CLAUDE_DIR = "$( fixture real-secrets)"
156 mkdir -p "${ CLAUDE_DIR }/skills/demo/examples"
157 printf '%s\n' "${ OPENAI_KEY }" > "${ CLAUDE_DIR }/skills/demo/examples/config.json"
158 printf '{"token": "%s"}\n' "${ GITHUB_TOKEN }" > "${ CLAUDE_DIR }/settings.json"
159 OUT = "$( bash "${ SCAN }" "${ CLAUDE_DIR }" 2>&1 )"
160 assert "detects a key under an examples/ path" expect "CRITICAL: OpenAI API key pattern detected" "${ OUT }"
161 assert "detects a token in settings.json" expect "CRITICAL: GitHub token pattern detected" "${ OUT }"
162 echo ""
163
164 # ---------------------------------------------------------------------------
165 # The scanner must not report this skill's own labelled-bad fixtures, whether
166 # the target is given as a relative or an absolute path.
167 # ---------------------------------------------------------------------------
168 echo "[6] scanning this skill does not flag its own fixtures"
169 OUT = "$( cd "${ SCRIPT_DIR }/.." && bash "${ SCAN }" . 2>&1 )"
170 assert "the scan actually ran (relative path)" expect "Scan Complete" "${ OUT }"
171 assert "no false positive (relative path)" reject "CRITICAL" "${ OUT }"
172 OUT = "$( bash "${ SCAN }" "${ SCRIPT_DIR }/.." 2>&1 )"
173 assert "the scan actually ran (absolute path)" expect "Scan Complete" "${ OUT }"
174 assert "no false positive (absolute path)" reject "CRITICAL" "${ OUT }"
175 echo ""
176
177 # ---------------------------------------------------------------------------
178 # A clean configuration reports every check as run, and exits 0. Check 3's rule
179 # tests need jq to tell an allow rule from a deny one, so without it the run is
180 # legitimately incomplete and the expectation flips.
181 # ---------------------------------------------------------------------------
182 echo "[7] clean configuration"
183 CLAUDE_DIR = "$( fixture clean)"
184 printf '{"permissions": {"allow": ["Read(./src/**)"]}}\n' > "${ CLAUDE_DIR }/settings.json"
185 git_fixture "${ CLAUDE_DIR }"
186 OUT = "$( bash "${ SCAN }" "${ CLAUDE_DIR }" 2>&1 )"
187 STATUS = $?
188 if command -v jq > /dev/null 2>&1 ; then
189 assert "reports the checks passed" expect "The deterministic checks passed" "${ OUT }"
190 assert "nothing recorded as skipped" reject "SKIPPED" "${ OUT }"
191 assert_status "exits 0" 0 "${ STATUS }"
192 else
193 echo " ℹ️ jq not installed: expecting the run to report as incomplete"
194 assert "reports the jq-gated checks as skipped" expect "SKIPPED" "${ OUT }"
195 assert "verdict reports the run as incomplete" expect "INCOMPLETE" "${ OUT }"
196 assert_status "exits 2" 2 "${ STATUS }"
197 fi
198 echo ""
199
200 echo "=== ${ PASSED } passed, ${ FAILED } failed ==="
201 [ "${ FAILED }" -eq 0 ]