Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions ARCHITECTURE.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,20 @@ TIER 3 — ENRICHMENT / CROSS-CHECK
},
"transitive_specs": [
{ "ecosystem": "npm", "package": "…", "version": "…" }
]
],
"project_context": null
}
```

핵심 필드:

- `hash` — `(ecosystem, package, version)` 의 deterministic hash. hook 이 명령에서 같은 hash 를 뽑아 ledger 를 조회한다.
- `hash` — `(ecosystem, package, version)` 의 deterministic hash. `project_context.context_hash` 가 있으면 함께 접어 넣는다. hook 이 명령(과, 있다면 살아있는 project context)에서 같은 hash 를 뽑아 ledger 를 조회한다.
- `approved_at` / `expires_at` — lifecycle TTL, 기본 30일. 만료 후엔 새 CVE 가능성이 있어 자동 revoke + re-check 강제.
- `evidence` — 승인 시점에 어느 source 가 무엇을 봤는지. audit trail.
- `transitive_specs` — direct entry 가 승인한 전체 transitive closure. npm effect gate 는 lockfile 에 있으면서 direct entry 에도 이 배열에도 없는 `pkg@version` 을 reorg 한다.
- `project_context` — 일반 published-package 승인은 `null`, Yarn 프로젝트의 resolved closure 에서 온 승인이면 `{ type: "yarn-project-lockfile", context_hash, project_root, manifest_path, lockfile_path }` (4장 "Yarn project-scoped closure" 참고). `context_hash` 는 project directory, 루트 `resolutions`, `yarn.lock` content 의 hash 다.

**Project-scoped isolation.** `project_context` 가 있는 승인은 `(ecosystem, package, version)` 에 더해 `context_hash` 로도 키가 잡히므로, 같은 spec 의 package-only 승인과는 다른 ledger path 에 놓이고 다른 프로젝트나 `resolutions`/`yarn.lock` 이 바뀐 이후의 같은 프로젝트에서는 조회에 성공하지 못한다 (hash 가 함께 바뀌므로). `safedeps_ledger_check` 는 호출자의 살아있는 context hash 를 저장된 값과 비교해 다르면 `reason: "context_mismatch"` 로 거부한다 — 승인이 프로젝트 경계를 조용히 넘어가는 일은 없다.

Lifecycle:

Expand Down Expand Up @@ -188,6 +192,35 @@ safedeps check npm "@jackwener/opencli@^1.7.0"

npm 은 "OSV query" 가 **전체 resolved closure** 를 `/v1/querybatch` 한 번으로 돌고, 승인 entry 가 모든 transitive 를 `transitive_specs` 에 기록한다.

**Yarn project-scoped closure.** 새로운 published-package probe 로 넘어가기 전에, `lib/npm/closure.sh` 의 `safedeps_npm_yarn_project_closure` 가 먼저 canonical Yarn project context 를 찾는다.

```
project context 해석 (cwd 에서 위로 탐색, .git 경계에서 중단):
├─► package.json 에 비어있지 않은 루트 `resolutions` + 옆에 yarn.lock
│ │
│ ├─► yarn.lock 에 `__metadata:` marker 없음 ──► INVALID CONTEXT (fail-closed)
│ └─► 유효한 Berry lockfile
│ │
│ ▼
│ context_hash = sha256(project_root, sha256(resolutions), sha256(yarn.lock))
│ │
│ ▼
│ `yarn info -A -R --json` → 전체 project locator graph
│ │
│ ▼
│ 요청된 `pkg@npm:version` locator 부터 traverse
│ │
│ ├─► locator 발견 ──► resolved project closure (approve 가능)
│ └─► locator 없음 ──► approval_scope: "deny-only"
│ (published closure 는 여전히 취약점 검사하지만
│ 결과를 approve 하지는 않음)
└─► 이 Git worktree 에 resolutions/yarn.lock 없음 ──► 일반 npm package-only check
```

descriptor-to-locator resolution 은 Yarn 소유다. safedeps 는 lockfile resolution 을 재구현하지 않고 `yarn info` 의 machine-readable graph 를 그대로 소비한다. context 가 resolve 되면 approved-spec ledger entry 가 `project_context` (3장) 를 함께 가져, 승인이 그 프로젝트 하나로 한정되고 다른 프로젝트로 새거나 `resolutions`/`yarn.lock` 변경 이후에도 살아남지 못한다.

### Phase 2 — fast command guard (PreToolUse / `safedeps-pre-guard.sh`)

```
Expand All @@ -203,6 +236,8 @@ Claude: npm install @jackwener/opencli@^1.7.16

guard 는 lockfile/manifest 도 snapshot 하고 v1 hardcoded pattern 차단(section 5)도 유지한다. 빠르고 advisory 일 뿐, 권위는 post gate 다.

npm ecosystem 명령이면 guard 도 위와 같은 Yarn project context 를 해석해(`SAFEDEPS_NPM_PROJECT_DIR` 를 project directory 로 고정) 그 `context_hash` 를 ledger 조회에 접어 넣는다 — 그래서 project-scoped 승인은 그 프로젝트 안에서만 guard 를 통과한다. context 가 invalid 하면(resolutions 는 있는데 lockfile 을 못 씀) package-only 조회로 넘어가지 않고 명령을 그대로 거부한다.

### Phase 3 — npm primary effect gate + reorg (PostToolUse / `safedeps-post-verify.sh`)

```
Expand Down Expand Up @@ -319,7 +354,7 @@ GHSA / NVD — 응답 무
| maven | `pom.xml` | (디렉토리) | `safedeps check maven <group>:<artifact>@<range>` |
| nuget | `*.csproj` | `packages.lock.json` | `safedeps check nuget <pkg>@<range>` |

OSV 가 ecosystem 이름을 정규화해줘서 advisory-check 시점엔 single API 로 전부 cover 한다. ecosystem 별 typosquat 명단·install-script 위험 패턴은 별도 정적 list 다. npm effect gate(closure-vs-ledger enforcement)는 현재 npm 한정이고, 나머지는 command-gate + reorg 모델을 쓴다.
OSV 가 ecosystem 이름을 정규화해줘서 advisory-check 시점엔 single API 로 전부 cover 한다. ecosystem 별 typosquat 명단·install-script 위험 패턴은 별도 정적 list 다. npm effect gate(closure-vs-ledger enforcement)는 현재 npm 한정이고, 나머지는 command-gate + reorg 모델을 쓴다. npm 으로 라우팅되는 lockfile 중 project-scoped closure resolution(4장 Phase 1)을 받는 건 Yarn 하나뿐이며, 루트 `resolutions` entry 가 있을 때만이다. pnpm 과 일반 npm 은 항상 published-package probe 를 쓴다.

---

Expand All @@ -334,8 +369,8 @@ OSV 가 ecosystem 이름을 정규화해줘서 advisory-check 시점엔 single A
| `scripts/safedeps-pre-guard.sh` | PreToolUse hook — ledger 일치 + v1 hardcoded pattern + snapshot. |
| `scripts/safedeps-post-verify.sh` | PostToolUse hook — closure-vs-ledger effect gate + reorg. |
| `lib/providers/` | OSV / KEV / GHSA (옵션 NVD / deps.dev / Snyk) adapter, 단일 query interface. |
| `lib/ledger/` | approved-spec ledger I/O — atomic write, hashing, TTL 검사. |
| `lib/npm/closure.sh` | lockfile 에서 npm closure 해석. |
| `lib/ledger/` | approved-spec ledger I/O — atomic write, hashing, TTL 검사, project-context-scoped key. |
| `lib/npm/closure.sh` | lockfile 에서 npm closure 해석, 더해 Yarn project context/closure 해석 (루트 `resolutions` + `yarn info`). |
| `lib/gates/` | release-time repo lane — `scan.sh`(gitleaks runner), `audit.sh`(멀티-ecosystem lockfile audit — npm/pnpm/yarn/bun, 각 네이티브 도구에 위임), `hooks.sh`(`install`/`check`/`init`), `doctor.sh`(자세 진단 + `--fix`), `repo-profile.sh`(public/private 판별). *실행*을 소유하고 *policy* 는 repo 가 소유. |
| `lib/gates/templates/` | 시작용 `.gitleaks[.private].toml` + `.githooks/pre-commit`, `hooks init` 가 scaffold. repo 가 소유·튜닝하는 seed — 재실행 시 덮지 않음. |

Expand Down Expand Up @@ -399,6 +434,7 @@ rm -rf ~/.safedeps/cache/osv/ # OSV cache 비우기 (강제 re-query)
- registry 자체(npm/PyPI/…) 손상은 막지 못한다.
- KEV 는 하루 1회 update — 그 사이 등재된 KEV 는 다음 refresh 까지 못 잡는다.
- transitive closure 검사는 ledger 를 수백 개로 키울 수 있어 최적화가 필요하다.
- Yarn project-scoped closure 는 `PATH` 상의 Yarn CLI 와 Yarn Berry lockfile(`__metadata:` 존재)이 필요하다. Yarn Classic(`yarn.lock` v1)이나 루트 `resolutions` 가 없는 workspace 는 일반 npm package-only check 로 떨어진다.

**미래 방향** ([`ROADMAP.md`](./ROADMAP.md) 참고):

Expand Down
46 changes: 41 additions & 5 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,16 +136,20 @@ Design principle: **OSV is the one canonical truth.** Every other source is over
},
"transitive_specs": [
{ "ecosystem": "npm", "package": "…", "version": "…" }
]
],
"project_context": null
}
```

Key fields:

- `hash` — a deterministic hash of `(ecosystem, package, version)`. The hook derives the same hash from a command and looks the ledger up by it.
- `hash` — a deterministic hash of `(ecosystem, package, version)`, folded together with `project_context.context_hash` when one is present. The hook derives the same hash from a command (plus the live project context, if any) and looks the ledger up by it.
- `approved_at` / `expires_at` — lifecycle TTL, 30 days by default. After expiry a new CVE may exist, so the spec is auto-revoked and re-check is forced.
- `evidence` — which source saw what, at approval time. An audit trail.
- `transitive_specs` — the full transitive closure the direct entry approved. The npm effect gate reorgs any `pkg@version` that appears in the lockfile but is in neither the direct entry nor this array.
- `project_context` — `null` for an ordinary published-package approval, or `{ type: "yarn-project-lockfile", context_hash, project_root, manifest_path, lockfile_path }` when the approval came from a Yarn project's resolved closure (see "Yarn project-scoped closure" in section 4). `context_hash` is a hash of the project directory, its root `resolutions`, and its `yarn.lock` content.

**Project-scoped isolation.** A `project_context` approval is keyed by `context_hash` in addition to `(ecosystem, package, version)`, so it lives at a different ledger path than a package-only approval of the same spec and cannot satisfy a lookup from a different project or from the same project after `resolutions`/`yarn.lock` changes (the hash changes with them). `safedeps_ledger_check` compares the caller's live context hash against the stored one and denies with `reason: "context_mismatch"` on any difference — an approval never silently leaks across project boundaries.

Lifecycle:

Expand Down Expand Up @@ -189,6 +193,35 @@ safedeps check npm "@jackwener/opencli@^1.7.0"

For npm, "OSV query" runs over the **whole resolved closure** in one `/v1/querybatch` call, and the approved entry records every transitive package in `transitive_specs`.

**Yarn project-scoped closure.** Before falling back to a fresh published-package probe, `safedeps_npm_yarn_project_closure` (in `lib/npm/closure.sh`) looks for a canonical Yarn project context:

```
resolve project context (walk up from cwd, stop at .git boundary):
├─► package.json has non-empty root `resolutions` + a yarn.lock next to it
│ │
│ ├─► yarn.lock has no `__metadata:` marker ──► INVALID CONTEXT (fail-closed)
│ └─► valid Berry lockfile
│ │
│ ▼
│ context_hash = sha256(project_root, sha256(resolutions), sha256(yarn.lock))
│ │
│ ▼
│ `yarn info -A -R --json` → full project locator graph
│ │
│ ▼
│ traverse from the requested `pkg@npm:version` locator
│ │
│ ├─► locator found ──► resolved project closure (approvable)
│ └─► locator absent ──► approval_scope: "deny-only"
│ (published closure still checked for vulnerabilities,
│ but the result is never approved)
└─► no resolutions / no yarn.lock in this Git worktree ──► ordinary npm package-only check
```

Yarn owns descriptor-to-locator resolution; safedeps consumes `yarn info`'s machine-readable graph rather than re-implementing lockfile resolution. When the context resolves, the approved-spec ledger entry carries `project_context` (section 3) so the approval is scoped to that exact project and cannot leak to a different one or survive a `resolutions`/`yarn.lock` change.

### Phase 2 — fast command guard (PreToolUse / `safedeps-pre-guard.sh`)

```
Expand All @@ -204,6 +237,8 @@ Claude runs: npm install @jackwener/opencli@^1.7.16

The guard also snapshots lockfiles/manifests and keeps the v1 hardcoded pattern blocks (see section 5). It is fast and advisory; the authority is the post-install gate.

For an npm-ecosystem command, the guard resolves the same Yarn project context described above (`SAFEDEPS_NPM_PROJECT_DIR` pinned to the project directory) and folds its `context_hash` into the ledger lookup, so a project-scoped approval only passes the guard inside its own project. An invalid context (resolutions present, lockfile unusable) denies the command outright rather than falling back to a package-only lookup.

### Phase 3 — npm primary effect gate + reorg (PostToolUse / `safedeps-post-verify.sh`)

```
Expand Down Expand Up @@ -320,7 +355,7 @@ Design principle: **no silent fallback.** When the canonical truth (OSV) cannot
| maven | `pom.xml` | (directory) | `safedeps check maven <group>:<artifact>@<range>` |
| nuget | `*.csproj` | `packages.lock.json` | `safedeps check nuget <pkg>@<range>` |

OSV normalizes ecosystem names, so one API path covers all of them at advisory-check time. Per-ecosystem typosquat lists and install-script risk patterns live in separate static lists. Note that the npm effect gate (closure-vs-ledger enforcement) is npm-only today; the other ecosystems use the command-gate + reorg model.
OSV normalizes ecosystem names, so one API path covers all of them at advisory-check time. Per-ecosystem typosquat lists and install-script risk patterns live in separate static lists. Note that the npm effect gate (closure-vs-ledger enforcement) is npm-only today; the other ecosystems use the command-gate + reorg model. Yarn is the one npm-routed lockfile that gets project-scoped closure resolution (section 4, Phase 1) when a root `resolutions` entry is present; pnpm and plain npm always use the published-package probe.

---

Expand All @@ -335,8 +370,8 @@ OSV normalizes ecosystem names, so one API path covers all of them at advisory-c
| `scripts/safedeps-pre-guard.sh` | PreToolUse hook — ledger match + v1 hardcoded patterns + snapshots. |
| `scripts/safedeps-post-verify.sh` | PostToolUse hook — closure-vs-ledger effect gate + reorg. |
| `lib/providers/` | OSV / KEV / GHSA (and optional NVD / deps.dev / Snyk) adapters behind one query interface. |
| `lib/ledger/` | Approved-spec ledger I/O — atomic write, hashing, TTL checks. |
| `lib/npm/closure.sh` | npm closure resolution from a lockfile. |
| `lib/ledger/` | Approved-spec ledger I/O — atomic write, hashing, TTL checks, project-context-scoped keys. |
| `lib/npm/closure.sh` | npm closure resolution from a lockfile, plus Yarn project context/closure resolution (root `resolutions` + `yarn info`). |
| `lib/gates/` | Release-time repo lane — `scan.sh` (gitleaks runner), `audit.sh` (multi-ecosystem lockfile audit — npm/pnpm/yarn/bun, delegated to each native tool), `hooks.sh` (`install`/`check`/`init`), `doctor.sh` (posture diagnose + `--fix`), `repo-profile.sh` (public/private resolution). Owns *execution*; the repo owns *policy*. |
| `lib/gates/templates/` | Starter `.gitleaks[.private].toml` + `.githooks/pre-commit`, scaffolded by `hooks init`. Seeds the repo owns and tunes — never overwritten on re-run. |

Expand Down Expand Up @@ -400,6 +435,7 @@ Migration:
- A compromise of the registry itself (npm/PyPI/…) is out of reach.
- KEV updates once a day; a KEV listed in between is not caught until the next refresh.
- Transitive-closure checking can grow the ledger to hundreds of entries; this needs optimization.
- Yarn project-scoped closure requires the Yarn CLI on `PATH` and a Yarn Berry lockfile (`__metadata:` present); Yarn Classic (`yarn.lock` v1) and workspaces without a root `resolutions` entry fall back to the ordinary npm package-only check.

**Future direction** (see [`ROADMAP.md`](./ROADMAP.md)):

Expand Down
2 changes: 2 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ safedeps check <ecosystem> <pkg>@<version|range> --json

이 명령은 OSV(표준), CISA KEV(고위험 오버레이), GitHub Advisory(강화 정보)를 조회합니다. npm의 경우 먼저 스크립트가 없는 임시 lockfile을 `npm install --package-lock-only --ignore-scripts`로 생성한 뒤 전체 의존성 폐쇄성을 추출하고 OSV `/v1/querybatch`를 쿼리합니다. 정상이거나 안전하게 축소된 spec은 `~/.safedeps/approved-specs/`에 기록되고, npm 항목은 `transitive_specs`도 함께 저장합니다.

**Yarn project-scoped closure.** 대상 디렉터리가 루트 `resolutions`를 가진 Yarn Berry 프로젝트라면, `check`는 새로운 published-package probe 대신 그 프로젝트의 실제 `yarn.lock`을 `yarn info`로 읽어 폐쇄성을 계산합니다. 덕분에 `resolutions`로 취약한 transitive dependency를 patched version으로 고정한 프로젝트는 실제 resolved dependency tree 기준으로 승인받을 수 있습니다 -- published package closure만 봤다면 여전히 취약한 version이 보여 install이 거부됐을 것입니다. 승인 범위는 그 프로젝트 하나로 한정됩니다. ledger key가 project directory, `resolutions`, `yarn.lock` content의 hash를 포함하므로, 다른 프로젝트나 `resolutions`/`yarn.lock`이 바뀐 이후에는 같은 승인을 재사용할 수 없습니다. `resolutions`가 선언돼 있는데도 요청한 package를 프로젝트의 resolved graph에서 검증할 수 없거나 lockfile이 지원되는 Yarn Berry lockfile이 아니면, check는 fail-closed 상태를 유지합니다.

### Phase 2: Fast Command Guard + Snapshots (PreToolUse)

Claude Code 또는 Codex CLI가 `npm install`, `pip install`, `cargo add`, `go get`, `gem install` 같은 명령을 실행하려 할 때, 가드 훅이 빠른 advisory/UX 레이어를 제공합니다.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ safedeps check <ecosystem> <pkg>@<version|range> --json

That command queries OSV (canonical), CISA KEV (hard-risk overlay), and GitHub Advisory (enrichment). For npm, it first creates a script-free temp lockfile with `npm install --package-lock-only --ignore-scripts`, extracts the full dependency closure, and queries OSV `/v1/querybatch`. Clean or safely narrowed specs are written to `~/.safedeps/approved-specs/`; npm entries also record `transitive_specs`.

**Yarn project-scoped closure.** When the target directory is a Yarn Berry project with a root `resolutions` entry, `check` resolves the closure from that project's actual `yarn.lock` via `yarn info`, instead of a fresh published-package probe. This lets a project that pins a vulnerable transitive dependency to a patched version through `resolutions` get approved on its real, resolved dependency tree -- the published package closure alone would still show the vulnerable version and deny the install. The approval only covers that exact project: the ledger key folds in a hash of the project directory, `resolutions`, and `yarn.lock` content, so it cannot satisfy the check for a different project or after `resolutions`/`yarn.lock` changes. If `resolutions` is declared but the requested package can't be verified in the project's resolved graph, or the lockfile isn't a supported Yarn Berry lockfile, the check stays fail-closed.

### Phase 2: Fast Command Guard + Snapshots (PreToolUse)

When Claude Code or Codex CLI is about to run `npm install`, `pip install`, `cargo add`, `go get`, `gem install`, or similar commands, the guard hook provides a fast advisory/UX layer:
Expand Down
Loading