@aldegad/safedeps 2.5.0 → 2.6.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/README.ko.md CHANGED
@@ -1,18 +1,30 @@
1
- # Safedeps
1
+ # safedeps
2
2
 
3
- > **모든 install 미확정 블록으로 본다 `safedeps` 안전한 것만 승인하고, 그렇지 않으면 reorg 한다.**
3
+ > **AI 코딩 에이전트가 취약하거나 미승인된 의존성을 설치하지 못하게 막고, 빠져나간 롤백한다.**
4
4
  >
5
- > OSV / CISA KEV / GitHub Advisory 의존성 spec사전 승인하고, Claude Code Codex CLI hook 에서 실제 설치 closure 를 강제하며, 승인과 어긋난 install 자동으로 마지막 안전 snapshot 으로 롤백한다.
5
+ > `safedeps` Claude Code·Codex CLI 에이전트가 실행하는 모든 의존성 install게이트한다. 패키지를 OSV / CISA KEV / GitHub Advisory 사전 승인하고, 실제로 lockfile 에 깔린 closure 를 다시 검증하며, 어긋난 자동으로 롤백한다. 전부 로컬에서 돌고 런타임 의존성은 0.
6
6
 
7
- *Detailed reference [README.md](./README.md) (영문, SSoT)*
7
+ - **사전 승인** — 모든 `pkg@version` 과 (npm 은) 그 전체 transitive closure 를 설치 *전에* OSV(정본)·CISA KEV·GitHub Advisory 로 검사한다.
8
+ - **실제 effect 강제** — 설치 후 실제 `package-lock.json` closure 를 다시 확인하므로, 래핑·난독화된 명령도 게이트를 못 빠져나간다.
9
+ - **롤백** — 미승인·신규 취약 패키지는 마지막 확정 안전 snapshot 으로 되돌린다. Claude Code 에서는 install 이 inert(`--ignore-scripts`)로 돌아 거부된 패키지의 lifecycle script 가 아예 실행되지 않는다.
8
10
 
9
- ---
11
+ ## Quickstart
10
12
 
11
- ## "reorg" 는 뭐고 왜 그 비유인가
13
+ ```bash
14
+ # 1. CLI 설치 — npm 패키지는 scoped, @aldegad/ 접두사 주의
15
+ npm install -g @aldegad/safedeps
12
16
 
13
- 블록체인에서 **reorg (재편성)** 미확정 블록 시퀀스를 무효화하고 마지막 확정된 안전 상태로 체인을 되돌린다. `safedeps` 는 같은 원리를 `node_modules` 적용한다 모든 install 은 일련의 공급망 보안 검사를 통과하기 전까지는 **미확정 블록 후보** 로 취급된다. 의심스러우면 도구가 **reorg** 를 수행한다 — lock 파일, `package.json`, `node_modules` 를 마지막 확정된 안전 snapshot 으로 되돌린다.
17
+ # 2. Claude Code / Codexhook 연결 (idempotent)
18
+ cd "$(npm root -g)/@aldegad/safedeps" && node scripts/install/install-safedeps-hooks.mjs
14
19
 
15
- 빠른 advisory 피드백, 관측 가능한 rollback, silent fallback 없음. command guard best-effort UX 이고, 설치 결과 effect 가 backstop 이다.
20
+ # 3. 이제 에이전트가 실행하는 모든 의존성 install 자동으로 게이트된다.
21
+ ```
22
+
23
+ > `safedeps` 는 CLI 명령어이고, npm 패키지는 **`@aldegad/safedeps`** 다 — npm 의 unscoped `safedeps` 는 무관한 남의 패키지. 전체 skill 소스 트리를 원하면 [설치](#설치) 참고.
24
+
25
+ ![safedeps 가 취약한 install 을 보류하고, 패치 버전은 통과시킨다](assets/demo.gif)
26
+
27
+ *Detailed reference → [README.md](./README.md) (영문, SSoT)*
16
28
 
17
29
  ---
18
30
 
@@ -193,6 +205,14 @@ node scripts/install/install-safedeps-recheck-agent.mjs install --hour 9 --minut
193
205
 
194
206
  ---
195
207
 
208
+ ## "reorg" 는 뭐고 왜 그 비유인가
209
+
210
+ 블록체인에서 **reorg (재편성)** 은 미확정 블록 시퀀스를 무효화하고 마지막 확정된 안전 상태로 체인을 되돌린다. `safedeps` 는 모든 install 을 똑같이 본다 — 일련의 공급망 보안 검사를 통과하기 전까지는 미확정 블록 후보다. 설치된 effect 가 어긋나면 도구가 **reorg** 를 수행한다 — lock 파일, `package.json`, `node_modules` 를 마지막 확정된 안전 snapshot 으로 되돌린다.
211
+
212
+ 하지만 reorg 는 **최전선이 아니라 backstop 이다.** 나쁜 install 의 대부분은 여기까지 오지도 않는다 — 사전 승인 게이트가 미승인·플래그된 패키지를 실행 전에 *거부* 하고, Claude Code 에서는 install 이 **inert (`--ignore-scripts`)** 로 돌아 closure 가 깨끗하다고 검증되기 전까지 lifecycle script 가 실행되지 않는다. reorg 가 발동하는 건 잔여 케이스뿐이다 — 승인된 직접 패키지가 미승인·취약 transitive 를 끌어오거나, 래핑된 명령이 advisory 계층을 빠져나간 경우 — 그리고 그때조차 실행되지도 못한 파일을 되돌린다.
213
+
214
+ 빠른 advisory 피드백, 관측 가능한 rollback, silent fallback 없음. command guard 는 best-effort UX 이고, 설치 결과 effect 가 backstop 이다.
215
+
196
216
  ## 다른 도구와 뭐가 다른가
197
217
 
198
218
  `safedeps` 는 **AI 에이전트가 코딩 중 install 명령을 작성하는 순간**에 끼어드는 도구다. CI 스캔, PR 권장, runtime sandbox 처럼 다른 시점에서 동작하는 도구들과 핵심 차별점이 여기 있다.
package/README.md CHANGED
@@ -1,14 +1,28 @@
1
- # Safedeps
1
+ # safedeps
2
2
 
3
- > **Treat every install as an unconfirmed block`safedeps` approves the safe ones, reorgs the rest.**
3
+ > **Stop your AI coding agent from installing vulnerable or unapproved dependencies and roll back the ones that slip through.**
4
4
  >
5
- > Pre-approve dependency installs against OSV / CISA KEV / GitHub Advisory, enforce the installed closure from Claude Code and Codex CLI hooks, and auto-rollback any install that diverges from the approved closure. *(한국어 README → [README.ko.md](./README.ko.md))*
5
+ > `safedeps` gates every dependency install your Claude Code or Codex CLI agent runs. It pre-approves packages against OSV / CISA KEV / GitHub Advisory, re-verifies the closure that actually lands in your lockfile, and auto-rolls-back anything that diverges. Local-only, with zero runtime dependencies. *(한국어 README → [README.ko.md](./README.ko.md))*
6
6
 
7
- ## Why "reorg"?
7
+ - **Pre-approve** — every `pkg@version`, plus its full transitive closure for npm, is cleared against OSV (canonical), CISA KEV, and GitHub Advisory *before* it installs.
8
+ - **Enforce the real effect** — after the install, the actual `package-lock.json` closure is re-checked, so a wrapped or obfuscated command can't sneak a package past the gate.
9
+ - **Roll back** — anything unapproved or newly-vulnerable is reverted to the last confirmed safe snapshot. On Claude Code the install runs inert (`--ignore-scripts`), so a rejected package's lifecycle scripts never run.
8
10
 
9
- In blockchain networks, a **reorganization (reorg)** invalidates a sequence of blocks and reverts the chain to a previously confirmed safe state. `safedeps` applies the same principle to your `node_modules`: every install is treated as an unconfirmed block candidate until it passes a battery of supply-chain security checks. If anything looks wrong, the tool performs a **reorg** -- rolling back lock files, `package.json`, and `node_modules` to the last confirmed safe snapshot.
11
+ ## Quickstart
10
12
 
11
- Fast advisory feedback, observable rollback, and no hidden fallback. The command guard is best-effort UX; the installed effect is the backstop.
13
+ ```bash
14
+ # 1. Install the CLI — the npm package is scoped, note the @aldegad/ prefix
15
+ npm install -g @aldegad/safedeps
16
+
17
+ # 2. Wire the hooks into Claude Code / Codex (idempotent)
18
+ cd "$(npm root -g)/@aldegad/safedeps" && node scripts/install/install-safedeps-hooks.mjs
19
+
20
+ # 3. Done — every dependency install your agent runs is now gated.
21
+ ```
22
+
23
+ > `safedeps` is the CLI command; the npm package is **`@aldegad/safedeps`** — the unscoped `safedeps` on npm is an unrelated package. Prefer the full skill source tree? See [Installation](#installation).
24
+
25
+ ![safedeps withholds a vulnerable install, then clears the patched version](assets/demo.gif)
12
26
 
13
27
  ## Distribution Model
14
28
 
@@ -115,6 +129,14 @@ After the install command completes, the verify hook analyzes what changed. For
115
129
  4. The event is logged to `~/.safedeps/reorg.log`.
116
130
  5. Claude Code receives a system message detailing the detected threats and rollback actions.
117
131
 
132
+ ## Why "reorg"?
133
+
134
+ The name borrows from blockchain, where a **reorganization (reorg)** invalidates a sequence of unconfirmed blocks and reverts the chain to its last confirmed safe state. `safedeps` treats every install the same way: an unconfirmed block candidate until it passes a battery of supply-chain checks. If the installed effect diverges, the tool performs a **reorg** -- rolling the lock file, `package.json`, and `node_modules` back to the last confirmed safe snapshot.
135
+
136
+ But the reorg is the **backstop, not the front line.** Most bad installs never reach it: the pre-approval gate *denies* an unapproved or flagged package before it runs, and on Claude Code the install runs **inert** (`--ignore-scripts`) so lifecycle scripts do not execute until the closure verifies clean. The reorg fires for the residual case -- an approved direct package that pulls in an unapproved or vulnerable transitive, or a wrapped command that slips past the advisory layer -- and even then it rolls back files that never got to run.
137
+
138
+ Fast advisory feedback, observable rollback, and no hidden fallback. The command guard is best-effort UX; the installed effect is the backstop.
139
+
118
140
  ## The Blockchain Analogy
119
141
 
120
142
  | Blockchain Concept | Safedeps Equivalent |
package/bin/safedeps CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  set -euo pipefail
9
9
 
10
- SAFEDEPS_VERSION="2.5.0"
10
+ SAFEDEPS_VERSION="2.6.0"
11
11
 
12
12
  # ---- repo / lib bootstrap ----------------------------------------------------
13
13
 
@@ -340,7 +340,7 @@ sf_cmd_check_npm_full_closure() {
340
340
  evidence_file=$(sf_mktemp_evidence)
341
341
  transitive_file=$(sf_closure_temp_file)
342
342
 
343
- sf_spinner_start "npm closure 해석 중 (${pkg}@${version})"
343
+ sf_spinner_start "resolving npm closure (${pkg}@${version})"
344
344
  if ! sf_npm_closure_for_spec "${pkg}" "${version}" "${closure_file}"; then
345
345
  sf_spinner_stop
346
346
  rm -f "${closure_file}" "${batch_file}" "${evidence_file}" "${transitive_file}"
@@ -352,10 +352,10 @@ sf_cmd_check_npm_full_closure() {
352
352
  fi
353
353
  sf_spinner_stop
354
354
 
355
- sf_spinner_start "closure 취약점 batch 조회 중 (OSV / KEV)"
355
+ sf_spinner_start "batch-checking closure for advisories (OSV / KEV)"
356
356
  if ! sf_npm_batch_check_closure "${closure_file}" "${batch_file}"; then
357
357
  sf_spinner_stop
358
- sf_err "OSV batch 응답 없음 — fail-closed (closure cache miss + 라이브 실패)"
358
+ sf_err "no OSV batch response — fail-closed (closure cache miss + live query failed)"
359
359
  sf_advisory_log "check fail-closed npm-closure package=${pkg} version=${version}"
360
360
  rm -f "${closure_file}" "${batch_file}" "${evidence_file}" "${transitive_file}"
361
361
  if [[ "${SAFEDEPS_JSON_MODE}" -eq 1 ]]; then
@@ -377,7 +377,7 @@ sf_cmd_check_npm_full_closure() {
377
377
  if [[ "${kev_count}" -gt 0 ]]; then
378
378
  local kev_summary
379
379
  kev_summary=$(jq -r '[.[] | select(.status == "hard_block") | "\(.package)@\(.version)" + (if .direct then " (direct)" else " (transitive)" end)] | join(", ")' "${batch_file}")
380
- sf_err "KEV 매칭 closure 감지 설치 차단: ${kev_summary}"
380
+ sf_err "KEV-matched package in closure — install blocked: ${kev_summary}"
381
381
  sf_advisory_log "check block(KEV closure) package=${pkg} version=${version} affected=${kev_summary}"
382
382
  if [[ "${SAFEDEPS_JSON_MODE}" -eq 1 ]]; then
383
383
  sf_npm_emit_closure_block_json "kev_hard_block" "${ecosystem}" "${pkg}" "${range}" "${version}" "${batch_file}"
@@ -402,7 +402,7 @@ sf_cmd_check_npm_full_closure() {
402
402
  fi
403
403
 
404
404
  for patched_version in "${patched_versions[@]+${patched_versions[@]}}"; do
405
- sf_warn "${pkg}@${version} direct 취약후보 ${patched_version} closure 재조회 중."
405
+ sf_warn "${pkg}@${version} direct vulnerablere-checking closure for candidate ${patched_version}."
406
406
  if ! sf_npm_closure_for_spec "${pkg}" "${patched_version}" "${closure_file}"; then
407
407
  continue
408
408
  fi
@@ -422,7 +422,7 @@ sf_cmd_check_npm_full_closure() {
422
422
  local hash; hash=$(jq -r '.hash' <<< "${spec_json}")
423
423
  local expires_at; expires_at=$(jq -r '.expires_at' <<< "${spec_json}")
424
424
  local transitive_count; transitive_count=$(jq 'length' "${transitive_file}")
425
- sf_ok "${pkg}@${patched_version} full closure 승인 (transitive ${transitive_count}, until ${expires_at})"
425
+ sf_ok "${pkg}@${patched_version} full closure cleared (transitive ${transitive_count}, until ${expires_at})"
426
426
  sf_info "ledger: ${hash}"
427
427
  sf_advisory_log "check approve(patched closure) package=${pkg} version=${patched_version} hash=${hash} transitive=${transitive_count} prev_version=${version}"
428
428
  rm -f "${direct_evidence}" "${closure_file}" "${batch_file}" "${evidence_file}" "${transitive_file}"
@@ -443,7 +443,7 @@ sf_cmd_check_npm_full_closure() {
443
443
 
444
444
  local affected_summary
445
445
  affected_summary=$(jq -r '[.[] | select(.status == "vulnerable") | "\(.package)@\(.version)" + (if .direct then " (direct)" else " (transitive)" end)] | join(", ")' "${batch_file}")
446
- sf_warn "closure 취약 패키지 감지 승인 보류: ${affected_summary}"
446
+ sf_warn "vulnerable package in closureapproval withheld: ${affected_summary}"
447
447
  sf_advisory_log "check block(vulnerable closure) package=${pkg} version=${version} affected=${affected_summary}"
448
448
  if [[ "${SAFEDEPS_JSON_MODE}" -eq 1 ]]; then
449
449
  sf_npm_emit_closure_block_json "${block_result}" "${ecosystem}" "${pkg}" "${range}" "${version}" "${batch_file}"
@@ -459,7 +459,7 @@ sf_cmd_check_npm_full_closure() {
459
459
  local hash; hash=$(jq -r '.hash' <<< "${spec_json}")
460
460
  local expires_at; expires_at=$(jq -r '.expires_at' <<< "${spec_json}")
461
461
  local transitive_count; transitive_count=$(jq 'length' "${transitive_file}")
462
- sf_ok "${pkg}@${version} full closure 승인 (transitive ${transitive_count}, until ${expires_at})"
462
+ sf_ok "${pkg}@${version} full closure cleared (transitive ${transitive_count}, until ${expires_at})"
463
463
  sf_info "ledger: ${hash}"
464
464
  sf_advisory_log "check approve(clean closure) package=${pkg} version=${version} hash=${hash} transitive=${transitive_count}"
465
465
  rm -f "${closure_file}" "${batch_file}" "${evidence_file}" "${transitive_file}"
@@ -506,10 +506,10 @@ cmd_check() {
506
506
  range=$(sf_parse_pkg_spec "${pkg_spec}" | sed -n '2p')
507
507
 
508
508
  local version
509
- sf_spinner_start "버전 해석 (${pkg}@${range})"
509
+ sf_spinner_start "resolving version (${pkg}@${range})"
510
510
  if ! version=$(sf_resolve_version "${ecosystem}" "${pkg}" "${range}"); then
511
511
  sf_spinner_stop
512
- sf_err "버전 해석 실패: ${pkg}@${range}"
512
+ sf_err "version resolution failed: ${pkg}@${range}"
513
513
  if [[ "${SAFEDEPS_JSON_MODE}" -eq 1 ]]; then
514
514
  jq -nc --arg ecosystem "${ecosystem}" --arg package "${pkg}" --arg range "${range}" \
515
515
  '{command:"check", ecosystem:$ecosystem, package:$package, input_range:$range, result:"error", error:"version_resolution_failed"}'
@@ -526,7 +526,7 @@ cmd_check() {
526
526
  hash=$(jq -r '.hash' <<< "${ledger_check}")
527
527
  approved_at=$(jq -r '.spec.approved_at // "n/a"' <<< "${ledger_check}")
528
528
  expires_at=$(jq -r '.spec.expires_at // "n/a"' <<< "${ledger_check}")
529
- sf_ok "${pkg}@${version} 이미 승인됨 (until ${expires_at})"
529
+ sf_ok "${pkg}@${version} already approved (until ${expires_at})"
530
530
  sf_info "ledger: ${hash}"
531
531
  if [[ "${SAFEDEPS_JSON_MODE}" -eq 1 ]]; then
532
532
  jq -nc \
@@ -551,10 +551,10 @@ cmd_check() {
551
551
 
552
552
  # Provider query (canonical truth = OSV; KEV overlay; GHSA enrichment)
553
553
  local provider_json
554
- sf_spinner_start "취약점 조회 (OSV / KEV / GHSA)"
554
+ sf_spinner_start "checking advisories (OSV / KEV / GHSA)"
555
555
  if ! provider_json=$(safedeps_providers_query "${ecosystem}" "${pkg}" "${version}"); then
556
556
  sf_spinner_stop
557
- sf_err "OSV primary 응답 없음 — fail-closed (cache miss + 라이브 실패)"
557
+ sf_err "no OSV primary response — fail-closed (cache miss + live query failed)"
558
558
  sf_advisory_log "check fail-closed ecosystem=${ecosystem} package=${pkg} version=${version}"
559
559
  if [[ "${SAFEDEPS_JSON_MODE}" -eq 1 ]]; then
560
560
  jq -nc \
@@ -577,11 +577,11 @@ cmd_check() {
577
577
 
578
578
  case "${status}" in
579
579
  hard_block)
580
- sf_err "KEV 매칭 — ${pkg}@${version} 실제 야생에서 exploit 확인됨. 설치 차단."
580
+ sf_err "KEV match — ${pkg}@${version} is exploited in the wild. install blocked."
581
581
  local kev_cves
582
582
  kev_cves=$(jq -r '[.kev.matches[]?.cveID] | unique | join(", ")' <<< "${provider_json}")
583
- [[ -n "${kev_cves}" ]] && sf_info "관련 CVE: ${kev_cves}"
584
- sf_warn "대체 모듈을 검토하세요. spec ledger 승인되지 않습니다."
583
+ [[ -n "${kev_cves}" ]] && sf_info "related CVEs: ${kev_cves}"
584
+ sf_warn "consider an alternative package; this spec is not approved in the ledger."
585
585
  sf_advisory_log "check block(KEV) ecosystem=${ecosystem} package=${pkg} version=${version} cves=${kev_cves}"
586
586
  if [[ "${SAFEDEPS_JSON_MODE}" -eq 1 ]]; then
587
587
  jq -c \
@@ -610,11 +610,11 @@ cmd_check() {
610
610
 
611
611
  for patched_version in "${patched_versions[@]}"; do
612
612
  last_patched="${patched_version}"
613
- sf_warn "${pkg}@${version} ${vuln_count} CVE — 후보 ${patched_version} 재조회 중."
614
- sf_spinner_start "${patched_version} 재조회 중"
613
+ sf_warn "${pkg}@${version} has ${vuln_count} CVE(s)re-checking candidate ${patched_version}."
614
+ sf_spinner_start "re-checking ${patched_version}"
615
615
  if ! narrow_json=$(safedeps_providers_query "${ecosystem}" "${pkg}" "${patched_version}"); then
616
616
  sf_spinner_stop
617
- sf_err "${pkg}@${patched_version} 재조회 실패 — fail-closed"
617
+ sf_err "${pkg}@${patched_version} re-check failed — fail-closed"
618
618
  rm -f "${tmp_evidence}"
619
619
  if [[ "${SAFEDEPS_JSON_MODE}" -eq 1 ]]; then
620
620
  jq -nc \
@@ -630,7 +630,7 @@ cmd_check() {
630
630
  narrow_status=$(jq -r '.status' <<< "${narrow_json}")
631
631
  last_status="${narrow_status}"
632
632
  if [[ "${narrow_status}" != "clean" ]]; then
633
- sf_warn "패치 후보 ${patched_version} 깨끗하지 않음 (status=${narrow_status}); 다음 후보를 확인합니다."
633
+ sf_warn "patch candidate ${patched_version} is not clean either (status=${narrow_status}); trying the next candidate."
634
634
  continue
635
635
  fi
636
636
 
@@ -644,7 +644,7 @@ cmd_check() {
644
644
  rm -f "${narrow_evidence}" "${tmp_evidence}"
645
645
  local hash; hash=$(jq -r '.hash' <<< "${spec_json}")
646
646
  local expires_at; expires_at=$(jq -r '.expires_at' <<< "${spec_json}")
647
- sf_ok "${pkg}@${patched_version} 승인 (until ${expires_at})"
647
+ sf_ok "${pkg}@${patched_version} approved (until ${expires_at})"
648
648
  sf_info "ledger: ${hash}"
649
649
  sf_advisory_log "check approve(patched) ecosystem=${ecosystem} package=${pkg} version=${patched_version} hash=${hash} prev_version=${version}"
650
650
  if [[ "${SAFEDEPS_JSON_MODE}" -eq 1 ]]; then
@@ -658,7 +658,7 @@ cmd_check() {
658
658
  return 0
659
659
  done
660
660
 
661
- sf_err "패치 후보를 모두 재조회했지만 clean 후보가 없음 (last=${last_patched}, status=${last_status})"
661
+ sf_err "re-checked all patch candidates but none are clean (last=${last_patched}, status=${last_status})"
662
662
  rm -f "${tmp_evidence}"
663
663
  if [[ "${SAFEDEPS_JSON_MODE}" -eq 1 ]]; then
664
664
  jq -nc \
@@ -669,7 +669,7 @@ cmd_check() {
669
669
  fi
670
670
  return 2
671
671
  else
672
- sf_warn "${pkg}@${version} ${vuln_count} CVE — 사용 가능한 patch 없음. 승인 보류."
672
+ sf_warn "${pkg}@${version} has ${vuln_count} CVE(s)no patch available. approval withheld."
673
673
  sf_advisory_log "check warn(no-patch) ecosystem=${ecosystem} package=${pkg} version=${version} vulns=${vuln_count}"
674
674
  rm -f "${tmp_evidence}"
675
675
  if [[ "${SAFEDEPS_JSON_MODE}" -eq 1 ]]; then
@@ -688,7 +688,7 @@ cmd_check() {
688
688
  rm -f "${tmp_evidence}"
689
689
  local hash; hash=$(jq -r '.hash' <<< "${spec_json}")
690
690
  local expires_at; expires_at=$(jq -r '.expires_at' <<< "${spec_json}")
691
- sf_ok "${pkg}@${version} 승인 (until ${expires_at})"
691
+ sf_ok "${pkg}@${version} approved (until ${expires_at})"
692
692
  sf_info "ledger: ${hash}"
693
693
  sf_advisory_log "check approve(clean) ecosystem=${ecosystem} package=${pkg} version=${version} hash=${hash}"
694
694
  if [[ "${SAFEDEPS_JSON_MODE}" -eq 1 ]]; then
@@ -702,7 +702,7 @@ cmd_check() {
702
702
  ;;
703
703
 
704
704
  *)
705
- sf_err "예상치 못한 provider status: ${status}"
705
+ sf_err "unexpected provider status: ${status}"
706
706
  rm -f "${tmp_evidence}"
707
707
  return 4
708
708
  ;;
@@ -757,7 +757,7 @@ cmd_ledger() {
757
757
  fi
758
758
 
759
759
  if [[ ${#entries[@]} -eq 0 ]]; then
760
- sf_info "approved-specs 비어있음 (${SAFEDEPS_LEDGER_DIR})"
760
+ sf_info "approved-specs is empty (${SAFEDEPS_LEDGER_DIR})"
761
761
  return 0
762
762
  fi
763
763
 
@@ -830,7 +830,7 @@ cmd_revoke() {
830
830
  local target_file=""
831
831
  if [[ "${arg1}" == sha256:* ]]; then
832
832
  target_file=$(safedeps_ledger_path_for_hash "${arg1}")
833
- [[ -f "${target_file}" ]] || { sf_err "ledger entry 없음: ${arg1}"; return 1; }
833
+ [[ -f "${target_file}" ]] || { sf_err "no ledger entry: ${arg1}"; return 1; }
834
834
  else
835
835
  # one or two args. Two = ecosystem + pkg@version. One = pkg@version (scan).
836
836
  if [[ -n "${arg2}" ]]; then
@@ -839,7 +839,7 @@ cmd_revoke() {
839
839
  version=$(sf_parse_pkg_spec "${arg2}" | sed -n '2p')
840
840
  [[ -n "${version}" ]] || { sf_eprintf "safedeps: revoke needs pkg@version, got '${arg2}'"; return 4; }
841
841
  target_file=$(safedeps_ledger_path "${arg1}" "${pkg}" "${version}")
842
- [[ -f "${target_file}" ]] || { sf_err "ledger entry 없음: ${arg1} ${pkg}@${version}"; return 1; }
842
+ [[ -f "${target_file}" ]] || { sf_err "no ledger entry: ${arg1} ${pkg}@${version}"; return 1; }
843
843
  else
844
844
  local pkg version
845
845
  pkg=$(sf_parse_pkg_spec "${arg1}" | sed -n '1p')
@@ -855,9 +855,9 @@ cmd_revoke() {
855
855
  fi
856
856
  done < <(find "${SAFEDEPS_LEDGER_DIR}" -maxdepth 1 -name '*.json' -type f -print0 2>/dev/null)
857
857
  case "${#matches[@]}" in
858
- 0) sf_err "ledger entry 없음: ${pkg}@${version}"; return 1 ;;
858
+ 0) sf_err "no ledger entry: ${pkg}@${version}"; return 1 ;;
859
859
  1) target_file="${matches[0]}" ;;
860
- *) sf_err "${pkg}@${version} 여러 ecosystem 에서 매칭됨 ecosystem 명시하세요"; return 4 ;;
860
+ *) sf_err "${pkg}@${version} matched in multiple ecosystemsspecify the ecosystem"; return 4 ;;
861
861
  esac
862
862
  fi
863
863
  fi
@@ -870,7 +870,7 @@ cmd_revoke() {
870
870
  local revoked_json
871
871
  revoked_json=$(safedeps_ledger_revoke "${ecosystem}" "${pkg}" "${version}" "${reason}")
872
872
  sf_advisory_log "revoke ecosystem=${ecosystem} package=${pkg} version=${version} reason=${reason}"
873
- sf_ok "취소: ${ecosystem} ${pkg}@${version}"
873
+ sf_ok "revoked: ${ecosystem} ${pkg}@${version}"
874
874
  sf_info "reason: ${reason}"
875
875
 
876
876
  if [[ "${SAFEDEPS_JSON_MODE}" -eq 1 ]]; then
@@ -909,17 +909,17 @@ cmd_recheck() {
909
909
  hash=$(jq -r '.hash // ""' "${f}")
910
910
  [[ -n "${revoked_at}" ]] && continue
911
911
  if [[ -f "${SAFEDEPS_ADVISORY_LOG}" ]] && ! sf_ledger_has_approval_provenance "${hash}" "${ecosystem}" "${pkg}" "${version}"; then
912
- sf_warn " provenance 없음 위조 의심 flag (revoke 안 함)"
912
+ sf_warn " no provenance — flagged as suspected forgery (not revoked)"
913
913
  suspected_forgery_arr=$(jq -c \
914
914
  --arg ecosystem "${ecosystem}" --arg package "${pkg}" --arg version "${version}" --arg hash "${hash}" \
915
915
  '. + [{ecosystem:$ecosystem, package:$package, version:$version, hash:$hash, reason:"missing_advisory_log_approval"}]' <<< "${suspected_forgery_arr}")
916
916
  fi
917
917
  checked=$(( checked + 1 ))
918
918
 
919
- sf_info "재검증 ${ecosystem} ${pkg}@${version}"
919
+ sf_info "re-checking ${ecosystem} ${pkg}@${version}"
920
920
  local pj
921
921
  if ! pj=$(safedeps_providers_query "${ecosystem}" "${pkg}" "${version}" 2>/dev/null); then
922
- sf_warn " provider 응답 없음 skip"
922
+ sf_warn " no provider responseskipped"
923
923
  continue
924
924
  fi
925
925
  local s; s=$(jq -r '.status' <<< "${pj}")
@@ -932,12 +932,12 @@ cmd_recheck() {
932
932
  safedeps_ledger_revoke "${ecosystem}" "${pkg}" "${version}" "${reason}" >/dev/null
933
933
  sf_advisory_log "re-check revoke ecosystem=${ecosystem} package=${pkg} version=${version} status=${s}"
934
934
  if [[ "${s}" == "hard_block" ]]; then
935
- sf_err " KEV 매칭 revoke"
935
+ sf_err " KEV match -> revoked"
936
936
  kev_hit_arr=$(jq -c \
937
937
  --arg ecosystem "${ecosystem}" --arg package "${pkg}" --arg version "${version}" \
938
938
  '. + [{ecosystem:$ecosystem, package:$package, version:$version, status:"hard_block"}]' <<< "${kev_hit_arr}")
939
939
  else
940
- sf_warn " CVE 매치 revoke"
940
+ sf_warn " new CVE match -> revoked"
941
941
  newly_vuln_arr=$(jq -c \
942
942
  --arg ecosystem "${ecosystem}" --arg package "${pkg}" --arg version "${version}" \
943
943
  '. + [{ecosystem:$ecosystem, package:$package, version:$version, status:"vulnerable"}]' <<< "${newly_vuln_arr}")
@@ -961,15 +961,15 @@ cmd_recheck() {
961
961
  return 0
962
962
  fi
963
963
 
964
- sf_info "검증 완료: ${checked} 개 중 ${still_clean} clean"
964
+ sf_info "re-check complete: ${still_clean}/${checked} still clean"
965
965
  local nv kv
966
966
  nv=$(jq -r 'length' <<< "${newly_vuln_arr}")
967
967
  kv=$(jq -r 'length' <<< "${kev_hit_arr}")
968
968
  local fg
969
969
  fg=$(jq -r 'length' <<< "${suspected_forgery_arr}")
970
- [[ "${nv}" -gt 0 ]] && sf_warn " CVE 매치로 ${nv} revoke"
971
- [[ "${kv}" -gt 0 ]] && sf_err "KEV 매치로 ${kv} revoke"
972
- [[ "${fg}" -gt 0 ]] && sf_warn "approval provenance 없는 ledger entry ${fg} flag"
970
+ [[ "${nv}" -gt 0 ]] && sf_warn "revoked ${nv} for new CVE match"
971
+ [[ "${kv}" -gt 0 ]] && sf_err "revoked ${kv} for KEV match"
972
+ [[ "${fg}" -gt 0 ]] && sf_warn "flagged ${fg} ledger entries with no approval provenance"
973
973
  }
974
974
 
975
975
  # ---- migrate -----------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aldegad/safedeps",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Dependency install safety gate with OSV-backed advisory checks, approved-spec ledger enforcement, and reorg rollback hooks",
5
5
  "main": "bin/safedeps",
6
6
  "bin": {
@@ -781,12 +781,12 @@ LOG_EOF
781
781
  --arg log_path "${GUARD_DIR}/reorg.log" \
782
782
  '{
783
783
  systemMessage: (
784
- "safedeps: 의심스러운 패키지 변경 감지, 마지막으로 confirmed 안전 스냅샷으로 롤백했습니다.\n\n" +
785
- "감지된 문제:\n" + $reasons + "\n\n" +
786
- "롤백 기준 스냅샷: " + $rollback_snapshot + "\n" +
787
- "롤백된 파일: " + $rolled_back +
788
- (if $warnings == "" then "" else "\n\n추가 경고:\n" + $warnings end) +
789
- "\n\n상세 로그: " + $log_path
784
+ "safedeps: suspicious dependency change detected rolled back to the last confirmed safe snapshot.\n\n" +
785
+ "Detected problems:\n" + $reasons + "\n\n" +
786
+ "Rollback snapshot: " + $rollback_snapshot + "\n" +
787
+ "Rolled-back files: " + $rolled_back +
788
+ (if $warnings == "" then "" else "\n\nAdditional warnings:\n" + $warnings end) +
789
+ "\n\nDetails log: " + $log_path
790
790
  )
791
791
  }'
792
792
  exit 0
@@ -190,6 +190,50 @@ EOF
190
190
  grep -qx 'rebuild' "${tmp_root}/npm-calls.log" || fail "post hook runs npm rebuild after verified injected install"
191
191
  pass "post hook rebuilds after verified inert install"
192
192
 
193
+ # Reorg must actually revert the on-disk lockfile, not just print the message. The
194
+ # missing-transitive test below proves the systemMessage; this proves the stronger
195
+ # claim — a tampered lockfile is restored byte-for-byte to the last confirmed safe
196
+ # snapshot on disk. Regression guard so a future change cannot break the rollback
197
+ # while keeping the message green. Stub npm keeps `npm ci` from rewriting the file.
198
+ revert_project="${tmp_root}/revert-project"
199
+ mkdir -p "${revert_project}"
200
+ printf '{"dependencies":{"fixture-parent":"1.0.0"}}\n' > "${revert_project}/package.json"
201
+ cat > "${revert_project}/package-lock.json" <<'EOF'
202
+ {
203
+ "name": "revert-project",
204
+ "lockfileVersion": 3,
205
+ "packages": {
206
+ "": {"dependencies": {"fixture-parent": "1.0.0"}},
207
+ "node_modules/fixture-parent": {"version": "1.0.0", "dependencies": {"fixture-child": "1.0.0"}},
208
+ "node_modules/fixture-child": {"version": "1.0.0"}
209
+ }
210
+ }
211
+ EOF
212
+ cp "${revert_project}/package-lock.json" "${tmp_root}/revert-safe-lock.json"
213
+ scripts/safedeps-pre-guard.sh > /dev/null <<EOF
214
+ {"tool_name":"Bash","tool_input":{"command":"npm install fixture-parent@1.0.0"},"cwd":"${revert_project}"}
215
+ EOF
216
+ cat > "${revert_project}/package-lock.json" <<'EOF'
217
+ {
218
+ "name": "revert-project",
219
+ "lockfileVersion": 3,
220
+ "packages": {
221
+ "": {"dependencies": {"fixture-parent": "1.0.0"}},
222
+ "node_modules/fixture-parent": {"version": "1.0.0", "dependencies": {"fixture-child": "1.0.0"}},
223
+ "node_modules/fixture-child": {"version": "1.0.0"},
224
+ "node_modules/fixture-evil": {"version": "6.6.6", "resolved": "git://evil.example.com/fixture-evil.git"}
225
+ }
226
+ }
227
+ EOF
228
+ revert_post=$(
229
+ PATH="${stub_bin}:${PATH}" scripts/safedeps-post-verify.sh <<EOF
230
+ {"tool_name":"Bash","tool_input":{"command":"npm install fixture-parent@1.0.0"},"cwd":"${revert_project}"}
231
+ EOF
232
+ )
233
+ grep -q 'suspicious dependency change detected' <<< "${revert_post}" || fail "reorg fires on a tampered lockfile"
234
+ cmp -s "${revert_project}/package-lock.json" "${tmp_root}/revert-safe-lock.json" || fail "reorg restores the exact safe lockfile content on disk"
235
+ pass "reorg reverts a tampered lockfile to safe content on disk"
236
+
193
237
  export SAFEDEPS_HOME="${tmp_root}/safe-missing-transitive"
194
238
  export SAFEDEPS_OSV_API_URL="http://127.0.0.1:${port}/osv/v1/query"
195
239
  export SAFEDEPS_OSV_BATCH_API_URL="http://127.0.0.1:${port}/osv/v1/querybatch"
@@ -223,9 +267,15 @@ missing_post=$(
223
267
  {"tool_name":"Bash","tool_input":{"command":"npm install fixture-parent@1.0.0"},"cwd":"${missing_project}"}
224
268
  EOF
225
269
  )
226
- grep -q '의심스러운 패키지 변경 감지' <<< "${missing_post}" || fail "post hook reorgs unapproved transitive package"
270
+ grep -q 'suspicious dependency change detected' <<< "${missing_post}" || fail "post hook reorgs unapproved transitive package"
227
271
  grep -q 'fixture-child@1.0.0' <<< "${missing_post}" || fail "post hook names unapproved transitive package"
228
- pass "post hook reorgs unapproved transitive package"
272
+ # Not just the message — the unapproved transitive must be gone from the on-disk
273
+ # lockfile. (Reorg removes the tampered lockfile; a no-network reinstall may recreate
274
+ # an empty one, so assert fixture-child is absent rather than the file itself.)
275
+ if grep -q 'fixture-child' "${missing_project}/package-lock.json" 2>/dev/null; then
276
+ fail "post hook reorg leaves the unapproved transitive in the on-disk lockfile"
277
+ fi
278
+ pass "post hook reorgs unapproved transitive package (verified on disk)"
229
279
 
230
280
  export SAFEDEPS_HOME="${tmp_root}/safe"
231
281
  export SAFEDEPS_OSV_API_URL="http://127.0.0.1:${port}/osv/v1/query"
@@ -262,7 +262,7 @@ tamper_post=$(
262
262
  jq -nc --arg cwd "${project_dir}" '{tool_name:"Bash",tool_input:{command:"npm install ledger-tamper@1.0.0"},cwd:$cwd}' |
263
263
  HOME="${tamper_home}" SAFEDEPS_HOME="${tamper_safe}" scripts/safedeps-post-verify.sh
264
264
  )
265
- grep -q '의심스러운 패키지 변경 감지' <<< "${tamper_post}" || fail "post hook reorgs safedeps ledger tamper script"
265
+ grep -q 'suspicious dependency change detected' <<< "${tamper_post}" || fail "post hook reorgs safedeps ledger tamper script"
266
266
  pass "post hook reorgs safedeps ledger tamper script"
267
267
 
268
268
  fixture_json="${tmp_root}/recheck-fixture.json"