@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.
@@ -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'