@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
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT=""
|
|
5
|
+
STRICT=0
|
|
6
|
+
NO_RUN=0
|
|
7
|
+
|
|
8
|
+
usage() {
|
|
9
|
+
cat <<'EOF'
|
|
10
|
+
Usage: run-release-gates.sh [--root <repo>] [--strict] [--no-run]
|
|
11
|
+
|
|
12
|
+
Runs release-time security gates for the current repository tree.
|
|
13
|
+
EOF
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
while [ $# -gt 0 ]; do
|
|
17
|
+
case "$1" in
|
|
18
|
+
--root)
|
|
19
|
+
ROOT="${2:-}"
|
|
20
|
+
shift 2
|
|
21
|
+
;;
|
|
22
|
+
--strict)
|
|
23
|
+
STRICT=1
|
|
24
|
+
shift
|
|
25
|
+
;;
|
|
26
|
+
--no-run)
|
|
27
|
+
NO_RUN=1
|
|
28
|
+
shift
|
|
29
|
+
;;
|
|
30
|
+
-h|--help)
|
|
31
|
+
usage
|
|
32
|
+
exit 0
|
|
33
|
+
;;
|
|
34
|
+
*)
|
|
35
|
+
usage >&2
|
|
36
|
+
exit 64
|
|
37
|
+
;;
|
|
38
|
+
esac
|
|
39
|
+
done
|
|
40
|
+
|
|
41
|
+
if [ -z "$ROOT" ]; then
|
|
42
|
+
ROOT="$(pwd)"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
if command -v realpath >/dev/null 2>&1; then
|
|
46
|
+
ROOT="$(realpath "$ROOT")"
|
|
47
|
+
else
|
|
48
|
+
ROOT="$(cd "$ROOT" && pwd)"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
if [ ! -d "$ROOT" ]; then
|
|
52
|
+
printf 'ERROR: repo root does not exist: %s\n' "$ROOT" >&2
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
cd "$ROOT"
|
|
57
|
+
|
|
58
|
+
FAILURES=0
|
|
59
|
+
WARNINGS=0
|
|
60
|
+
RAN=0
|
|
61
|
+
|
|
62
|
+
section() {
|
|
63
|
+
printf '\n== %s ==\n' "$1"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pass() {
|
|
67
|
+
printf 'PASS [%s] %s\n' "$1" "$2"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
warn() {
|
|
71
|
+
WARNINGS=$((WARNINGS + 1))
|
|
72
|
+
printf 'WARN [%s] %s\n' "$1" "$2" >&2
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fail() {
|
|
76
|
+
FAILURES=$((FAILURES + 1))
|
|
77
|
+
printf 'FAIL [%s] %s\n' "$1" "$2" >&2
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
strict_or_warn() {
|
|
81
|
+
if [ "$STRICT" -eq 1 ]; then
|
|
82
|
+
fail "$1" "$2"
|
|
83
|
+
else
|
|
84
|
+
warn "$1" "$2"
|
|
85
|
+
fi
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
run_cmd() {
|
|
89
|
+
local gate="$1"
|
|
90
|
+
local desc="$2"
|
|
91
|
+
shift 2
|
|
92
|
+
|
|
93
|
+
RAN=$((RAN + 1))
|
|
94
|
+
printf 'RUN [%s] %s\n' "$gate" "$desc"
|
|
95
|
+
printf 'CMD [%s] %s\n' "$gate" "$*"
|
|
96
|
+
|
|
97
|
+
if [ "$NO_RUN" -eq 1 ]; then
|
|
98
|
+
pass "$gate" "planned only (--no-run)"
|
|
99
|
+
return 0
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
if "$@"; then
|
|
103
|
+
pass "$gate" "$desc"
|
|
104
|
+
else
|
|
105
|
+
fail "$gate" "$desc"
|
|
106
|
+
fi
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
has_file() {
|
|
110
|
+
[ -f "$1" ]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
has_npm_script() {
|
|
114
|
+
local script_name="$1"
|
|
115
|
+
has_file package.json || return 1
|
|
116
|
+
command -v node >/dev/null 2>&1 || return 1
|
|
117
|
+
node -e '
|
|
118
|
+
const fs = require("node:fs");
|
|
119
|
+
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
|
|
120
|
+
process.exit(pkg.scripts && Object.prototype.hasOwnProperty.call(pkg.scripts, process.argv[1]) ? 0 : 1);
|
|
121
|
+
' "$script_name"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
run_npm_script_if_present() {
|
|
125
|
+
local script_name="$1"
|
|
126
|
+
local gate="$2"
|
|
127
|
+
if has_npm_script "$script_name"; then
|
|
128
|
+
run_cmd "$gate" "npm run $script_name" npm run "$script_name"
|
|
129
|
+
return 0
|
|
130
|
+
fi
|
|
131
|
+
return 1
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
detect_python_surface() {
|
|
135
|
+
find . -maxdepth 3 \
|
|
136
|
+
\( -name 'requirements*.txt' -o -name 'pyproject.toml' -o -name 'poetry.lock' -o -name 'uv.lock' -o -name 'Pipfile.lock' \) \
|
|
137
|
+
-not -path './node_modules/*' \
|
|
138
|
+
-not -path './.git/*' \
|
|
139
|
+
-print
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
detect_requirements_files() {
|
|
143
|
+
find . -maxdepth 3 \
|
|
144
|
+
-name 'requirements*.txt' \
|
|
145
|
+
-not -path './node_modules/*' \
|
|
146
|
+
-not -path './.git/*' \
|
|
147
|
+
-print | sort
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
hook_file_mentions_reorg_guard() {
|
|
151
|
+
local file="$1"
|
|
152
|
+
[ -f "$file" ] || return 1
|
|
153
|
+
grep -q 'safedeps' "$file"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
safedeps_install_guard_present() {
|
|
157
|
+
[ -d "$HOME/.claude/skills/safedeps" ] && return 0
|
|
158
|
+
[ -d "$HOME/.codex/skills/safedeps" ] && return 0
|
|
159
|
+
hook_file_mentions_reorg_guard "$HOME/.claude/settings.json" && return 0
|
|
160
|
+
hook_file_mentions_reorg_guard "$HOME/.codex/hooks.json" && return 0
|
|
161
|
+
return 1
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
section "repo"
|
|
165
|
+
printf 'root: %s\n' "$ROOT"
|
|
166
|
+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
167
|
+
pass repo "inside git worktree"
|
|
168
|
+
else
|
|
169
|
+
strict_or_warn repo "not inside a git worktree"
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
if [ -f docs/security-release-gates.md ] || [ -f SECURITY.md ]; then
|
|
173
|
+
pass repo "release/security documentation present"
|
|
174
|
+
else
|
|
175
|
+
warn repo "no docs/security-release-gates.md or SECURITY.md"
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
section "secrets"
|
|
179
|
+
if run_npm_script_if_present "security:hooks:check" "secrets"; then
|
|
180
|
+
:
|
|
181
|
+
fi
|
|
182
|
+
if run_npm_script_if_present "security:scan:worktree" "secrets"; then
|
|
183
|
+
:
|
|
184
|
+
elif [ -x scripts/security/run-gitleaks.sh ]; then
|
|
185
|
+
run_cmd secrets "repo gitleaks wrapper" bash scripts/security/run-gitleaks.sh --worktree
|
|
186
|
+
elif command -v gitleaks >/dev/null 2>&1 && { [ -f .gitleaks.toml ] || [ -f gitleaks.toml ]; }; then
|
|
187
|
+
config=".gitleaks.toml"
|
|
188
|
+
[ -f "$config" ] || config="gitleaks.toml"
|
|
189
|
+
run_cmd secrets "gitleaks dir scan" gitleaks dir --no-banner --redact --verbose --config "$config" .
|
|
190
|
+
else
|
|
191
|
+
strict_or_warn secrets "no gitleaks gate detected"
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
section "node"
|
|
195
|
+
if has_file package.json; then
|
|
196
|
+
pass node "package.json detected"
|
|
197
|
+
if run_npm_script_if_present "security:audit" "node"; then
|
|
198
|
+
:
|
|
199
|
+
elif has_file package-lock.json || has_file npm-shrinkwrap.json; then
|
|
200
|
+
run_cmd node "npm audit --audit-level=moderate" npm audit --audit-level=moderate
|
|
201
|
+
else
|
|
202
|
+
strict_or_warn node "package.json exists but no npm lockfile/audit script was detected"
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
if safedeps_install_guard_present; then
|
|
206
|
+
pass install-guard "safedeps appears installed/configured"
|
|
207
|
+
elif [ "$STRICT" -eq 1 ] || [ "${SECURITY_RELEASE_GATES_REQUIRE_INSTALL_GUARD:-0}" = "1" ]; then
|
|
208
|
+
fail install-guard "npm project has no detectable safedeps install-time guard"
|
|
209
|
+
else
|
|
210
|
+
warn install-guard "safedeps not detected; release gate can continue, install-time guard is separate"
|
|
211
|
+
fi
|
|
212
|
+
else
|
|
213
|
+
pass node "no package.json detected"
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
section "python"
|
|
217
|
+
PYTHON_SURFACE="$(detect_python_surface || true)"
|
|
218
|
+
if [ -z "$PYTHON_SURFACE" ]; then
|
|
219
|
+
pass python "no Python dependency surface detected"
|
|
220
|
+
elif [ -n "${SECURITY_RELEASE_GATES_PYTHON_AUDIT_COMMAND:-}" ]; then
|
|
221
|
+
run_cmd python "custom Python audit command" bash -lc "$SECURITY_RELEASE_GATES_PYTHON_AUDIT_COMMAND"
|
|
222
|
+
elif command -v pip-audit >/dev/null 2>&1; then
|
|
223
|
+
REQUIREMENTS="$(detect_requirements_files || true)"
|
|
224
|
+
if [ -n "$REQUIREMENTS" ]; then
|
|
225
|
+
while IFS= read -r requirements_file; do
|
|
226
|
+
[ -n "$requirements_file" ] || continue
|
|
227
|
+
run_cmd python "pip-audit $requirements_file" pip-audit -r "$requirements_file"
|
|
228
|
+
done <<EOF_REQ
|
|
229
|
+
$REQUIREMENTS
|
|
230
|
+
EOF_REQ
|
|
231
|
+
else
|
|
232
|
+
strict_or_warn python "Python lock/project files detected, but no requirements*.txt or repo-provided Python audit command exists"
|
|
233
|
+
fi
|
|
234
|
+
else
|
|
235
|
+
strict_or_warn python "Python dependency files detected, but pip-audit is not installed and no custom audit command was provided"
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
section "ci"
|
|
239
|
+
if find .github/workflows -maxdepth 1 -type f 2>/dev/null | xargs grep -E 'security:|gitleaks|pip-audit|npm audit' >/dev/null 2>&1; then
|
|
240
|
+
pass ci "workflow appears to run security gates"
|
|
241
|
+
else
|
|
242
|
+
warn ci "no obvious GitHub security gate workflow detected"
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
section "summary"
|
|
246
|
+
printf 'gates_run=%s warnings=%s failures=%s strict=%s no_run=%s\n' "$RAN" "$WARNINGS" "$FAILURES" "$STRICT" "$NO_RUN"
|
|
247
|
+
|
|
248
|
+
if [ "$FAILURES" -gt 0 ]; then
|
|
249
|
+
exit 1
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
exit 0
|
|
@@ -40,6 +40,14 @@ if ! command -v jq >/dev/null 2>&1; then
|
|
|
40
40
|
exit 0
|
|
41
41
|
fi
|
|
42
42
|
|
|
43
|
+
SAFEDEPS_REPO_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
44
|
+
# shellcheck source=../lib/ledger/ledger.sh
|
|
45
|
+
source "${SAFEDEPS_REPO_DIR}/lib/ledger/ledger.sh"
|
|
46
|
+
# shellcheck source=../lib/providers/providers.sh
|
|
47
|
+
source "${SAFEDEPS_REPO_DIR}/lib/providers/providers.sh"
|
|
48
|
+
# shellcheck source=../lib/npm/closure.sh
|
|
49
|
+
source "${SAFEDEPS_REPO_DIR}/lib/npm/closure.sh"
|
|
50
|
+
|
|
43
51
|
acquire_state_lock() {
|
|
44
52
|
local attempts=0
|
|
45
53
|
|
|
@@ -75,10 +83,16 @@ release_state_lock() {
|
|
|
75
83
|
write_state_file() {
|
|
76
84
|
local target_path="$1"
|
|
77
85
|
local value="$2"
|
|
78
|
-
local
|
|
79
|
-
|
|
86
|
+
local target_dir
|
|
87
|
+
local target_base
|
|
88
|
+
local temp_path
|
|
89
|
+
|
|
90
|
+
target_dir=$(dirname "${target_path}")
|
|
91
|
+
target_base=$(basename "${target_path}")
|
|
92
|
+
mkdir -p "${target_dir}" || return 1
|
|
93
|
+
temp_path=$(mktemp "${target_dir}/.${target_base}.XXXXXX") || return 1
|
|
80
94
|
printf '%s\n' "${value}" > "${temp_path}"
|
|
81
|
-
mv "${temp_path}" "${target_path}"
|
|
95
|
+
mv -f "${temp_path}" "${target_path}"
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
compute_dir_hash() {
|
|
@@ -214,7 +228,7 @@ collect_protected_snapshot_ids() {
|
|
|
214
228
|
local already_seen="false"
|
|
215
229
|
local seen_id
|
|
216
230
|
|
|
217
|
-
for seen_id in "${seen[@]}"; do
|
|
231
|
+
for seen_id in "${seen[@]+${seen[@]}}"; do
|
|
218
232
|
if [[ "${seen_id}" == "${snapshot_id}" ]]; then
|
|
219
233
|
already_seen="true"
|
|
220
234
|
break
|
|
@@ -305,6 +319,41 @@ restore_node_modules() {
|
|
|
305
319
|
ROLLBACK_WARNINGS+=("node_modules reinstall failed; review the project manually")
|
|
306
320
|
}
|
|
307
321
|
|
|
322
|
+
run_verified_npm_rebuild_if_injected() {
|
|
323
|
+
local injected
|
|
324
|
+
|
|
325
|
+
injected=$(jq -r '.ignore_scripts_injected == true' "${META_FILE}" 2>/dev/null || printf 'false')
|
|
326
|
+
[[ "${injected}" == "true" ]] || return 0
|
|
327
|
+
|
|
328
|
+
if ! command -v npm >/dev/null 2>&1; then
|
|
329
|
+
ROLLBACK_WARNINGS+=("npm is not installed; npm rebuild was not run after verified inert install")
|
|
330
|
+
return 0
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
if (cd "${PROJECT_DIR}" && npm rebuild >/dev/null 2>&1); then
|
|
334
|
+
return 0
|
|
335
|
+
fi
|
|
336
|
+
|
|
337
|
+
ROLLBACK_WARNINGS+=("npm rebuild failed after verified inert install; lifecycle scripts may need manual review")
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
emit_confirm_warnings_if_any() {
|
|
341
|
+
local warning_str
|
|
342
|
+
|
|
343
|
+
[[ ${#ROLLBACK_WARNINGS[@]} -gt 0 ]] || return 0
|
|
344
|
+
|
|
345
|
+
warning_str=$(printf '%s; ' "${ROLLBACK_WARNINGS[@]}")
|
|
346
|
+
cat >> "${GUARD_DIR}/reorg.log" << LOG_EOF
|
|
347
|
+
[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] CONFIRM warnings
|
|
348
|
+
Snapshot: ${SNAPSHOT_ID}
|
|
349
|
+
Project: ${PROJECT_DIR}
|
|
350
|
+
Warnings: ${warning_str%%; }
|
|
351
|
+
LOG_EOF
|
|
352
|
+
|
|
353
|
+
jq -nc --arg warnings "${warning_str%%; }" \
|
|
354
|
+
'{systemMessage: ("safedeps: verified install completed, but npm rebuild warning(s) were recorded:\n" + $warnings)}'
|
|
355
|
+
}
|
|
356
|
+
|
|
308
357
|
# Read tool input from stdin
|
|
309
358
|
INPUT=$(cat)
|
|
310
359
|
|
|
@@ -358,6 +407,32 @@ SUSPICIOUS=false
|
|
|
358
407
|
REASONS=()
|
|
359
408
|
ROLLBACK_WARNINGS=()
|
|
360
409
|
|
|
410
|
+
redact_install_script_content() {
|
|
411
|
+
local script_content="$1"
|
|
412
|
+
local flattened
|
|
413
|
+
local byte_count
|
|
414
|
+
local digest
|
|
415
|
+
local suffix=""
|
|
416
|
+
|
|
417
|
+
flattened=$(printf '%s' "${script_content}" | tr '\r\n\t' ' ' | cut -c 1-160)
|
|
418
|
+
byte_count=$(printf '%s' "${script_content}" | wc -c | tr -d ' ')
|
|
419
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
420
|
+
digest=$(printf '%s' "${script_content}" | shasum -a 256 | cut -d' ' -f1)
|
|
421
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
422
|
+
digest=$(printf '%s' "${script_content}" | sha256sum | cut -d' ' -f1)
|
|
423
|
+
else
|
|
424
|
+
digest="unavailable"
|
|
425
|
+
fi
|
|
426
|
+
if [[ "${byte_count}" -gt 160 ]]; then
|
|
427
|
+
suffix="..."
|
|
428
|
+
fi
|
|
429
|
+
printf '[redacted install script sha256=%s bytes=%s preview=%s%s]' \
|
|
430
|
+
"${digest}" \
|
|
431
|
+
"${byte_count}" \
|
|
432
|
+
"${flattened}" \
|
|
433
|
+
"${suffix}"
|
|
434
|
+
}
|
|
435
|
+
|
|
361
436
|
# Function: check for suspicious postinstall scripts in new/changed dependencies
|
|
362
437
|
check_postinstall_scripts() {
|
|
363
438
|
local pkg_json="${PROJECT_DIR}/package.json"
|
|
@@ -411,17 +486,17 @@ check_postinstall_scripts() {
|
|
|
411
486
|
# Check for network calls in install scripts
|
|
412
487
|
if echo "${script_content}" | grep -qEi '(curl|wget|fetch|http|https|net\.|socket|dns)'; then
|
|
413
488
|
SUSPICIOUS=true
|
|
414
|
-
REASONS+=("Package '${pkg_name}' has install script with network access: ${script_content}")
|
|
489
|
+
REASONS+=("Package '${pkg_name}' has install script with network access: $(redact_install_script_content "${script_content}")")
|
|
415
490
|
fi
|
|
416
491
|
|
|
417
492
|
# Check for eval/exec in install scripts
|
|
418
493
|
if echo "${script_content}" | grep -qEi '(eval|exec|spawn|child_process|Function\()'; then
|
|
419
494
|
SUSPICIOUS=true
|
|
420
|
-
REASONS+=("Package '${pkg_name}' has install script with code execution: ${script_content}")
|
|
495
|
+
REASONS+=("Package '${pkg_name}' has install script with code execution: $(redact_install_script_content "${script_content}")")
|
|
421
496
|
fi
|
|
422
497
|
|
|
423
498
|
# Check for filesystem access outside project
|
|
424
|
-
if echo "${script_content}" | grep -qEi '(\/etc\/|\/home\/|~\/|\$HOME|\.ssh|\.env|\.aws|credentials)'; then
|
|
499
|
+
if echo "${script_content}" | grep -qEi '(\/etc\/|\/home\/|~\/|\$HOME|\.ssh|\.env|\.aws|credentials|~\/\.safedeps|\$HOME\/\.safedeps|\.safedeps\/|SAFEDEPS_HOME)'; then
|
|
425
500
|
SUSPICIOUS=true
|
|
426
501
|
REASONS+=("Package '${pkg_name}' has install script accessing sensitive paths")
|
|
427
502
|
fi
|
|
@@ -507,7 +582,74 @@ check_binaries() {
|
|
|
507
582
|
fi
|
|
508
583
|
}
|
|
509
584
|
|
|
585
|
+
check_npm_effect_closure() {
|
|
586
|
+
local lockfile="${PROJECT_DIR}/package-lock.json"
|
|
587
|
+
local closure_file
|
|
588
|
+
local provider_file
|
|
589
|
+
local miss_file
|
|
590
|
+
local package_name
|
|
591
|
+
local version
|
|
592
|
+
local miss_count
|
|
593
|
+
local vulnerable_summary
|
|
594
|
+
local kev_summary
|
|
595
|
+
|
|
596
|
+
[[ -f "${lockfile}" ]] || return 0
|
|
597
|
+
|
|
598
|
+
closure_file=$(mktemp "${TMPDIR:-/tmp}/safedeps-post-closure.XXXXXX") || return
|
|
599
|
+
provider_file=$(mktemp "${TMPDIR:-/tmp}/safedeps-post-provider.XXXXXX") || {
|
|
600
|
+
rm -f "${closure_file}"
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
miss_file=$(mktemp "${TMPDIR:-/tmp}/safedeps-post-miss.XXXXXX") || {
|
|
604
|
+
rm -f "${closure_file}" "${provider_file}"
|
|
605
|
+
return
|
|
606
|
+
}
|
|
607
|
+
: > "${miss_file}"
|
|
608
|
+
|
|
609
|
+
if ! safedeps_npm_lock_closure "${lockfile}" > "${closure_file}"; then
|
|
610
|
+
SUSPICIOUS=true
|
|
611
|
+
REASONS+=("npm package-lock closure could not be parsed")
|
|
612
|
+
rm -f "${closure_file}" "${provider_file}" "${miss_file}"
|
|
613
|
+
return
|
|
614
|
+
fi
|
|
615
|
+
|
|
616
|
+
while IFS=$'\t' read -r package_name version; do
|
|
617
|
+
[[ -n "${package_name}" && -n "${version}" ]] || continue
|
|
618
|
+
if ! safedeps_ledger_effect_check "npm" "${package_name}" "${version}" >/dev/null 2>&1; then
|
|
619
|
+
printf '%s@%s\n' "${package_name}" "${version}" >> "${miss_file}"
|
|
620
|
+
fi
|
|
621
|
+
done < <(jq -r '.[] | [.package, (.version | tostring)] | @tsv' "${closure_file}")
|
|
622
|
+
|
|
623
|
+
miss_count=$(wc -l < "${miss_file}" | tr -d ' ')
|
|
624
|
+
if [[ "${miss_count}" -gt 0 ]]; then
|
|
625
|
+
SUSPICIOUS=true
|
|
626
|
+
REASONS+=("npm closure contains ${miss_count} unapproved package(s): $(head -20 "${miss_file}" | paste -sd ', ' -)")
|
|
627
|
+
fi
|
|
628
|
+
|
|
629
|
+
if ! safedeps_providers_query_batch "npm" "${closure_file}" > "${provider_file}"; then
|
|
630
|
+
SUSPICIOUS=true
|
|
631
|
+
REASONS+=("npm closure OSV batch verification failed; fail-closed")
|
|
632
|
+
rm -f "${closure_file}" "${provider_file}" "${miss_file}"
|
|
633
|
+
return
|
|
634
|
+
fi
|
|
635
|
+
|
|
636
|
+
kev_summary=$(jq -r '[.[] | select(.status == "hard_block") | "\(.package)@\(.version)"] | join(", ")' "${provider_file}")
|
|
637
|
+
if [[ -n "${kev_summary}" ]]; then
|
|
638
|
+
SUSPICIOUS=true
|
|
639
|
+
REASONS+=("npm closure contains KEV-blocked package(s): ${kev_summary}")
|
|
640
|
+
fi
|
|
641
|
+
|
|
642
|
+
vulnerable_summary=$(jq -r '[.[] | select(.status == "vulnerable") | "\(.package)@\(.version)"] | join(", ")' "${provider_file}")
|
|
643
|
+
if [[ -n "${vulnerable_summary}" ]]; then
|
|
644
|
+
SUSPICIOUS=true
|
|
645
|
+
REASONS+=("npm closure contains vulnerable package(s): ${vulnerable_summary}")
|
|
646
|
+
fi
|
|
647
|
+
|
|
648
|
+
rm -f "${closure_file}" "${provider_file}" "${miss_file}"
|
|
649
|
+
}
|
|
650
|
+
|
|
510
651
|
# Run all checks
|
|
652
|
+
check_npm_effect_closure
|
|
511
653
|
check_postinstall_scripts
|
|
512
654
|
check_lockfile_diff
|
|
513
655
|
check_binaries
|
|
@@ -572,13 +714,28 @@ if [[ "${SUSPICIOUS}" == "true" ]]; then
|
|
|
572
714
|
Rollback warnings: ${WARNING_STR%%; }
|
|
573
715
|
LOG_EOF
|
|
574
716
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
717
|
+
jq -nc \
|
|
718
|
+
--arg reasons "${REASON_STR%%; }" \
|
|
719
|
+
--arg rollback_snapshot "${ROLLBACK_SNAPSHOT_ID}" \
|
|
720
|
+
--arg rolled_back "${ROLLED_BACK_STR%, }" \
|
|
721
|
+
--arg warnings "${WARNING_STR%%; }" \
|
|
722
|
+
--arg log_path "${GUARD_DIR}/reorg.log" \
|
|
723
|
+
'{
|
|
724
|
+
systemMessage: (
|
|
725
|
+
"safedeps: 의심스러운 패키지 변경 감지, 마지막으로 confirmed 된 안전 스냅샷으로 롤백했습니다.\n\n" +
|
|
726
|
+
"감지된 문제:\n" + $reasons + "\n\n" +
|
|
727
|
+
"롤백 기준 스냅샷: " + $rollback_snapshot + "\n" +
|
|
728
|
+
"롤백된 파일: " + $rolled_back +
|
|
729
|
+
(if $warnings == "" then "" else "\n\n추가 경고:\n" + $warnings end) +
|
|
730
|
+
"\n\n상세 로그: " + $log_path
|
|
731
|
+
)
|
|
732
|
+
}'
|
|
578
733
|
exit 0
|
|
579
734
|
fi
|
|
580
735
|
|
|
736
|
+
run_verified_npm_rebuild_if_injected
|
|
581
737
|
confirm_snapshot "${SNAPSHOT_ID}" "${DIR_HASH}"
|
|
582
738
|
cleanup_old_snapshots
|
|
739
|
+
emit_confirm_warnings_if_any
|
|
583
740
|
|
|
584
741
|
exit 0
|