@aldegad/safedeps 2.1.1 → 2.2.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 +268 -462
- package/README.ko.md +34 -12
- package/README.md +65 -38
- package/ROADMAP.md +82 -87
- package/SKILL.md +13 -7
- package/bin/safedeps +385 -52
- package/lib/gates/audit.sh +36 -0
- package/lib/gates/hooks.sh +93 -0
- package/lib/gates/repo-profile.sh +60 -0
- package/lib/gates/scan.sh +94 -0
- package/lib/ledger/ledger.sh +94 -16
- package/lib/npm/closure.sh +115 -0
- package/lib/providers/providers.sh +244 -25
- package/package.json +1 -1
- package/scripts/install/install-safedeps-hooks.mjs +62 -23
- package/scripts/release-gates.sh +252 -0
- package/scripts/safedeps-post-verify.sh +167 -10
- package/scripts/safedeps-pre-guard.sh +270 -32
- package/scripts/test/e2e.sh +180 -4
- package/scripts/test/fixture-provider.mjs +21 -0
- package/scripts/test/smoke.sh +135 -10
|
@@ -76,10 +76,16 @@ release_state_lock() {
|
|
|
76
76
|
write_state_file() {
|
|
77
77
|
local target_path="$1"
|
|
78
78
|
local value="$2"
|
|
79
|
-
local
|
|
80
|
-
|
|
79
|
+
local target_dir
|
|
80
|
+
local target_base
|
|
81
|
+
local temp_path
|
|
82
|
+
|
|
83
|
+
target_dir=$(dirname "${target_path}")
|
|
84
|
+
target_base=$(basename "${target_path}")
|
|
85
|
+
mkdir -p "${target_dir}" || return 1
|
|
86
|
+
temp_path=$(mktemp "${target_dir}/.${target_base}.XXXXXX") || return 1
|
|
81
87
|
printf '%s\n' "${value}" > "${temp_path}"
|
|
82
|
-
mv "${temp_path}" "${target_path}"
|
|
88
|
+
mv -f "${temp_path}" "${target_path}"
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
compute_dir_hash() {
|
|
@@ -99,10 +105,13 @@ command_is_dependency_install() {
|
|
|
99
105
|
local scan_command
|
|
100
106
|
local install_pattern
|
|
101
107
|
|
|
102
|
-
|
|
103
|
-
install_pattern='(^|[;&|]+[[:space:]]*)((npm[[:space:]]+(install|i|add|update|up|upgrade))|npx[[:space:]]|pnpm[[:space:]]+(add|install|update|up|dlx)|yarn[[:space:]]+(add|install|upgrade|dlx)|((python3?|py)[[:space:]]+-m[[:space:]]+pip|pip3?)[[:space:]]+install|poetry[[:space:]]+add|uv[[:space:]]+(add|pip[[:space:]]+install)|pipenv[[:space:]]+install|cargo[[:space:]]+(add|install)|go[[:space:]]+(get|install)|gem[[:space:]]+install|bundle[[:space:]]+add|mvn[[:space:]]+dependency:get|dotnet[[:space:]]+add[[:space:]]+package)([[:space:]]|$)'
|
|
108
|
+
install_pattern='(^|[;&|]+[[:space:]]*)((npm([[:space:]]+--?[a-zA-Z0-9_-]+([=[:space:]][^[:space:]]+)?)?[[:space:]]+(install|i|add|ci|update|up|upgrade))|npx[[:space:]]|pnpm([[:space:]]+--?[a-zA-Z0-9_-]+([=[:space:]][^[:space:]]+)?)?[[:space:]]+(add|install|update|up|dlx)|yarn([[:space:]]+--?[a-zA-Z0-9_-]+([=[:space:]][^[:space:]]+)?)?[[:space:]]+(add|install|upgrade|dlx)|((python3?|py)[[:space:]]+-m[[:space:]]+pip|pip3?)[[:space:]]+install|poetry[[:space:]]+add|uv[[:space:]]+(add|pip[[:space:]]+install)|pipenv[[:space:]]+install|cargo[[:space:]]+(add|install)|go[[:space:]]+(get|install)|gem[[:space:]]+install|bundle[[:space:]]+add|mvn[[:space:]]+dependency:get|dotnet[[:space:]]+add[[:space:]]+package)([[:space:]]|$)'
|
|
104
109
|
|
|
105
|
-
|
|
110
|
+
while IFS= read -r scan_command; do
|
|
111
|
+
scan_command=$(command_scan_text "${scan_command}")
|
|
112
|
+
echo "${scan_command}" | grep -qEi "${install_pattern}" && return 0
|
|
113
|
+
done < <(command_candidate_texts "${command}")
|
|
114
|
+
return 1
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
command_hides_dependency_install() {
|
|
@@ -113,7 +122,7 @@ command_hides_dependency_install() {
|
|
|
113
122
|
manager_pattern='(npm|npx|pnpm|yarn|pip3?|python3?[[:space:]]+-m[[:space:]]+pip|poetry|uv|pipenv|cargo|go|gem|bundle|mvn|dotnet)'
|
|
114
123
|
verb_pattern='(install|i|add|update|up|upgrade|dlx|get|dependency:get|package)'
|
|
115
124
|
|
|
116
|
-
echo "${command}" | grep -qEi '(eval[[:space:]]|\$\(
|
|
125
|
+
echo "${command}" | grep -qEi '(eval[[:space:]]|\$\(|`|(^|[[:space:];|&])(bash|sh|zsh)[[:space:]]+-[A-Za-z]*c[[:space:]])' && \
|
|
117
126
|
echo "${command}" | grep -qEi "${manager_pattern}.*${verb_pattern}"
|
|
118
127
|
}
|
|
119
128
|
|
|
@@ -154,6 +163,93 @@ command_scan_text() {
|
|
|
154
163
|
printf '%s' "${output}"
|
|
155
164
|
}
|
|
156
165
|
|
|
166
|
+
normalize_install_text() {
|
|
167
|
+
local text="$1"
|
|
168
|
+
|
|
169
|
+
for _ in 1 2 3; do
|
|
170
|
+
text=$(printf '%s' "${text}" | sed -E \
|
|
171
|
+
-e 's#(^|[[:space:];|&])(/[^[:space:];|&]+/)(npm|npx|pnpm|yarn|pip3?|python3?|py|poetry|uv|pipenv|cargo|go|gem|bundle|mvn|dotnet)([[:space:];|&]|$)#\1\3\4#g' \
|
|
172
|
+
-e 's#(^|[;&|][[:space:]]*)(env[[:space:]]+([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]+[[:space:]]+)*|command[[:space:]]+)#\1#g')
|
|
173
|
+
done
|
|
174
|
+
printf '%s' "${text}"
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
strip_heredoc_bodies() {
|
|
178
|
+
local input="$1"
|
|
179
|
+
local line
|
|
180
|
+
local delimiter=""
|
|
181
|
+
local heredoc_re="<<-?[[:space:]]*[\"']?([A-Za-z0-9_][A-Za-z0-9_.-]*)[\"']?"
|
|
182
|
+
|
|
183
|
+
while IFS= read -r line || [[ -n "${line}" ]]; do
|
|
184
|
+
if [[ -n "${delimiter}" ]]; then
|
|
185
|
+
if [[ "${line}" == "${delimiter}" ]]; then
|
|
186
|
+
delimiter=""
|
|
187
|
+
fi
|
|
188
|
+
continue
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
if [[ "${line}" =~ ${heredoc_re} ]]; then
|
|
192
|
+
delimiter="${BASH_REMATCH[1]}"
|
|
193
|
+
fi
|
|
194
|
+
printf '%s\n' "${line}"
|
|
195
|
+
done <<< "${input}"
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
extract_shell_c_payloads() {
|
|
199
|
+
local rest="$1"
|
|
200
|
+
|
|
201
|
+
while [[ "${rest}" =~ (bash|sh|zsh)[[:space:]]+-[A-Za-z]*c[[:space:]]+\"([^\"]*)\" ]]; do
|
|
202
|
+
printf '%s\n' "${BASH_REMATCH[2]}"
|
|
203
|
+
rest="${rest#*"${BASH_REMATCH[0]}"}"
|
|
204
|
+
done
|
|
205
|
+
|
|
206
|
+
rest="$1"
|
|
207
|
+
while [[ "${rest}" =~ (bash|sh|zsh)[[:space:]]+-[A-Za-z]*c[[:space:]]+\'([^\']*)\' ]]; do
|
|
208
|
+
printf '%s\n' "${BASH_REMATCH[2]}"
|
|
209
|
+
rest="${rest#*"${BASH_REMATCH[0]}"}"
|
|
210
|
+
done
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
command_candidate_texts() {
|
|
214
|
+
local command="$1"
|
|
215
|
+
local payload
|
|
216
|
+
|
|
217
|
+
command=$(strip_heredoc_bodies "${command}")
|
|
218
|
+
|
|
219
|
+
normalize_install_text "${command}"
|
|
220
|
+
printf '\n'
|
|
221
|
+
while IFS= read -r payload; do
|
|
222
|
+
[[ -z "${payload}" ]] && continue
|
|
223
|
+
normalize_install_text "${payload}"
|
|
224
|
+
printf '\n'
|
|
225
|
+
done < <(extract_shell_c_payloads "${command}")
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
command_is_injectable_npm_install() {
|
|
229
|
+
local command="$1"
|
|
230
|
+
local scan_command
|
|
231
|
+
local npm_install_pattern
|
|
232
|
+
|
|
233
|
+
npm_install_pattern='(^|[;&|]+[[:space:]]*)npm([[:space:]]+--?[a-zA-Z0-9_-]+([=[:space:]][^[:space:]]+)?)?[[:space:]]+(install|i|add|ci|update|up|upgrade)([[:space:]]|$)'
|
|
234
|
+
|
|
235
|
+
while IFS= read -r scan_command; do
|
|
236
|
+
scan_command=$(command_scan_text "${scan_command}")
|
|
237
|
+
echo "${scan_command}" | grep -qEi "${npm_install_pattern}" && return 0
|
|
238
|
+
done < <(command_candidate_texts "${command}")
|
|
239
|
+
return 1
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
command_has_ignore_scripts_flag() {
|
|
243
|
+
local command="$1"
|
|
244
|
+
local scan_command
|
|
245
|
+
|
|
246
|
+
while IFS= read -r scan_command; do
|
|
247
|
+
scan_command=$(command_scan_text "${scan_command}")
|
|
248
|
+
echo "${scan_command}" | grep -qEi -- '(^|[[:space:]])--ignore-scripts([=[:space:]]|$)' && return 0
|
|
249
|
+
done < <(command_candidate_texts "${command}")
|
|
250
|
+
return 1
|
|
251
|
+
}
|
|
252
|
+
|
|
157
253
|
snapshot_project_file() {
|
|
158
254
|
local relative_file="$1"
|
|
159
255
|
local category="${2:-manifest}"
|
|
@@ -275,10 +371,24 @@ cat > "${SNAPSHOT_DIR}/${SNAPSHOT_ID}_meta.json" << META_EOF
|
|
|
275
371
|
"timestamp": ${TIMESTAMP},
|
|
276
372
|
"project_dir": $(printf '%s' "${PROJECT_DIR}" | jq -Rs .),
|
|
277
373
|
"command": $(printf '%s' "${COMMAND}" | jq -Rs .),
|
|
374
|
+
"ignore_scripts_injected": false,
|
|
278
375
|
"lock_files_found": ${SNAPSHOTTED}
|
|
279
376
|
}
|
|
280
377
|
META_EOF
|
|
281
378
|
|
|
379
|
+
mark_ignore_scripts_injected() {
|
|
380
|
+
local meta_file="${SNAPSHOT_DIR}/${SNAPSHOT_ID}_meta.json"
|
|
381
|
+
local temp_file
|
|
382
|
+
|
|
383
|
+
[[ -f "${meta_file}" ]] || return 0
|
|
384
|
+
temp_file=$(mktemp "${SNAPSHOT_DIR}/.${SNAPSHOT_ID}_meta.XXXXXX") || return 0
|
|
385
|
+
if jq '.ignore_scripts_injected = true' "${meta_file}" > "${temp_file}"; then
|
|
386
|
+
mv -f "${temp_file}" "${meta_file}"
|
|
387
|
+
else
|
|
388
|
+
rm -f "${temp_file}"
|
|
389
|
+
fi
|
|
390
|
+
}
|
|
391
|
+
|
|
282
392
|
# --- Pre-flight security checks on the command itself ---
|
|
283
393
|
|
|
284
394
|
SUSPICIOUS=false
|
|
@@ -320,8 +430,10 @@ fi
|
|
|
320
430
|
|
|
321
431
|
# --- Phase 2 advisory gate — ledger enforcement -------------------------------
|
|
322
432
|
# For commands that name specific packages, require an entry in the approved-
|
|
323
|
-
# spec ledger. Miss/expired → block with a structured message that names
|
|
324
|
-
#
|
|
433
|
+
# spec ledger. Miss/expired → block with a structured message that names a
|
|
434
|
+
# runnable `safedeps check` command the caller (agent or human) should run
|
|
435
|
+
# next — PATH command when present, else an absolute path, so the self-heal
|
|
436
|
+
# loop never dead-ends on a missing PATH symlink.
|
|
325
437
|
#
|
|
326
438
|
# Conservative: only block when at least one pkg@spec token is parseable. Bare
|
|
327
439
|
# `npm install` (lockfile install) falls through to the v1 reorg checks.
|
|
@@ -333,33 +445,132 @@ guard_detect_ecosystem() {
|
|
|
333
445
|
local cmd="$1"
|
|
334
446
|
local scan_cmd
|
|
335
447
|
|
|
336
|
-
scan_cmd
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
448
|
+
while IFS= read -r scan_cmd; do
|
|
449
|
+
scan_cmd=$(command_scan_text "${scan_cmd}")
|
|
450
|
+
if echo "${scan_cmd}" | grep -qEi '(^|[;&|]+[[:space:]]*)(npm|pnpm|yarn|npx)([[:space:]]|$)'; then
|
|
451
|
+
printf 'npm'
|
|
452
|
+
return 0
|
|
453
|
+
elif echo "${scan_cmd}" | grep -qEi '(^|[;&|]+[[:space:]]*)(pip3?|poetry|uv|pipenv|((python3?|py)[[:space:]]+-m[[:space:]]+pip))([[:space:]]|$)'; then
|
|
454
|
+
printf 'pypi'
|
|
455
|
+
return 0
|
|
456
|
+
elif echo "${scan_cmd}" | grep -qEi '(^|[;&|]+[[:space:]]*)cargo([[:space:]]|$)'; then
|
|
457
|
+
printf 'crates.io'
|
|
458
|
+
return 0
|
|
459
|
+
elif echo "${scan_cmd}" | grep -qEi '(^|[;&|]+[[:space:]]*)go([[:space:]]|$)'; then
|
|
460
|
+
printf 'go'
|
|
461
|
+
return 0
|
|
462
|
+
elif echo "${scan_cmd}" | grep -qEi '(^|[;&|]+[[:space:]]*)(gem|bundle)([[:space:]]|$)'; then
|
|
463
|
+
printf 'rubygems'
|
|
464
|
+
return 0
|
|
465
|
+
elif echo "${scan_cmd}" | grep -qEi '(^|[;&|]+[[:space:]]*)mvn([[:space:]]|$)'; then
|
|
466
|
+
printf 'maven'
|
|
467
|
+
return 0
|
|
468
|
+
elif echo "${scan_cmd}" | grep -qEi '(^|[;&|]+[[:space:]]*)dotnet([[:space:]]|$)'; then
|
|
469
|
+
printf 'nuget'
|
|
470
|
+
return 0
|
|
471
|
+
fi
|
|
472
|
+
done < <(command_candidate_texts "${cmd}")
|
|
473
|
+
printf ''
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
guard_runner_operands() {
|
|
477
|
+
# Runner forms (`npx`, `pnpm dlx`, `yarn dlx`) EXECUTE a package; tokens after
|
|
478
|
+
# the executed package are arguments to that program, NOT package specs. Emit
|
|
479
|
+
# only the spec-bearing operands: any `-p/--package <pkg>` value plus the first
|
|
480
|
+
# bare token (the executed package). This stops an argument such as an email
|
|
481
|
+
# (`dev1@block-s.io`) or a secret value passed to `npx wrangler ...` from being
|
|
482
|
+
# misread as a `pkg@spec` install.
|
|
483
|
+
local scan="$1"
|
|
484
|
+
local after want_value tok
|
|
485
|
+
after=$(printf '%s' "${scan}" | grep -oiE '(npx|dlx)[[:space:]].*' | head -n1 || true)
|
|
486
|
+
after="${after#* }" # drop the runner keyword, keep its operands
|
|
487
|
+
[[ -z "${after}" ]] && return 0
|
|
488
|
+
|
|
489
|
+
want_value=false
|
|
490
|
+
for tok in ${after}; do
|
|
491
|
+
if [[ "${want_value}" == true ]]; then
|
|
492
|
+
printf '%s\n' "${tok}"
|
|
493
|
+
want_value=false
|
|
494
|
+
continue
|
|
495
|
+
fi
|
|
496
|
+
case "${tok}" in
|
|
497
|
+
-p|--package) want_value=true ;;
|
|
498
|
+
--package=*) printf '%s\n' "${tok#--package=}" ;;
|
|
499
|
+
-*) : ;; # other flag (e.g. -y/--yes), skip
|
|
500
|
+
*)
|
|
501
|
+
printf '%s\n' "${tok}" # executed package; rest are program args
|
|
502
|
+
break
|
|
503
|
+
;;
|
|
504
|
+
esac
|
|
505
|
+
done
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
guard_extract_flagged_specs() {
|
|
509
|
+
awk '
|
|
510
|
+
{
|
|
511
|
+
for (i = 1; i <= NF; i++) {
|
|
512
|
+
if ($i ~ /^[A-Za-z][A-Za-z0-9._-]*==[A-Za-z0-9][A-Za-z0-9._+!~-]*$/) {
|
|
513
|
+
split($i, parts, "==")
|
|
514
|
+
print parts[1] "\t" parts[2]
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if ($i == "gem" && $(i + 1) == "install") {
|
|
518
|
+
pkg = $(i + 2)
|
|
519
|
+
for (j = i + 3; j <= NF; j++) {
|
|
520
|
+
if (($j == "-v" || $j == "--version") && $(j + 1) != "") print pkg "\t" $(j + 1)
|
|
521
|
+
if ($j ~ /^--version=/) { sub(/^--version=/, "", $j); print pkg "\t" $j }
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if ($i == "cargo" && $(i + 1) == "add") {
|
|
526
|
+
pkg = $(i + 2)
|
|
527
|
+
for (j = i + 3; j <= NF; j++) {
|
|
528
|
+
if (($j == "--vers" || $j == "--version") && $(j + 1) != "") print pkg "\t" $(j + 1)
|
|
529
|
+
if ($j ~ /^--(vers|version)=/) { sub(/^--(vers|version)=/, "", $j); print pkg "\t" $j }
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if ($i == "dotnet" && $(i + 1) == "add" && $(i + 2) == "package") {
|
|
534
|
+
pkg = $(i + 3)
|
|
535
|
+
for (j = i + 4; j <= NF; j++) {
|
|
536
|
+
if ($j == "--version" && $(j + 1) != "") print pkg "\t" $(j + 1)
|
|
537
|
+
if ($j ~ /^--version=/) { sub(/^--version=/, "", $j); print pkg "\t" $j }
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
'
|
|
354
543
|
}
|
|
355
544
|
|
|
356
545
|
guard_extract_specs() {
|
|
357
|
-
# Echo one "pkg<TAB>spec" line per pkg@spec
|
|
358
|
-
#
|
|
546
|
+
# Echo one "pkg<TAB>spec" line per pkg@spec OPERAND genuinely being installed.
|
|
547
|
+
# Handles @scope/name@spec and bare-name@spec. Two precision rules keep
|
|
548
|
+
# non-package "@" tokens from being misread as an install:
|
|
549
|
+
# 1. Runner segments (npx / pnpm dlx / yarn dlx) contribute ONLY their
|
|
550
|
+
# executed package — trailing tokens are program arguments, not specs
|
|
551
|
+
# (so `npx wrangler ... dev1@block-s.io` is never read as a spec).
|
|
552
|
+
# 2. Email / host operands (user@domain.tld) are never package specs.
|
|
553
|
+
# Each shell segment is judged independently so a genuine install in one
|
|
554
|
+
# segment is still gated even when another segment just runs a tool via npx.
|
|
359
555
|
local cmd="$1"
|
|
360
|
-
|
|
361
|
-
|
|
556
|
+
local seg source=""
|
|
557
|
+
|
|
558
|
+
while IFS= read -r seg; do
|
|
559
|
+
[[ -z "${seg//[[:space:]]/}" ]] && continue
|
|
560
|
+
if printf '%s' "${seg}" | grep -qEi '(^|[[:space:]])(npx|dlx)([[:space:]]|$)'; then
|
|
561
|
+
source+="$(guard_runner_operands "${seg}")"$'\n'
|
|
562
|
+
else
|
|
563
|
+
source+="${seg}"$'\n'
|
|
564
|
+
fi
|
|
565
|
+
done < <(command_candidate_texts "${cmd}" | tr ';|&' '\n')
|
|
566
|
+
|
|
567
|
+
{ printf '%s' "${source}" \
|
|
568
|
+
| grep -oE '(@[a-zA-Z0-9._/-]+/)?[a-zA-Z][a-zA-Z0-9._-]*@[a-zA-Z0-9._^~|<>=*+-]+' || true; } \
|
|
362
569
|
| while IFS= read -r token; do
|
|
570
|
+
# An email / host operand (user@domain.tld) is never a package spec.
|
|
571
|
+
if [[ "${token}" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
|
|
572
|
+
continue
|
|
573
|
+
fi
|
|
363
574
|
local pkg spec
|
|
364
575
|
if [[ "${token}" =~ ^(@[^@]+)@(.+)$ ]]; then
|
|
365
576
|
pkg="${BASH_REMATCH[1]}"
|
|
@@ -370,12 +581,18 @@ guard_extract_specs() {
|
|
|
370
581
|
fi
|
|
371
582
|
printf '%s\t%s\n' "${pkg}" "${spec}"
|
|
372
583
|
done
|
|
584
|
+
printf '%s\n' "${source}" | guard_extract_flagged_specs
|
|
373
585
|
}
|
|
374
586
|
|
|
375
587
|
LEDGER_ECOSYSTEM=$(guard_detect_ecosystem "${COMMAND}")
|
|
376
588
|
LEDGER_SPECS=()
|
|
377
589
|
while IFS= read -r ledger_spec_line; do
|
|
378
590
|
[[ -z "${ledger_spec_line}" ]] && continue
|
|
591
|
+
if [[ ${#LEDGER_SPECS[@]} -gt 0 ]]; then
|
|
592
|
+
for existing_spec_line in "${LEDGER_SPECS[@]}"; do
|
|
593
|
+
[[ "${existing_spec_line}" == "${ledger_spec_line}" ]] && continue 2
|
|
594
|
+
done
|
|
595
|
+
fi
|
|
379
596
|
LEDGER_SPECS+=("${ledger_spec_line}")
|
|
380
597
|
done < <(guard_extract_specs "${COMMAND}")
|
|
381
598
|
|
|
@@ -383,6 +600,17 @@ if [[ -n "${LEDGER_ECOSYSTEM}" && ${#LEDGER_SPECS[@]} -gt 0 && -f "${SAFEDEPS_LE
|
|
|
383
600
|
# shellcheck source=../lib/ledger/ledger.sh
|
|
384
601
|
source "${SAFEDEPS_LEDGER_LIB}"
|
|
385
602
|
|
|
603
|
+
# Resolve a runnable `safedeps` invocation for the block message so the
|
|
604
|
+
# self-heal loop works whether or not the CLI is on PATH. Prefer the PATH
|
|
605
|
+
# command (clean UX); otherwise name the absolute repo bin (quoted via %q so
|
|
606
|
+
# it survives spaces in $HOME). Keeps the gate self-contained — the install
|
|
607
|
+
# of a `~/.local/bin/safedeps` symlink is a convenience, never a requirement.
|
|
608
|
+
if command -v safedeps >/dev/null 2>&1; then
|
|
609
|
+
SAFEDEPS_INVOKE="safedeps"
|
|
610
|
+
else
|
|
611
|
+
printf -v SAFEDEPS_INVOKE '%q' "${SAFEDEPS_REPO_BIN}"
|
|
612
|
+
fi
|
|
613
|
+
|
|
386
614
|
GUARD_BLOCKED_CMDS=()
|
|
387
615
|
for entry in "${LEDGER_SPECS[@]}"; do
|
|
388
616
|
pkg="${entry%%$'\t'*}"
|
|
@@ -390,7 +618,7 @@ if [[ -n "${LEDGER_ECOSYSTEM}" && ${#LEDGER_SPECS[@]} -gt 0 && -f "${SAFEDEPS_LE
|
|
|
390
618
|
[[ -z "${pkg}" || -z "${spec}" ]] && continue
|
|
391
619
|
if ! safedeps_ledger_check "${LEDGER_ECOSYSTEM}" "${pkg}" "${spec}" 2>/dev/null \
|
|
392
620
|
| jq -e '.approved == true' >/dev/null 2>&1; then
|
|
393
|
-
GUARD_BLOCKED_CMDS+=("
|
|
621
|
+
GUARD_BLOCKED_CMDS+=("${SAFEDEPS_INVOKE} check ${LEDGER_ECOSYSTEM} ${pkg}@${spec}")
|
|
394
622
|
fi
|
|
395
623
|
done
|
|
396
624
|
|
|
@@ -423,5 +651,15 @@ CURRENT_STATE=$(jq -n --arg sid "${SNAPSHOT_ID}" --arg pdir "${PROJECT_DIR}" --a
|
|
|
423
651
|
'{snapshot_id: $sid, project_dir: $pdir, dir_hash: $dhash}')
|
|
424
652
|
write_state_file "${GUARD_DIR}/current_state" "${CURRENT_STATE}"
|
|
425
653
|
|
|
654
|
+
if ! jq -e 'has("turn_id")' <<< "${INPUT}" >/dev/null 2>&1 && \
|
|
655
|
+
command_is_injectable_npm_install "${COMMAND}" && \
|
|
656
|
+
! command_has_ignore_scripts_flag "${COMMAND}"; then
|
|
657
|
+
UPDATED_COMMAND="${COMMAND} --ignore-scripts"
|
|
658
|
+
mark_ignore_scripts_injected
|
|
659
|
+
jq -nc --arg command "${UPDATED_COMMAND}" \
|
|
660
|
+
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",updatedInput:{command:$command}}}'
|
|
661
|
+
exit 0
|
|
662
|
+
fi
|
|
663
|
+
|
|
426
664
|
# Allow the command to proceed — PostToolUse will verify the result
|
|
427
665
|
exit 0
|
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,12 +260,21 @@ 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
|
|
@@ -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({
|