@aldegad/safedeps 2.1.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +273 -463
- package/README.ko.md +76 -12
- package/README.md +107 -38
- package/ROADMAP.md +123 -84
- package/SECURITY.md +45 -0
- package/SKILL.md +86 -143
- package/bin/safedeps +419 -52
- package/lib/gates/audit.sh +36 -0
- package/lib/gates/doctor.sh +212 -0
- package/lib/gates/hooks.sh +131 -0
- package/lib/gates/repo-profile.sh +60 -0
- package/lib/gates/scan.sh +94 -0
- package/lib/gates/templates/gitleaks.private.toml.tmpl +45 -0
- package/lib/gates/templates/gitleaks.toml.tmpl +43 -0
- package/lib/gates/templates/pre-commit.tmpl +49 -0
- package/lib/ledger/ledger.sh +94 -16
- package/lib/npm/closure.sh +115 -0
- package/lib/providers/providers.sh +248 -26
- package/package.json +2 -1
- package/scripts/install/install-safedeps-hooks.mjs +65 -23
- package/scripts/release-gates.sh +252 -0
- package/scripts/safedeps-post-verify.sh +185 -15
- package/scripts/safedeps-pre-guard.sh +309 -39
- package/scripts/test/e2e.sh +228 -4
- package/scripts/test/fixture-provider.mjs +21 -0
- package/scripts/test/smoke.sh +212 -10
package/scripts/test/e2e.sh
CHANGED
|
@@ -38,19 +38,68 @@ port=$(cat "${port_file}")
|
|
|
38
38
|
|
|
39
39
|
export SAFEDEPS_HOME="${tmp_root}/safe"
|
|
40
40
|
export SAFEDEPS_OSV_API_URL="http://127.0.0.1:${port}/osv/v1/query"
|
|
41
|
+
export SAFEDEPS_OSV_BATCH_API_URL="http://127.0.0.1:${port}/osv/v1/querybatch"
|
|
41
42
|
export SAFEDEPS_KEV_CATALOG_URL="http://127.0.0.1:${port}/kev.json"
|
|
42
43
|
export SAFEDEPS_GHSA_API_URL="http://127.0.0.1:${port}/advisories"
|
|
43
44
|
export SAFEDEPS_PROVIDER_CACHE_TTL_SECONDS=0
|
|
44
45
|
|
|
46
|
+
closure_fixture="${tmp_root}/closure-fixture.json"
|
|
47
|
+
cat > "${closure_fixture}" <<'EOF'
|
|
48
|
+
{
|
|
49
|
+
"fixture-clean@1.0.0": [
|
|
50
|
+
{"package":"fixture-clean","version":"1.0.0","direct":true}
|
|
51
|
+
],
|
|
52
|
+
"fixture-vuln@1.0.0": [
|
|
53
|
+
{"package":"fixture-vuln","version":"1.0.0","direct":true}
|
|
54
|
+
],
|
|
55
|
+
"fixture-vuln@1.0.1": [
|
|
56
|
+
{"package":"fixture-vuln","version":"1.0.1","direct":true}
|
|
57
|
+
],
|
|
58
|
+
"fixture-multi-vuln@1.0.0": [
|
|
59
|
+
{"package":"fixture-multi-vuln","version":"1.0.0","direct":true}
|
|
60
|
+
],
|
|
61
|
+
"fixture-multi-vuln@1.0.1": [
|
|
62
|
+
{"package":"fixture-multi-vuln","version":"1.0.1","direct":true}
|
|
63
|
+
],
|
|
64
|
+
"fixture-multi-vuln@1.0.5": [
|
|
65
|
+
{"package":"fixture-multi-vuln","version":"1.0.5","direct":true}
|
|
66
|
+
],
|
|
67
|
+
"fixture-unpatched@1.0.0": [
|
|
68
|
+
{"package":"fixture-unpatched","version":"1.0.0","direct":true}
|
|
69
|
+
],
|
|
70
|
+
"fixture-kev@1.0.0": [
|
|
71
|
+
{"package":"fixture-kev","version":"1.0.0","direct":true}
|
|
72
|
+
],
|
|
73
|
+
"fixture-parent@1.0.0": [
|
|
74
|
+
{"package":"fixture-parent","version":"1.0.0","direct":true},
|
|
75
|
+
{"package":"fixture-child","version":"1.0.0","direct":false}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
EOF
|
|
79
|
+
export SAFEDEPS_NPM_CLOSURE_FIXTURE_JSON="${closure_fixture}"
|
|
80
|
+
|
|
45
81
|
clean_json=$(./bin/safedeps --json check npm fixture-clean@1.0.0)
|
|
46
82
|
[[ "$(jq -r '.result' <<< "${clean_json}")" == "clean" ]] || fail "clean fixture approved"
|
|
47
83
|
pass "clean advisory approval"
|
|
48
84
|
|
|
85
|
+
closure_json=$(./bin/safedeps --json check npm fixture-parent@1.0.0)
|
|
86
|
+
[[ "$(jq -r '.result' <<< "${closure_json}")" == "clean" ]] || fail "closure fixture approved"
|
|
87
|
+
[[ "$(jq -r '.transitive_count' <<< "${closure_json}")" == "1" ]] || fail "closure fixture records transitive count"
|
|
88
|
+
parent_hash=$(jq -r '.spec_hash' <<< "${closure_json}")
|
|
89
|
+
parent_file="${SAFEDEPS_HOME}/approved-specs/${parent_hash/:/-}.json"
|
|
90
|
+
[[ "$(jq -r '.transitive_specs[0].package' "${parent_file}")" == "fixture-child" ]] || fail "ledger transitive_specs records fixture child"
|
|
91
|
+
pass "closure approval records transitive_specs"
|
|
92
|
+
|
|
49
93
|
patched_json=$(./bin/safedeps --json check npm fixture-vuln@1.0.0)
|
|
50
94
|
[[ "$(jq -r '.result' <<< "${patched_json}")" == "patched_available" ]] || fail "patched fixture narrows"
|
|
51
95
|
[[ "$(jq -r '.suggested_spec' <<< "${patched_json}")" == "1.0.1" ]] || fail "patched fixture suggests fixed version"
|
|
52
96
|
pass "patched advisory narrowing"
|
|
53
97
|
|
|
98
|
+
multi_patched_json=$(./bin/safedeps --json check npm fixture-multi-vuln@1.0.0)
|
|
99
|
+
[[ "$(jq -r '.result' <<< "${multi_patched_json}")" == "patched_available" ]] || fail "multi patched fixture narrows"
|
|
100
|
+
[[ "$(jq -r '.suggested_spec' <<< "${multi_patched_json}")" == "1.0.5" ]] || fail "multi patched fixture tries later clean fixed version"
|
|
101
|
+
pass "patched advisory tries all fixed candidates"
|
|
102
|
+
|
|
54
103
|
set +e
|
|
55
104
|
unpatched_json=$(./bin/safedeps --json check npm fixture-unpatched@1.0.0)
|
|
56
105
|
unpatched_status=$?
|
|
@@ -68,18 +117,136 @@ mkdir -p "${project_dir}"
|
|
|
68
117
|
printf '{"dependencies":{}}\n' > "${project_dir}/package.json"
|
|
69
118
|
hook_allow=$(
|
|
70
119
|
scripts/safedeps-pre-guard.sh <<EOF
|
|
71
|
-
{"tool_name":"Bash","tool_input":{"command":"npm install fixture-vuln@1.0.1"},"cwd":"${project_dir}"}
|
|
120
|
+
{"tool_name":"Bash","tool_input":{"command":"npm install fixture-vuln@1.0.1"},"cwd":"${project_dir}","turn_id":"turn-e2e","model":"codex-test"}
|
|
72
121
|
EOF
|
|
73
122
|
)
|
|
74
123
|
[[ -z "${hook_allow}" ]] || fail "hook allows narrowed approved spec"
|
|
75
124
|
pass "hook allows approved narrowed spec"
|
|
76
125
|
|
|
126
|
+
effect_project="${tmp_root}/effect-project"
|
|
127
|
+
mkdir -p "${effect_project}"
|
|
128
|
+
printf '{"dependencies":{}}\n' > "${effect_project}/package.json"
|
|
129
|
+
|
|
130
|
+
effect_clean_pre=$(
|
|
131
|
+
scripts/safedeps-pre-guard.sh <<EOF
|
|
132
|
+
{"tool_name":"Bash","tool_input":{"command":"npm install fixture-parent@1.0.0"},"cwd":"${effect_project}","turn_id":"turn-e2e","model":"codex-test"}
|
|
133
|
+
EOF
|
|
134
|
+
)
|
|
135
|
+
[[ -z "${effect_clean_pre}" ]] || fail "effect clean pre hook allows closure-approved direct spec"
|
|
136
|
+
cat > "${effect_project}/package-lock.json" <<'EOF'
|
|
137
|
+
{
|
|
138
|
+
"name": "effect-project",
|
|
139
|
+
"lockfileVersion": 3,
|
|
140
|
+
"packages": {
|
|
141
|
+
"": {"dependencies": {"fixture-parent": "1.0.0"}},
|
|
142
|
+
"node_modules/fixture-parent": {"version": "1.0.0", "dependencies": {"fixture-child": "1.0.0"}},
|
|
143
|
+
"node_modules/fixture-child": {"version": "1.0.0"}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
EOF
|
|
147
|
+
effect_clean_post=$(
|
|
148
|
+
scripts/safedeps-post-verify.sh <<EOF
|
|
149
|
+
{"tool_name":"Bash","tool_input":{"command":"npm install fixture-parent@1.0.0"}}
|
|
150
|
+
EOF
|
|
151
|
+
)
|
|
152
|
+
[[ -z "${effect_clean_post}" ]] || fail "post hook passes approved full closure"
|
|
153
|
+
pass "post hook passes approved full closure"
|
|
154
|
+
|
|
155
|
+
inert_project="${tmp_root}/inert-project"
|
|
156
|
+
mkdir -p "${inert_project}"
|
|
157
|
+
printf '{"dependencies":{}}\n' > "${inert_project}/package.json"
|
|
158
|
+
inert_pre=$(
|
|
159
|
+
scripts/safedeps-pre-guard.sh <<EOF
|
|
160
|
+
{"tool_name":"Bash","tool_input":{"command":"npm install fixture-parent@1.0.0"},"cwd":"${inert_project}"}
|
|
161
|
+
EOF
|
|
162
|
+
)
|
|
163
|
+
[[ "$(jq -r '.hookSpecificOutput.permissionDecision' <<< "${inert_pre}")" == "allow" ]] || fail "inert pre hook emits Claude allow"
|
|
164
|
+
[[ "$(jq -r '.hookSpecificOutput.updatedInput.command' <<< "${inert_pre}")" == "npm install fixture-parent@1.0.0 --ignore-scripts" ]] || fail "inert pre hook injects ignore-scripts"
|
|
165
|
+
cat > "${inert_project}/package-lock.json" <<'EOF'
|
|
166
|
+
{
|
|
167
|
+
"name": "inert-project",
|
|
168
|
+
"lockfileVersion": 3,
|
|
169
|
+
"packages": {
|
|
170
|
+
"": {"dependencies": {"fixture-parent": "1.0.0"}},
|
|
171
|
+
"node_modules/fixture-parent": {"version": "1.0.0", "dependencies": {"fixture-child": "1.0.0"}},
|
|
172
|
+
"node_modules/fixture-child": {"version": "1.0.0"}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
EOF
|
|
176
|
+
stub_bin="${tmp_root}/stub-bin"
|
|
177
|
+
mkdir -p "${stub_bin}"
|
|
178
|
+
cat > "${stub_bin}/npm" <<EOF
|
|
179
|
+
#!/usr/bin/env bash
|
|
180
|
+
printf '%s\n' "\$*" >> "${tmp_root}/npm-calls.log"
|
|
181
|
+
exit 0
|
|
182
|
+
EOF
|
|
183
|
+
chmod +x "${stub_bin}/npm"
|
|
184
|
+
inert_post=$(
|
|
185
|
+
PATH="${stub_bin}:${PATH}" scripts/safedeps-post-verify.sh <<EOF
|
|
186
|
+
{"tool_name":"Bash","tool_input":{"command":"npm install fixture-parent@1.0.0 --ignore-scripts"}}
|
|
187
|
+
EOF
|
|
188
|
+
)
|
|
189
|
+
[[ -z "${inert_post}" ]] || fail "post hook keeps verified inert rebuild success quiet"
|
|
190
|
+
grep -qx 'rebuild' "${tmp_root}/npm-calls.log" || fail "post hook runs npm rebuild after verified injected install"
|
|
191
|
+
pass "post hook rebuilds after verified inert install"
|
|
192
|
+
|
|
193
|
+
export SAFEDEPS_HOME="${tmp_root}/safe-missing-transitive"
|
|
194
|
+
export SAFEDEPS_OSV_API_URL="http://127.0.0.1:${port}/osv/v1/query"
|
|
195
|
+
export SAFEDEPS_OSV_BATCH_API_URL="http://127.0.0.1:${port}/osv/v1/querybatch"
|
|
196
|
+
export SAFEDEPS_KEV_CATALOG_URL="http://127.0.0.1:${port}/kev.json"
|
|
197
|
+
export SAFEDEPS_GHSA_API_URL="http://127.0.0.1:${port}/advisories"
|
|
198
|
+
export SAFEDEPS_PROVIDER_CACHE_TTL_SECONDS=0
|
|
199
|
+
export SAFEDEPS_NPM_CLOSURE_FIXTURE_JSON="${closure_fixture}"
|
|
200
|
+
missing_project="${tmp_root}/missing-project"
|
|
201
|
+
mkdir -p "${missing_project}"
|
|
202
|
+
printf '{"dependencies":{}}\n' > "${missing_project}/package.json"
|
|
203
|
+
SAFEDEPS_HOME="${SAFEDEPS_HOME}" lib/ledger/ledger.sh approve npm fixture-parent 1.0.0 1.0.0 direct-only >/dev/null
|
|
204
|
+
missing_pre=$(
|
|
205
|
+
scripts/safedeps-pre-guard.sh <<EOF
|
|
206
|
+
{"tool_name":"Bash","tool_input":{"command":"npm install fixture-parent@1.0.0"},"cwd":"${missing_project}","turn_id":"turn-e2e","model":"codex-test"}
|
|
207
|
+
EOF
|
|
208
|
+
)
|
|
209
|
+
[[ -z "${missing_pre}" ]] || fail "missing-transitive pre hook allows direct-only approved spec"
|
|
210
|
+
cat > "${missing_project}/package-lock.json" <<'EOF'
|
|
211
|
+
{
|
|
212
|
+
"name": "missing-project",
|
|
213
|
+
"lockfileVersion": 3,
|
|
214
|
+
"packages": {
|
|
215
|
+
"": {"dependencies": {"fixture-parent": "1.0.0"}},
|
|
216
|
+
"node_modules/fixture-parent": {"version": "1.0.0", "dependencies": {"fixture-child": "1.0.0"}},
|
|
217
|
+
"node_modules/fixture-child": {"version": "1.0.0"}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
EOF
|
|
221
|
+
missing_post=$(
|
|
222
|
+
scripts/safedeps-post-verify.sh <<EOF
|
|
223
|
+
{"tool_name":"Bash","tool_input":{"command":"npm install fixture-parent@1.0.0"}}
|
|
224
|
+
EOF
|
|
225
|
+
)
|
|
226
|
+
grep -q '의심스러운 패키지 변경 감지' <<< "${missing_post}" || fail "post hook reorgs unapproved transitive package"
|
|
227
|
+
grep -q 'fixture-child@1.0.0' <<< "${missing_post}" || fail "post hook names unapproved transitive package"
|
|
228
|
+
pass "post hook reorgs unapproved transitive package"
|
|
229
|
+
|
|
230
|
+
export SAFEDEPS_HOME="${tmp_root}/safe"
|
|
231
|
+
export SAFEDEPS_OSV_API_URL="http://127.0.0.1:${port}/osv/v1/query"
|
|
232
|
+
export SAFEDEPS_OSV_BATCH_API_URL="http://127.0.0.1:${port}/osv/v1/querybatch"
|
|
233
|
+
export SAFEDEPS_KEV_CATALOG_URL="http://127.0.0.1:${port}/kev.json"
|
|
234
|
+
export SAFEDEPS_GHSA_API_URL="http://127.0.0.1:${port}/advisories"
|
|
235
|
+
export SAFEDEPS_PROVIDER_CACHE_TTL_SECONDS=0
|
|
236
|
+
export SAFEDEPS_NPM_CLOSURE_FIXTURE_JSON="${closure_fixture}"
|
|
237
|
+
|
|
77
238
|
printf '%s\n' '{"vulnerable":["fixture-clean@1.0.0"]}' > "${state_file}"
|
|
78
239
|
recheck_json=$(./bin/safedeps --json re-check)
|
|
79
240
|
[[ "$(jq -r '.revoked | length' <<< "${recheck_json}")" == "1" ]] || fail "re-check revokes newly vulnerable spec"
|
|
80
241
|
[[ "$(jq -r '.revoked[0].package' <<< "${recheck_json}")" == "fixture-clean" ]] || fail "re-check revoked expected package"
|
|
81
242
|
pass "re-check revocation"
|
|
82
243
|
|
|
244
|
+
SAFEDEPS_HOME="${SAFEDEPS_HOME}" lib/ledger/ledger.sh approve npm fixture-forged 1.0.0 1.0.0 forged-test >/dev/null
|
|
245
|
+
forgery_json=$(./bin/safedeps --json re-check)
|
|
246
|
+
[[ "$(jq -r '.suspected_forgery | length' <<< "${forgery_json}")" == "1" ]] || fail "re-check flags direct ledger write without approval provenance"
|
|
247
|
+
[[ "$(jq -r '.suspected_forgery[0].package' <<< "${forgery_json}")" == "fixture-forged" ]] || fail "re-check flags expected forged package"
|
|
248
|
+
pass "re-check flags ledger approval provenance mismatch"
|
|
249
|
+
|
|
83
250
|
legacy_home="${tmp_root}/legacy"
|
|
84
251
|
target_home="${tmp_root}/migrated"
|
|
85
252
|
mkdir -p "${legacy_home}/approved-specs"
|
|
@@ -93,15 +260,72 @@ pass "legacy state migration"
|
|
|
93
260
|
installer_home="${tmp_root}/installer-home"
|
|
94
261
|
mkdir -p "${installer_home}/.claude" "${installer_home}/.codex"
|
|
95
262
|
cat > "${installer_home}/.claude/settings.json" <<EOF
|
|
96
|
-
{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"${installer_home}/.claude/skills/npm-reorg-guard/scripts/guard.sh"}]}],"PostToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"${installer_home}/.claude/skills/npm-reorg-guard/scripts/verify.sh"}]}]}}
|
|
263
|
+
{"hooks":{"PreToolUse":[{"matcher":"Other","hooks":[{"type":"command","command":"~/.claude/skills/safedeps/scripts/safedeps-pre-guard.sh"}]},{"matcher":"Bash","hooks":[{"type":"command","command":"${installer_home}/.claude/skills/npm-reorg-guard/scripts/guard.sh"}]}],"PostToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"${installer_home}/.claude/skills/npm-reorg-guard/scripts/verify.sh"}]}]}}
|
|
97
264
|
EOF
|
|
98
265
|
HOME="${installer_home}" node scripts/install/install-safedeps-hooks.mjs >/dev/null
|
|
99
|
-
jq -e --arg pre "
|
|
100
|
-
[.hooks.PreToolUse[]
|
|
266
|
+
jq -e --arg pre "~/.claude/skills/safedeps/scripts/safedeps-pre-guard.sh" '
|
|
267
|
+
[.hooks.PreToolUse[]? | select(.matcher == "Bash") | .hooks[]?.command] | index($pre)
|
|
101
268
|
' "${installer_home}/.claude/settings.json" >/dev/null || fail "installer writes new pre hook"
|
|
269
|
+
jq -e --arg post "~/.claude/skills/safedeps/scripts/safedeps-post-verify.sh" '
|
|
270
|
+
[.hooks.PostToolUse[]?.hooks[]?.command] | index($post)
|
|
271
|
+
' "${installer_home}/.claude/settings.json" >/dev/null || fail "installer writes new post hook"
|
|
272
|
+
jq -e --arg pre "~/.codex/skills/safedeps/scripts/safedeps-pre-guard.sh" '
|
|
273
|
+
[.hooks.PreToolUse[]?.hooks[]?.command] | index($pre)
|
|
274
|
+
' "${installer_home}/.codex/hooks.json" >/dev/null || fail "installer writes codex pre hook"
|
|
275
|
+
jq -e --arg post "~/.codex/skills/safedeps/scripts/safedeps-post-verify.sh" '
|
|
276
|
+
[.hooks.PostToolUse[]?.hooks[]?.command] | index($post)
|
|
277
|
+
' "${installer_home}/.codex/hooks.json" >/dev/null || fail "installer writes codex post hook"
|
|
102
278
|
if jq -e '[.. | strings] | any(contains("npm-reorg-guard"))' "${installer_home}/.claude/settings.json" >/dev/null; then
|
|
103
279
|
fail "installer removes legacy hook"
|
|
104
280
|
fi
|
|
105
281
|
pass "installer legacy hook cleanup"
|
|
106
282
|
|
|
283
|
+
# --- Secret-leak lane: pre-commit gate must DENY a secret, PASS clean/example -
|
|
284
|
+
# The real bypass harness for the secret lane. Needs a scanner (gitleaks or
|
|
285
|
+
# docker) and openssl for a synthetic high-entropy secret; skip explicitly
|
|
286
|
+
# (not silently) when either is missing.
|
|
287
|
+
secret_repo="${tmp_root}/secret-repo"
|
|
288
|
+
mkdir -p "${secret_repo}"
|
|
289
|
+
git -C "${secret_repo}" init -q
|
|
290
|
+
git -C "${secret_repo}" config user.email t@safedeps.test
|
|
291
|
+
git -C "${secret_repo}" config user.name safedeps-e2e
|
|
292
|
+
|
|
293
|
+
# doctor flags gaps on the bare repo, then --fix scaffolds + activates the lane.
|
|
294
|
+
if HOME="${tmp_root}/doc-home" "${ROOT_DIR}/bin/safedeps" doctor --root "${secret_repo}" >/dev/null 2>&1; then
|
|
295
|
+
fail "doctor flags gaps on an unconfigured repo"
|
|
296
|
+
fi
|
|
297
|
+
HOME="${tmp_root}/doc-home" "${ROOT_DIR}/bin/safedeps" doctor --fix --root "${secret_repo}" >/dev/null
|
|
298
|
+
[[ -f "${secret_repo}/.gitleaks.toml" ]] || fail "doctor --fix scaffolds .gitleaks.toml"
|
|
299
|
+
[[ -x "${secret_repo}/.githooks/pre-commit" ]] || fail "doctor --fix scaffolds executable pre-commit"
|
|
300
|
+
[[ "$(git -C "${secret_repo}" config --get core.hooksPath)" == ".githooks" ]] || fail "doctor --fix activates core.hooksPath"
|
|
301
|
+
pass "doctor --fix scaffolds + activates the secret lane"
|
|
302
|
+
|
|
303
|
+
# The scaffolded pre-commit resolves `safedeps` via PATH, then SAFEDEPS_BIN, then
|
|
304
|
+
# the skill install paths. In CI none of those exist, so point it at this repo's
|
|
305
|
+
# binary; the git commit subprocess inherits the env and the hook resolves it.
|
|
306
|
+
export SAFEDEPS_BIN="${ROOT_DIR}/bin/safedeps"
|
|
307
|
+
|
|
308
|
+
if command -v gitleaks >/dev/null 2>&1 && command -v openssl >/dev/null 2>&1; then
|
|
309
|
+
# Regression: a clean file commits cleanly.
|
|
310
|
+
echo "hello" > "${secret_repo}/readme.txt"
|
|
311
|
+
git -C "${secret_repo}" add readme.txt
|
|
312
|
+
git -C "${secret_repo}" commit -q -m "clean" || fail "pre-commit allows a clean commit"
|
|
313
|
+
|
|
314
|
+
# Threat: a literal .env with an assigned (synthetic) secret must be blocked.
|
|
315
|
+
printf 'API_KEY=%s\n' "$(openssl rand -hex 20)" > "${secret_repo}/.env"
|
|
316
|
+
git -C "${secret_repo}" add .env
|
|
317
|
+
if git -C "${secret_repo}" commit -q -m "leak" 2>/dev/null; then
|
|
318
|
+
fail "pre-commit blocks a committed .env secret"
|
|
319
|
+
fi
|
|
320
|
+
git -C "${secret_repo}" reset -q HEAD .env >/dev/null 2>&1 || true
|
|
321
|
+
|
|
322
|
+
# Regression: the .env.example placeholder is allowlisted and commits.
|
|
323
|
+
printf 'API_KEY=your_api_key_here\n' > "${secret_repo}/.env.example"
|
|
324
|
+
git -C "${secret_repo}" add .env.example
|
|
325
|
+
git -C "${secret_repo}" commit -q -m "example" || fail "pre-commit allows the .env.example placeholder"
|
|
326
|
+
pass "pre-commit gate denies a secret, passes clean and example commits"
|
|
327
|
+
else
|
|
328
|
+
printf 'ok - pre-commit gate behavior SKIPPED (needs gitleaks + openssl)\n'
|
|
329
|
+
fi
|
|
330
|
+
|
|
107
331
|
printf 'e2e passed\n'
|
|
@@ -55,6 +55,17 @@ function osvResponse(packageName, version) {
|
|
|
55
55
|
if (packageName === 'fixture-vuln' && version === '1.0.0') {
|
|
56
56
|
return { vulns: [osvVuln('CVE-2026-1001', '1.0.1')] };
|
|
57
57
|
}
|
|
58
|
+
if (packageName === 'fixture-multi-vuln' && version === '1.0.0') {
|
|
59
|
+
return {
|
|
60
|
+
vulns: [
|
|
61
|
+
osvVuln('CVE-2026-1003', '1.0.1'),
|
|
62
|
+
osvVuln('CVE-2026-1004', '1.0.5')
|
|
63
|
+
]
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (packageName === 'fixture-multi-vuln' && version === '1.0.1') {
|
|
67
|
+
return { vulns: [osvVuln('CVE-2026-1004', '1.0.5')] };
|
|
68
|
+
}
|
|
58
69
|
if (packageName === 'fixture-unpatched') {
|
|
59
70
|
return { vulns: [osvVuln('CVE-2026-1002', null)] };
|
|
60
71
|
}
|
|
@@ -74,6 +85,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
74
85
|
return;
|
|
75
86
|
}
|
|
76
87
|
|
|
88
|
+
if (req.method === 'POST' && req.url === '/osv/v1/querybatch') {
|
|
89
|
+
const body = await readJson(req);
|
|
90
|
+
const queries = Array.isArray(body.queries) ? body.queries : [];
|
|
91
|
+
res.setHeader('content-type', 'application/json');
|
|
92
|
+
res.end(JSON.stringify({
|
|
93
|
+
results: queries.map((query) => osvResponse(query.package?.name || '', query.version || ''))
|
|
94
|
+
}));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
77
98
|
if (req.method === 'GET' && req.url === '/kev.json') {
|
|
78
99
|
res.setHeader('content-type', 'application/json');
|
|
79
100
|
res.end(JSON.stringify({
|
package/scripts/test/smoke.sh
CHANGED
|
@@ -22,9 +22,16 @@ trap cleanup EXIT
|
|
|
22
22
|
bash -n bin/safedeps
|
|
23
23
|
bash -n lib/providers/providers.sh
|
|
24
24
|
bash -n lib/ledger/ledger.sh
|
|
25
|
+
bash -n lib/npm/closure.sh
|
|
25
26
|
bash -n scripts/safedeps-pre-guard.sh
|
|
26
27
|
bash -n scripts/safedeps-post-verify.sh
|
|
27
28
|
bash -n scripts/safedeps-recheck-alert.sh
|
|
29
|
+
bash -n scripts/release-gates.sh
|
|
30
|
+
bash -n lib/gates/repo-profile.sh
|
|
31
|
+
bash -n lib/gates/scan.sh
|
|
32
|
+
bash -n lib/gates/audit.sh
|
|
33
|
+
bash -n lib/gates/hooks.sh
|
|
34
|
+
bash -n lib/gates/doctor.sh
|
|
28
35
|
pass "bash syntax"
|
|
29
36
|
|
|
30
37
|
node --check scripts/install/install-safedeps-hooks.mjs >/dev/null
|
|
@@ -35,7 +42,8 @@ node scripts/install/install-safedeps-recheck-agent.mjs --help >/dev/null
|
|
|
35
42
|
pass "node syntax"
|
|
36
43
|
|
|
37
44
|
version_json=$(HOME="${tmp_root}/home-version" SAFEDEPS_HOME="${tmp_root}/safe-version" ./bin/safedeps --json version)
|
|
38
|
-
|
|
45
|
+
pkg_version=$(jq -r '.version' package.json)
|
|
46
|
+
[[ "$(jq -r '.version' <<< "${version_json}")" == "${pkg_version}" ]] || fail "cli version matches package.json (${pkg_version})"
|
|
39
47
|
pass "cli version"
|
|
40
48
|
|
|
41
49
|
ledger_json=$(HOME="${tmp_root}/home-ledger" SAFEDEPS_HOME="${tmp_root}/safe-ledger" ./bin/safedeps --json ledger)
|
|
@@ -51,15 +59,39 @@ provider_created=$(
|
|
|
51
59
|
[[ "${provider_created}" == "${provider_tmp%/}/safedeps-providers."* ]] || fail "provider tmp helper uses requested TMPDIR"
|
|
52
60
|
pass "provider temp dir"
|
|
53
61
|
|
|
62
|
+
# Portability guard: safedeps_file_mtime must return a bare integer on both BSD
|
|
63
|
+
# (macOS, `stat -f`) and GNU (Linux, `stat -c`). A wrong-order stat leaks
|
|
64
|
+
# filesystem info into the value and breaks the cache-freshness arithmetic.
|
|
65
|
+
mtime_val=$(bash -c 'source lib/providers/providers.sh; f=$(mktemp); safedeps_file_mtime "$f"; rm -f "$f"')
|
|
66
|
+
[[ "${mtime_val}" =~ ^[0-9]+$ ]] || fail "safedeps_file_mtime returns a bare integer (got: ${mtime_val})"
|
|
67
|
+
pass "file mtime is a portable integer"
|
|
68
|
+
|
|
54
69
|
project_dir="${tmp_root}/project"
|
|
55
70
|
mkdir -p "${project_dir}"
|
|
56
71
|
printf '{"dependencies":{}}\n' > "${project_dir}/package.json"
|
|
57
72
|
|
|
73
|
+
run_hook_command() {
|
|
74
|
+
local home_dir="$1"
|
|
75
|
+
local safe_dir="$2"
|
|
76
|
+
local command="$3"
|
|
77
|
+
|
|
78
|
+
jq -nc --arg command "${command}" --arg cwd "${project_dir}" \
|
|
79
|
+
'{tool_name:"Bash",tool_input:{command:$command},cwd:$cwd}' |
|
|
80
|
+
HOME="${home_dir}" SAFEDEPS_HOME="${safe_dir}" scripts/safedeps-pre-guard.sh
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
run_codex_hook_command() {
|
|
84
|
+
local home_dir="$1"
|
|
85
|
+
local safe_dir="$2"
|
|
86
|
+
local command="$3"
|
|
87
|
+
|
|
88
|
+
jq -nc --arg command "${command}" --arg cwd "${project_dir}" \
|
|
89
|
+
'{tool_name:"Bash",tool_input:{command:$command},cwd:$cwd,turn_id:"turn-smoke",model:"codex-test"}' |
|
|
90
|
+
HOME="${home_dir}" SAFEDEPS_HOME="${safe_dir}" scripts/safedeps-pre-guard.sh
|
|
91
|
+
}
|
|
92
|
+
|
|
58
93
|
deny_json=$(
|
|
59
|
-
|
|
60
|
-
scripts/safedeps-pre-guard.sh <<EOF
|
|
61
|
-
{"tool_name":"Bash","tool_input":{"command":"npm install left-pad@1.3.0"},"cwd":"${project_dir}"}
|
|
62
|
-
EOF
|
|
94
|
+
run_hook_command "${tmp_root}/home-hook" "${tmp_root}/safe-hook" "npm install left-pad@1.3.0"
|
|
63
95
|
)
|
|
64
96
|
[[ "$(jq -r '.hookSpecificOutput.permissionDecision' <<< "${deny_json}")" == "deny" ]] || fail "hook denies unapproved install"
|
|
65
97
|
pass "hook denies unapproved install"
|
|
@@ -67,13 +99,148 @@ pass "hook denies unapproved install"
|
|
|
67
99
|
mkdir -p "${tmp_root}/safe-hook-allow"
|
|
68
100
|
SAFEDEPS_HOME="${tmp_root}/safe-hook-allow" lib/ledger/ledger.sh approve npm left-pad 1.3.0 1.3.0 smoke >/dev/null
|
|
69
101
|
allow_output=$(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
102
|
+
run_hook_command "${tmp_root}/home-hook-allow" "${tmp_root}/safe-hook-allow" "npm install left-pad@1.3.0"
|
|
103
|
+
)
|
|
104
|
+
[[ "$(jq -r '.hookSpecificOutput.permissionDecision' <<< "${allow_output}")" == "allow" ]] || fail "hook emits Claude allow decision for approved install"
|
|
105
|
+
[[ "$(jq -r '.hookSpecificOutput.updatedInput.command' <<< "${allow_output}")" == "npm install left-pad@1.3.0 --ignore-scripts" ]] || fail "hook injects --ignore-scripts for Claude npm install"
|
|
106
|
+
allow_sid=$(jq -r '.snapshot_id' "${tmp_root}/safe-hook-allow/current_state")
|
|
107
|
+
jq -e '.ignore_scripts_injected == true' "${tmp_root}/safe-hook-allow/snapshots/${allow_sid}_meta.json" >/dev/null || fail "hook records injected meta flag"
|
|
108
|
+
pass "hook injects --ignore-scripts for Claude approved install"
|
|
109
|
+
|
|
110
|
+
mkdir -p "${tmp_root}/safe-hook-codex"
|
|
111
|
+
SAFEDEPS_HOME="${tmp_root}/safe-hook-codex" lib/ledger/ledger.sh approve npm left-pad 1.3.0 1.3.0 smoke >/dev/null
|
|
112
|
+
codex_allow_output=$(
|
|
113
|
+
run_codex_hook_command "${tmp_root}/home-hook-codex" "${tmp_root}/safe-hook-codex" "npm install left-pad@1.3.0"
|
|
114
|
+
)
|
|
115
|
+
[[ -z "${codex_allow_output}" ]] || fail "hook keeps Codex approved install as plain allow"
|
|
116
|
+
codex_sid=$(jq -r '.snapshot_id' "${tmp_root}/safe-hook-codex/current_state")
|
|
117
|
+
jq -e '.ignore_scripts_injected == false' "${tmp_root}/safe-hook-codex/snapshots/${codex_sid}_meta.json" >/dev/null || fail "hook does not record injected meta flag for Codex"
|
|
118
|
+
pass "hook keeps Codex approved install as plain allow"
|
|
119
|
+
|
|
120
|
+
for inert_skip_cmd in "npm view left-pad" "npm run build" "npm --version"; do
|
|
121
|
+
inert_skip_output=$(run_hook_command "${tmp_root}/home-inert-skip" "${tmp_root}/safe-inert-skip" "${inert_skip_cmd}")
|
|
122
|
+
[[ -z "${inert_skip_output}" ]] || fail "hook does not inject non-install command: ${inert_skip_cmd}"
|
|
123
|
+
done
|
|
124
|
+
pass "hook does not inject npm non-install commands"
|
|
125
|
+
|
|
126
|
+
mkdir -p "${tmp_root}/safe-hook-ignore-scripts"
|
|
127
|
+
SAFEDEPS_HOME="${tmp_root}/safe-hook-ignore-scripts" lib/ledger/ledger.sh approve npm left-pad 1.3.0 1.3.0 smoke >/dev/null
|
|
128
|
+
ignore_scripts_output=$(
|
|
129
|
+
run_hook_command "${tmp_root}/home-hook-ignore-scripts" "${tmp_root}/safe-hook-ignore-scripts" "npm install left-pad@1.3.0 --ignore-scripts"
|
|
130
|
+
)
|
|
131
|
+
[[ -z "${ignore_scripts_output}" ]] || fail "hook does not duplicate --ignore-scripts"
|
|
132
|
+
ignore_sid=$(jq -r '.snapshot_id' "${tmp_root}/safe-hook-ignore-scripts/current_state")
|
|
133
|
+
jq -e '.ignore_scripts_injected == false' "${tmp_root}/safe-hook-ignore-scripts/snapshots/${ignore_sid}_meta.json" >/dev/null || fail "hook does not record injected meta flag when flag already exists"
|
|
134
|
+
pass "hook does not duplicate --ignore-scripts"
|
|
135
|
+
|
|
136
|
+
# Regression: `npx <tool> <args>` runs an already-installed binary. Arguments to
|
|
137
|
+
# the tool (e.g. an email) must NOT be misread as a pkg@spec install and denied.
|
|
138
|
+
npx_runner_output=$(
|
|
139
|
+
run_hook_command "${tmp_root}/home-npx-run" "${tmp_root}/safe-npx-run" "npx wrangler secret put ORIGIN_SHARED_SECRET --name pqc-auth-gateway dev1@block-s.io"
|
|
140
|
+
)
|
|
141
|
+
[[ -z "${npx_runner_output}" ]] || fail "hook allows npx tool run with @-bearing args"
|
|
142
|
+
pass "hook allows npx tool run with @-bearing args"
|
|
143
|
+
|
|
144
|
+
# Regression: a genuine install chained with an npx tool run must STILL be gated
|
|
145
|
+
# on the real package — and must not be polluted by the npx arg email.
|
|
146
|
+
mixed_output=$(
|
|
147
|
+
run_hook_command "${tmp_root}/home-mixed" "${tmp_root}/safe-mixed" "npm install evil-pkg@9.9.9 && npx wrangler secret put X dev1@block-s.io"
|
|
148
|
+
)
|
|
149
|
+
[[ "$(jq -r '.hookSpecificOutput.permissionDecision' <<< "${mixed_output}")" == "deny" ]] || fail "hook gates real install chained with npx run"
|
|
150
|
+
reason=$(jq -r '.hookSpecificOutput.permissionDecisionReason' <<< "${mixed_output}")
|
|
151
|
+
grep -q 'evil-pkg@9.9.9' <<< "${reason}" || fail "deny reason names the real package"
|
|
152
|
+
[[ "${reason}" != *"dev1@block-s.io"* ]] || fail "deny reason must not name the email arg"
|
|
153
|
+
pass "hook gates real install chained with npx run (email not polluted)"
|
|
154
|
+
|
|
155
|
+
echo_output=$(
|
|
156
|
+
run_hook_command "${tmp_root}/home-echo" "${tmp_root}/safe-echo" "echo \"npm install evil-pkg@9.9.9\""
|
|
157
|
+
)
|
|
158
|
+
[[ -z "${echo_output}" ]] || fail "hook ignores quoted echo text"
|
|
159
|
+
heredoc_output=$(
|
|
160
|
+
run_hook_command "${tmp_root}/home-heredoc" "${tmp_root}/safe-heredoc" $'cat <<'\''EOF'\''\nnpm install evil-pkg@9.9.9\nEOF'
|
|
161
|
+
)
|
|
162
|
+
[[ -z "${heredoc_output}" ]] || fail "hook ignores heredoc body text"
|
|
163
|
+
pass "hook ignores echo/heredoc text"
|
|
164
|
+
|
|
165
|
+
bypass_cases=(
|
|
166
|
+
"/usr/bin/npm install evil@1.2.3"
|
|
167
|
+
"bash -lc \"npm install evil@1.2.3\""
|
|
168
|
+
"env npm install evil@1.2.3"
|
|
169
|
+
"command npm install evil@1.2.3"
|
|
170
|
+
"npm --prefix sub install evil@1.2.3"
|
|
171
|
+
"pip install requests==2.31.0"
|
|
172
|
+
"gem install rails -v 7.1.0"
|
|
173
|
+
"cargo add serde --vers 1.0.0"
|
|
174
|
+
"dotnet add package X --version 1.0.0"
|
|
175
|
+
)
|
|
176
|
+
for bypass_cmd in "${bypass_cases[@]}"; do
|
|
177
|
+
bypass_output=$(run_hook_command "${tmp_root}/home-bypass" "${tmp_root}/safe-bypass" "${bypass_cmd}")
|
|
178
|
+
[[ "$(jq -r '.hookSpecificOutput.permissionDecision' <<< "${bypass_output}")" == "deny" ]] || fail "hook denies bypass: ${bypass_cmd}"
|
|
179
|
+
done
|
|
180
|
+
pass "hook denies install bypass forms"
|
|
181
|
+
|
|
182
|
+
# Fail-closed gate: when the gate cannot run it must NOT silently pass, and the
|
|
183
|
+
# outcome must be observable in the advisory log (AGENTS.md: no silent fallback).
|
|
184
|
+
fc_safe="${tmp_root}/safe-failclosed"
|
|
185
|
+
fc_home="${tmp_root}/home-failclosed"
|
|
186
|
+
mkdir -p "${fc_safe}"
|
|
187
|
+
# (a) lock unavailable on an install command → DENY (fail-closed), logged.
|
|
188
|
+
mkdir -p "${fc_safe}/state.lock"
|
|
189
|
+
fc_deny=$(
|
|
190
|
+
jq -nc --arg c "npm install evil@1.0.0" --arg cwd "${project_dir}" \
|
|
191
|
+
'{tool_name:"Bash",tool_input:{command:$c},cwd:$cwd}' |
|
|
192
|
+
HOME="${fc_home}" SAFEDEPS_HOME="${fc_safe}" SAFEDEPS_LOCK_MAX_ATTEMPTS=2 scripts/safedeps-pre-guard.sh
|
|
193
|
+
)
|
|
194
|
+
rmdir "${fc_safe}/state.lock" 2>/dev/null || true
|
|
195
|
+
[[ "$(jq -r '.hookSpecificOutput.permissionDecision' <<< "${fc_deny}")" == "deny" ]] || fail "pre-guard fails closed (deny) when the state lock is unavailable for an install"
|
|
196
|
+
grep -q 'pre-guard DENY' "${fc_safe}/advisory.log" || fail "pre-guard logs the fail-closed deny to advisory.log"
|
|
197
|
+
pass "pre-guard fails closed on lock contention (observable)"
|
|
198
|
+
|
|
199
|
+
# (b) jq missing → best-effort fail-closed: a likely install DENIES, a non-install
|
|
200
|
+
# is allowed, both recorded in advisory.log (never a silent skip).
|
|
201
|
+
fc_nojq=$(mktemp -d "${tmp_root}/nojq.XXXXXX")
|
|
202
|
+
for fc_tool in bash mkdir date printf cat grep; do
|
|
203
|
+
ln -sf "$(command -v "${fc_tool}")" "${fc_nojq}/${fc_tool}" 2>/dev/null || true
|
|
204
|
+
done
|
|
205
|
+
fc_nojq_deny=$(
|
|
206
|
+
jq -nc --arg c "npm install x@1" --arg cwd "${project_dir}" '{tool_name:"Bash",tool_input:{command:$c},cwd:$cwd}' |
|
|
207
|
+
HOME="${fc_home}" SAFEDEPS_HOME="${fc_safe}" PATH="${fc_nojq}" scripts/safedeps-pre-guard.sh 2>/dev/null
|
|
208
|
+
)
|
|
209
|
+
[[ "$(jq -r '.hookSpecificOutput.permissionDecision' <<< "${fc_nojq_deny}")" == "deny" ]] || fail "pre-guard denies a likely install when jq is missing (best-effort fail-closed)"
|
|
210
|
+
grep -q 'DENY: jq missing' "${fc_safe}/advisory.log" || fail "pre-guard logs the jq-missing install deny to advisory.log"
|
|
211
|
+
fc_nojq_allow=$(
|
|
212
|
+
jq -nc --arg c "ls -la" --arg cwd "${project_dir}" '{tool_name:"Bash",tool_input:{command:$c},cwd:$cwd}' |
|
|
213
|
+
HOME="${fc_home}" SAFEDEPS_HOME="${fc_safe}" PATH="${fc_nojq}" scripts/safedeps-pre-guard.sh 2>/dev/null
|
|
214
|
+
)
|
|
215
|
+
[[ "$(jq -r '.hookSpecificOutput.permissionDecision // "allow"' <<< "${fc_nojq_allow}" 2>/dev/null || echo allow)" != "deny" ]] || fail "pre-guard allows a non-install command when jq is missing"
|
|
216
|
+
pass "pre-guard fails closed on jq-missing installs, allows non-installs (observable)"
|
|
217
|
+
|
|
218
|
+
# (c) ledger library missing → DENY (fail-closed), logged — not a silent fall-through allow.
|
|
219
|
+
fc_noledger=$(
|
|
220
|
+
jq -nc --arg c "npm install x@1" --arg cwd "${project_dir}" '{tool_name:"Bash",tool_input:{command:$c},cwd:$cwd}' |
|
|
221
|
+
HOME="${fc_home}" SAFEDEPS_HOME="${fc_safe}" SAFEDEPS_LEDGER_LIB="${tmp_root}/does-not-exist.sh" scripts/safedeps-pre-guard.sh 2>/dev/null
|
|
222
|
+
)
|
|
223
|
+
[[ "$(jq -r '.hookSpecificOutput.permissionDecision' <<< "${fc_noledger}")" == "deny" ]] || fail "pre-guard denies an install when the ledger library is missing (fail-closed)"
|
|
224
|
+
grep -q 'ledger library missing' "${fc_safe}/advisory.log" || fail "pre-guard logs the missing-ledger deny to advisory.log"
|
|
225
|
+
pass "pre-guard fails closed when the ledger library is missing (observable)"
|
|
226
|
+
|
|
227
|
+
tamper_safe="${tmp_root}/safe-tamper"
|
|
228
|
+
tamper_home="${tmp_root}/home-tamper"
|
|
229
|
+
SAFEDEPS_HOME="${tamper_safe}" lib/ledger/ledger.sh approve npm ledger-tamper 1.0.0 1.0.0 smoke >/dev/null
|
|
230
|
+
tamper_pre=$(run_hook_command "${tamper_home}" "${tamper_safe}" "npm install ledger-tamper@1.0.0")
|
|
231
|
+
[[ "$(jq -r '.hookSpecificOutput.permissionDecision' <<< "${tamper_pre}")" == "allow" ]] || fail "tamper fixture pre hook allows approved install"
|
|
232
|
+
mkdir -p "${project_dir}/node_modules/ledger-tamper"
|
|
233
|
+
jq '.dependencies["ledger-tamper"]="1.0.0"' "${project_dir}/package.json" > "${project_dir}/package.json.tmp"
|
|
234
|
+
mv "${project_dir}/package.json.tmp" "${project_dir}/package.json"
|
|
235
|
+
cat > "${project_dir}/node_modules/ledger-tamper/package.json" <<'EOF'
|
|
236
|
+
{"name":"ledger-tamper","version":"1.0.0","scripts":{"postinstall":"node -e \"require('fs').writeFileSync(process.env.HOME + '/.safedeps/approved-specs/evil.json', '{}')\""}}
|
|
73
237
|
EOF
|
|
238
|
+
tamper_post=$(
|
|
239
|
+
jq -nc '{tool_name:"Bash",tool_input:{command:"npm install ledger-tamper@1.0.0"}}' |
|
|
240
|
+
HOME="${tamper_home}" SAFEDEPS_HOME="${tamper_safe}" scripts/safedeps-post-verify.sh
|
|
74
241
|
)
|
|
75
|
-
|
|
76
|
-
pass "hook
|
|
242
|
+
grep -q '의심스러운 패키지 변경 감지' <<< "${tamper_post}" || fail "post hook reorgs safedeps ledger tamper script"
|
|
243
|
+
pass "post hook reorgs safedeps ledger tamper script"
|
|
77
244
|
|
|
78
245
|
fixture_json="${tmp_root}/recheck-fixture.json"
|
|
79
246
|
printf '%s\n' '{"command":"re-check","checked":2,"still_clean":1,"newly_vulnerable":[],"kev_hit":[],"revoked":[]}' > "${fixture_json}"
|
|
@@ -86,4 +253,39 @@ grep -q '"checked":2' "${tmp_root}/safe-recheck/recheck.log" || fail "re-check w
|
|
|
86
253
|
grep -q '"provider_skipped":1' "${tmp_root}/safe-recheck/recheck-alerts.jsonl" || fail "re-check wrapper alerts on skipped provider checks"
|
|
87
254
|
pass "re-check alert wrapper"
|
|
88
255
|
|
|
256
|
+
# Release-time lane (absorbed from security-release-gates): commands must be
|
|
257
|
+
# registered and resolve their gate scripts.
|
|
258
|
+
gates_help=$(HOME="${tmp_root}/home-gates" SAFEDEPS_HOME="${tmp_root}/safe-gates" ./bin/safedeps help)
|
|
259
|
+
for gate_cmd in "gates" "scan secrets" "audit" "hooks" "doctor"; do
|
|
260
|
+
grep -q "${gate_cmd}" <<< "${gates_help}" || fail "release-time command listed in help: ${gate_cmd}"
|
|
261
|
+
done
|
|
262
|
+
for gate_script in scripts/release-gates.sh lib/gates/repo-profile.sh lib/gates/scan.sh lib/gates/audit.sh lib/gates/hooks.sh lib/gates/doctor.sh; do
|
|
263
|
+
[[ -f "${gate_script}" ]] || fail "release-time gate script present: ${gate_script}"
|
|
264
|
+
done
|
|
265
|
+
for tmpl in gitleaks.toml.tmpl gitleaks.private.toml.tmpl pre-commit.tmpl; do
|
|
266
|
+
[[ -f "lib/gates/templates/${tmpl}" ]] || fail "secret-lane template present: ${tmpl}"
|
|
267
|
+
done
|
|
268
|
+
pass "release-time gate commands registered"
|
|
269
|
+
|
|
270
|
+
# Secret-leak lane: doctor diagnoses, hooks init scaffolds, hooks install
|
|
271
|
+
# activates. No scanner (gitleaks/docker) needed for these structural checks.
|
|
272
|
+
doctor_repo=$(mktemp -d "${tmp_root}/secret-repo.XXXXXX")
|
|
273
|
+
git -C "${doctor_repo}" init -q
|
|
274
|
+
# doctor exits 1 when gaps exist; capture the JSON without tripping set -e.
|
|
275
|
+
doctor_json=$(HOME="${tmp_root}/home-doctor" ./bin/safedeps --json doctor --root "${doctor_repo}" || true)
|
|
276
|
+
[[ "$(jq -r '.command' <<< "${doctor_json}")" == "doctor" ]] || fail "doctor --json command field"
|
|
277
|
+
[[ "$(jq -r '.ok' <<< "${doctor_json}")" == "false" ]] || fail "doctor reports gaps on a bare repo"
|
|
278
|
+
secret_gaps=$(jq -r '[.checks[] | select(.lane == "secret" and .status == "gap")] | length' <<< "${doctor_json}")
|
|
279
|
+
[[ "${secret_gaps}" -ge 3 ]] || fail "doctor lists at least 3 secret-lane gaps (got ${secret_gaps})"
|
|
280
|
+
HOME="${tmp_root}/home-doctor" ./bin/safedeps hooks init --root "${doctor_repo}" >/dev/null
|
|
281
|
+
[[ -f "${doctor_repo}/.gitleaks.toml" ]] || fail "hooks init scaffolds .gitleaks.toml"
|
|
282
|
+
[[ -x "${doctor_repo}/.githooks/pre-commit" ]] || fail "hooks init scaffolds an executable pre-commit"
|
|
283
|
+
grep -q 'scan secrets --staged' "${doctor_repo}/.githooks/pre-commit" || fail "pre-commit delegates to safedeps scan"
|
|
284
|
+
printf '\n# repo-owned edit marker\n' >> "${doctor_repo}/.gitleaks.toml"
|
|
285
|
+
HOME="${tmp_root}/home-doctor" ./bin/safedeps hooks init --root "${doctor_repo}" >/dev/null
|
|
286
|
+
grep -q 'repo-owned edit marker' "${doctor_repo}/.gitleaks.toml" || fail "hooks init is non-destructive (keeps repo edits)"
|
|
287
|
+
HOME="${tmp_root}/home-doctor" ./bin/safedeps hooks install --root "${doctor_repo}" >/dev/null
|
|
288
|
+
[[ "$(git -C "${doctor_repo}" config --get core.hooksPath)" == ".githooks" ]] || fail "hooks install activates core.hooksPath"
|
|
289
|
+
pass "doctor + hooks init/install wire the secret lane (non-destructive)"
|
|
290
|
+
|
|
89
291
|
printf 'smoke passed\n'
|