@aldegad/safedeps 2.2.0 → 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 +5 -1
- package/README.ko.md +42 -0
- package/README.md +42 -0
- package/ROADMAP.md +46 -2
- package/SECURITY.md +45 -0
- package/SKILL.md +84 -147
- package/bin/safedeps +36 -2
- package/lib/gates/doctor.sh +212 -0
- package/lib/gates/hooks.sh +40 -2
- 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/providers/providers.sh +4 -1
- package/package.json +2 -1
- package/scripts/install/install-safedeps-hooks.mjs +3 -0
- package/scripts/safedeps-post-verify.sh +18 -5
- package/scripts/safedeps-pre-guard.sh +39 -7
- package/scripts/test/e2e.sh +48 -0
- package/scripts/test/smoke.sh +79 -2
package/scripts/test/smoke.sh
CHANGED
|
@@ -31,6 +31,7 @@ bash -n lib/gates/repo-profile.sh
|
|
|
31
31
|
bash -n lib/gates/scan.sh
|
|
32
32
|
bash -n lib/gates/audit.sh
|
|
33
33
|
bash -n lib/gates/hooks.sh
|
|
34
|
+
bash -n lib/gates/doctor.sh
|
|
34
35
|
pass "bash syntax"
|
|
35
36
|
|
|
36
37
|
node --check scripts/install/install-safedeps-hooks.mjs >/dev/null
|
|
@@ -58,6 +59,13 @@ provider_created=$(
|
|
|
58
59
|
[[ "${provider_created}" == "${provider_tmp%/}/safedeps-providers."* ]] || fail "provider tmp helper uses requested TMPDIR"
|
|
59
60
|
pass "provider temp dir"
|
|
60
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
|
+
|
|
61
69
|
project_dir="${tmp_root}/project"
|
|
62
70
|
mkdir -p "${project_dir}"
|
|
63
71
|
printf '{"dependencies":{}}\n' > "${project_dir}/package.json"
|
|
@@ -171,6 +179,51 @@ for bypass_cmd in "${bypass_cases[@]}"; do
|
|
|
171
179
|
done
|
|
172
180
|
pass "hook denies install bypass forms"
|
|
173
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
|
+
|
|
174
227
|
tamper_safe="${tmp_root}/safe-tamper"
|
|
175
228
|
tamper_home="${tmp_root}/home-tamper"
|
|
176
229
|
SAFEDEPS_HOME="${tamper_safe}" lib/ledger/ledger.sh approve npm ledger-tamper 1.0.0 1.0.0 smoke >/dev/null
|
|
@@ -203,12 +256,36 @@ pass "re-check alert wrapper"
|
|
|
203
256
|
# Release-time lane (absorbed from security-release-gates): commands must be
|
|
204
257
|
# registered and resolve their gate scripts.
|
|
205
258
|
gates_help=$(HOME="${tmp_root}/home-gates" SAFEDEPS_HOME="${tmp_root}/safe-gates" ./bin/safedeps help)
|
|
206
|
-
for gate_cmd in "gates" "scan secrets" "audit" "hooks"; do
|
|
259
|
+
for gate_cmd in "gates" "scan secrets" "audit" "hooks" "doctor"; do
|
|
207
260
|
grep -q "${gate_cmd}" <<< "${gates_help}" || fail "release-time command listed in help: ${gate_cmd}"
|
|
208
261
|
done
|
|
209
|
-
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; do
|
|
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
|
|
210
263
|
[[ -f "${gate_script}" ]] || fail "release-time gate script present: ${gate_script}"
|
|
211
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
|
|
212
268
|
pass "release-time gate commands registered"
|
|
213
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
|
+
|
|
214
291
|
printf 'smoke passed\n'
|