@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
|
@@ -35,11 +35,27 @@ SAFEDEPS_MANIFEST_FILES=(
|
|
|
35
35
|
umask 077
|
|
36
36
|
mkdir -p "${GUARD_DIR}" "${SNAPSHOT_DIR}"
|
|
37
37
|
|
|
38
|
+
# Observable record when the effect gate cannot run (AGENTS.md: no silent fallback).
|
|
39
|
+
# The install already happened by PostToolUse, so we cannot block it; what we can
|
|
40
|
+
# guarantee is that an un-runnable gate is recorded as UNVERIFIED, never a silent pass.
|
|
41
|
+
log_advisory() {
|
|
42
|
+
printf '%s\t%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$1" >> "${GUARD_DIR}/advisory.log" 2>/dev/null || true
|
|
43
|
+
}
|
|
44
|
+
|
|
38
45
|
if ! command -v jq >/dev/null 2>&1; then
|
|
39
|
-
|
|
46
|
+
log_advisory "post-verify UNVERIFIED: jq missing — could not verify the install closure. Install jq to restore the effect gate."
|
|
47
|
+
echo "safedeps: jq is not installed — the post-install effect gate could not run; this install is UNVERIFIED (logged to advisory.log)." >&2
|
|
40
48
|
exit 0
|
|
41
49
|
fi
|
|
42
50
|
|
|
51
|
+
SAFEDEPS_REPO_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
52
|
+
# shellcheck source=../lib/ledger/ledger.sh
|
|
53
|
+
source "${SAFEDEPS_REPO_DIR}/lib/ledger/ledger.sh"
|
|
54
|
+
# shellcheck source=../lib/providers/providers.sh
|
|
55
|
+
source "${SAFEDEPS_REPO_DIR}/lib/providers/providers.sh"
|
|
56
|
+
# shellcheck source=../lib/npm/closure.sh
|
|
57
|
+
source "${SAFEDEPS_REPO_DIR}/lib/npm/closure.sh"
|
|
58
|
+
|
|
43
59
|
acquire_state_lock() {
|
|
44
60
|
local attempts=0
|
|
45
61
|
|
|
@@ -47,8 +63,10 @@ acquire_state_lock() {
|
|
|
47
63
|
# Detect stale locks left by SIGKILL/OOM (V-005)
|
|
48
64
|
if [[ -d "${STATE_LOCK_DIR}" ]]; then
|
|
49
65
|
local lock_mtime=""
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
# GNU (`-c %Y`, Linux) first, then BSD/macOS (`-f %m`): on Linux `stat -f`
|
|
67
|
+
# means --file-system and would not yield an mtime.
|
|
68
|
+
if lock_mtime=$(stat -c %Y "${STATE_LOCK_DIR}" 2>/dev/null) || \
|
|
69
|
+
lock_mtime=$(stat -f %m "${STATE_LOCK_DIR}" 2>/dev/null); then
|
|
52
70
|
local now
|
|
53
71
|
now=$(date +%s)
|
|
54
72
|
if [[ $(( now - lock_mtime )) -gt 60 ]]; then
|
|
@@ -60,8 +78,11 @@ acquire_state_lock() {
|
|
|
60
78
|
fi
|
|
61
79
|
|
|
62
80
|
attempts=$((attempts + 1))
|
|
63
|
-
if [[ ${attempts} -ge 100 ]]; then
|
|
64
|
-
|
|
81
|
+
if [[ ${attempts} -ge ${SAFEDEPS_LOCK_MAX_ATTEMPTS:-100} ]]; then
|
|
82
|
+
# Install already ran; another safedeps run holds the lock. Record UNVERIFIED
|
|
83
|
+
# rather than silently passing — the other run, or a re-check, owns verification.
|
|
84
|
+
log_advisory "post-verify UNVERIFIED: state lock unavailable (another safedeps run active) — install not verified by this run."
|
|
85
|
+
echo "safedeps: could not acquire state lock; this install is UNVERIFIED by this run (logged to advisory.log)." >&2
|
|
65
86
|
exit 0
|
|
66
87
|
fi
|
|
67
88
|
sleep 0.1
|
|
@@ -75,10 +96,16 @@ release_state_lock() {
|
|
|
75
96
|
write_state_file() {
|
|
76
97
|
local target_path="$1"
|
|
77
98
|
local value="$2"
|
|
78
|
-
local
|
|
79
|
-
|
|
99
|
+
local target_dir
|
|
100
|
+
local target_base
|
|
101
|
+
local temp_path
|
|
102
|
+
|
|
103
|
+
target_dir=$(dirname "${target_path}")
|
|
104
|
+
target_base=$(basename "${target_path}")
|
|
105
|
+
mkdir -p "${target_dir}" || return 1
|
|
106
|
+
temp_path=$(mktemp "${target_dir}/.${target_base}.XXXXXX") || return 1
|
|
80
107
|
printf '%s\n' "${value}" > "${temp_path}"
|
|
81
|
-
mv "${temp_path}" "${target_path}"
|
|
108
|
+
mv -f "${temp_path}" "${target_path}"
|
|
82
109
|
}
|
|
83
110
|
|
|
84
111
|
compute_dir_hash() {
|
|
@@ -214,7 +241,7 @@ collect_protected_snapshot_ids() {
|
|
|
214
241
|
local already_seen="false"
|
|
215
242
|
local seen_id
|
|
216
243
|
|
|
217
|
-
for seen_id in "${seen[@]}"; do
|
|
244
|
+
for seen_id in "${seen[@]+${seen[@]}}"; do
|
|
218
245
|
if [[ "${seen_id}" == "${snapshot_id}" ]]; then
|
|
219
246
|
already_seen="true"
|
|
220
247
|
break
|
|
@@ -305,6 +332,41 @@ restore_node_modules() {
|
|
|
305
332
|
ROLLBACK_WARNINGS+=("node_modules reinstall failed; review the project manually")
|
|
306
333
|
}
|
|
307
334
|
|
|
335
|
+
run_verified_npm_rebuild_if_injected() {
|
|
336
|
+
local injected
|
|
337
|
+
|
|
338
|
+
injected=$(jq -r '.ignore_scripts_injected == true' "${META_FILE}" 2>/dev/null || printf 'false')
|
|
339
|
+
[[ "${injected}" == "true" ]] || return 0
|
|
340
|
+
|
|
341
|
+
if ! command -v npm >/dev/null 2>&1; then
|
|
342
|
+
ROLLBACK_WARNINGS+=("npm is not installed; npm rebuild was not run after verified inert install")
|
|
343
|
+
return 0
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
if (cd "${PROJECT_DIR}" && npm rebuild >/dev/null 2>&1); then
|
|
347
|
+
return 0
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
ROLLBACK_WARNINGS+=("npm rebuild failed after verified inert install; lifecycle scripts may need manual review")
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
emit_confirm_warnings_if_any() {
|
|
354
|
+
local warning_str
|
|
355
|
+
|
|
356
|
+
[[ ${#ROLLBACK_WARNINGS[@]} -gt 0 ]] || return 0
|
|
357
|
+
|
|
358
|
+
warning_str=$(printf '%s; ' "${ROLLBACK_WARNINGS[@]}")
|
|
359
|
+
cat >> "${GUARD_DIR}/reorg.log" << LOG_EOF
|
|
360
|
+
[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] CONFIRM warnings
|
|
361
|
+
Snapshot: ${SNAPSHOT_ID}
|
|
362
|
+
Project: ${PROJECT_DIR}
|
|
363
|
+
Warnings: ${warning_str%%; }
|
|
364
|
+
LOG_EOF
|
|
365
|
+
|
|
366
|
+
jq -nc --arg warnings "${warning_str%%; }" \
|
|
367
|
+
'{systemMessage: ("safedeps: verified install completed, but npm rebuild warning(s) were recorded:\n" + $warnings)}'
|
|
368
|
+
}
|
|
369
|
+
|
|
308
370
|
# Read tool input from stdin
|
|
309
371
|
INPUT=$(cat)
|
|
310
372
|
|
|
@@ -358,6 +420,32 @@ SUSPICIOUS=false
|
|
|
358
420
|
REASONS=()
|
|
359
421
|
ROLLBACK_WARNINGS=()
|
|
360
422
|
|
|
423
|
+
redact_install_script_content() {
|
|
424
|
+
local script_content="$1"
|
|
425
|
+
local flattened
|
|
426
|
+
local byte_count
|
|
427
|
+
local digest
|
|
428
|
+
local suffix=""
|
|
429
|
+
|
|
430
|
+
flattened=$(printf '%s' "${script_content}" | tr '\r\n\t' ' ' | cut -c 1-160)
|
|
431
|
+
byte_count=$(printf '%s' "${script_content}" | wc -c | tr -d ' ')
|
|
432
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
433
|
+
digest=$(printf '%s' "${script_content}" | shasum -a 256 | cut -d' ' -f1)
|
|
434
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
435
|
+
digest=$(printf '%s' "${script_content}" | sha256sum | cut -d' ' -f1)
|
|
436
|
+
else
|
|
437
|
+
digest="unavailable"
|
|
438
|
+
fi
|
|
439
|
+
if [[ "${byte_count}" -gt 160 ]]; then
|
|
440
|
+
suffix="..."
|
|
441
|
+
fi
|
|
442
|
+
printf '[redacted install script sha256=%s bytes=%s preview=%s%s]' \
|
|
443
|
+
"${digest}" \
|
|
444
|
+
"${byte_count}" \
|
|
445
|
+
"${flattened}" \
|
|
446
|
+
"${suffix}"
|
|
447
|
+
}
|
|
448
|
+
|
|
361
449
|
# Function: check for suspicious postinstall scripts in new/changed dependencies
|
|
362
450
|
check_postinstall_scripts() {
|
|
363
451
|
local pkg_json="${PROJECT_DIR}/package.json"
|
|
@@ -411,17 +499,17 @@ check_postinstall_scripts() {
|
|
|
411
499
|
# Check for network calls in install scripts
|
|
412
500
|
if echo "${script_content}" | grep -qEi '(curl|wget|fetch|http|https|net\.|socket|dns)'; then
|
|
413
501
|
SUSPICIOUS=true
|
|
414
|
-
REASONS+=("Package '${pkg_name}' has install script with network access: ${script_content}")
|
|
502
|
+
REASONS+=("Package '${pkg_name}' has install script with network access: $(redact_install_script_content "${script_content}")")
|
|
415
503
|
fi
|
|
416
504
|
|
|
417
505
|
# Check for eval/exec in install scripts
|
|
418
506
|
if echo "${script_content}" | grep -qEi '(eval|exec|spawn|child_process|Function\()'; then
|
|
419
507
|
SUSPICIOUS=true
|
|
420
|
-
REASONS+=("Package '${pkg_name}' has install script with code execution: ${script_content}")
|
|
508
|
+
REASONS+=("Package '${pkg_name}' has install script with code execution: $(redact_install_script_content "${script_content}")")
|
|
421
509
|
fi
|
|
422
510
|
|
|
423
511
|
# Check for filesystem access outside project
|
|
424
|
-
if echo "${script_content}" | grep -qEi '(\/etc\/|\/home\/|~\/|\$HOME|\.ssh|\.env|\.aws|credentials)'; then
|
|
512
|
+
if echo "${script_content}" | grep -qEi '(\/etc\/|\/home\/|~\/|\$HOME|\.ssh|\.env|\.aws|credentials|~\/\.safedeps|\$HOME\/\.safedeps|\.safedeps\/|SAFEDEPS_HOME)'; then
|
|
425
513
|
SUSPICIOUS=true
|
|
426
514
|
REASONS+=("Package '${pkg_name}' has install script accessing sensitive paths")
|
|
427
515
|
fi
|
|
@@ -507,7 +595,74 @@ check_binaries() {
|
|
|
507
595
|
fi
|
|
508
596
|
}
|
|
509
597
|
|
|
598
|
+
check_npm_effect_closure() {
|
|
599
|
+
local lockfile="${PROJECT_DIR}/package-lock.json"
|
|
600
|
+
local closure_file
|
|
601
|
+
local provider_file
|
|
602
|
+
local miss_file
|
|
603
|
+
local package_name
|
|
604
|
+
local version
|
|
605
|
+
local miss_count
|
|
606
|
+
local vulnerable_summary
|
|
607
|
+
local kev_summary
|
|
608
|
+
|
|
609
|
+
[[ -f "${lockfile}" ]] || return 0
|
|
610
|
+
|
|
611
|
+
closure_file=$(mktemp "${TMPDIR:-/tmp}/safedeps-post-closure.XXXXXX") || return
|
|
612
|
+
provider_file=$(mktemp "${TMPDIR:-/tmp}/safedeps-post-provider.XXXXXX") || {
|
|
613
|
+
rm -f "${closure_file}"
|
|
614
|
+
return
|
|
615
|
+
}
|
|
616
|
+
miss_file=$(mktemp "${TMPDIR:-/tmp}/safedeps-post-miss.XXXXXX") || {
|
|
617
|
+
rm -f "${closure_file}" "${provider_file}"
|
|
618
|
+
return
|
|
619
|
+
}
|
|
620
|
+
: > "${miss_file}"
|
|
621
|
+
|
|
622
|
+
if ! safedeps_npm_lock_closure "${lockfile}" > "${closure_file}"; then
|
|
623
|
+
SUSPICIOUS=true
|
|
624
|
+
REASONS+=("npm package-lock closure could not be parsed")
|
|
625
|
+
rm -f "${closure_file}" "${provider_file}" "${miss_file}"
|
|
626
|
+
return
|
|
627
|
+
fi
|
|
628
|
+
|
|
629
|
+
while IFS=$'\t' read -r package_name version; do
|
|
630
|
+
[[ -n "${package_name}" && -n "${version}" ]] || continue
|
|
631
|
+
if ! safedeps_ledger_effect_check "npm" "${package_name}" "${version}" >/dev/null 2>&1; then
|
|
632
|
+
printf '%s@%s\n' "${package_name}" "${version}" >> "${miss_file}"
|
|
633
|
+
fi
|
|
634
|
+
done < <(jq -r '.[] | [.package, (.version | tostring)] | @tsv' "${closure_file}")
|
|
635
|
+
|
|
636
|
+
miss_count=$(wc -l < "${miss_file}" | tr -d ' ')
|
|
637
|
+
if [[ "${miss_count}" -gt 0 ]]; then
|
|
638
|
+
SUSPICIOUS=true
|
|
639
|
+
REASONS+=("npm closure contains ${miss_count} unapproved package(s): $(head -20 "${miss_file}" | paste -sd ', ' -)")
|
|
640
|
+
fi
|
|
641
|
+
|
|
642
|
+
if ! safedeps_providers_query_batch "npm" "${closure_file}" > "${provider_file}"; then
|
|
643
|
+
SUSPICIOUS=true
|
|
644
|
+
REASONS+=("npm closure OSV batch verification failed; fail-closed")
|
|
645
|
+
rm -f "${closure_file}" "${provider_file}" "${miss_file}"
|
|
646
|
+
return
|
|
647
|
+
fi
|
|
648
|
+
|
|
649
|
+
kev_summary=$(jq -r '[.[] | select(.status == "hard_block") | "\(.package)@\(.version)"] | join(", ")' "${provider_file}")
|
|
650
|
+
if [[ -n "${kev_summary}" ]]; then
|
|
651
|
+
SUSPICIOUS=true
|
|
652
|
+
REASONS+=("npm closure contains KEV-blocked package(s): ${kev_summary}")
|
|
653
|
+
fi
|
|
654
|
+
|
|
655
|
+
vulnerable_summary=$(jq -r '[.[] | select(.status == "vulnerable") | "\(.package)@\(.version)"] | join(", ")' "${provider_file}")
|
|
656
|
+
if [[ -n "${vulnerable_summary}" ]]; then
|
|
657
|
+
SUSPICIOUS=true
|
|
658
|
+
REASONS+=("npm closure contains vulnerable package(s): ${vulnerable_summary}")
|
|
659
|
+
fi
|
|
660
|
+
|
|
661
|
+
rm -f "${closure_file}" "${provider_file}" "${miss_file}"
|
|
662
|
+
}
|
|
663
|
+
|
|
510
664
|
# Run all checks
|
|
665
|
+
check_npm_effect_closure
|
|
511
666
|
check_postinstall_scripts
|
|
512
667
|
check_lockfile_diff
|
|
513
668
|
check_binaries
|
|
@@ -572,13 +727,28 @@ if [[ "${SUSPICIOUS}" == "true" ]]; then
|
|
|
572
727
|
Rollback warnings: ${WARNING_STR%%; }
|
|
573
728
|
LOG_EOF
|
|
574
729
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
730
|
+
jq -nc \
|
|
731
|
+
--arg reasons "${REASON_STR%%; }" \
|
|
732
|
+
--arg rollback_snapshot "${ROLLBACK_SNAPSHOT_ID}" \
|
|
733
|
+
--arg rolled_back "${ROLLED_BACK_STR%, }" \
|
|
734
|
+
--arg warnings "${WARNING_STR%%; }" \
|
|
735
|
+
--arg log_path "${GUARD_DIR}/reorg.log" \
|
|
736
|
+
'{
|
|
737
|
+
systemMessage: (
|
|
738
|
+
"safedeps: 의심스러운 패키지 변경 감지, 마지막으로 confirmed 된 안전 스냅샷으로 롤백했습니다.\n\n" +
|
|
739
|
+
"감지된 문제:\n" + $reasons + "\n\n" +
|
|
740
|
+
"롤백 기준 스냅샷: " + $rollback_snapshot + "\n" +
|
|
741
|
+
"롤백된 파일: " + $rolled_back +
|
|
742
|
+
(if $warnings == "" then "" else "\n\n추가 경고:\n" + $warnings end) +
|
|
743
|
+
"\n\n상세 로그: " + $log_path
|
|
744
|
+
)
|
|
745
|
+
}'
|
|
578
746
|
exit 0
|
|
579
747
|
fi
|
|
580
748
|
|
|
749
|
+
run_verified_npm_rebuild_if_injected
|
|
581
750
|
confirm_snapshot "${SNAPSHOT_ID}" "${DIR_HASH}"
|
|
582
751
|
cleanup_old_snapshots
|
|
752
|
+
emit_confirm_warnings_if_any
|
|
583
753
|
|
|
584
754
|
exit 0
|