@aldegad/safedeps 2.4.1 → 2.5.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 CHANGED
@@ -43,7 +43,7 @@ safedeps owns security gates at two distinct moments, under one skill. It absorb
43
43
 
44
44
  The two lanes differ in timing and scope (one package's effect before/after install vs. the whole repo before a release). They live under one umbrella but stay separated by command namespace.
45
45
 
46
- **The secret-leak side of the release-time lane is per-repo and opt-in.** Its detection policy lives in the target repo, not in safedeps, so it does nothing until the repo provides a `.gitleaks` config and an active `.githooks/pre-commit`. `safedeps doctor` is the repo-entry diagnostic that closes that gap: it reports each piece of the secret-leak lane (`.gitleaks` policy, `pre-commit`, `core.hooksPath`, scanner availability) plus the global install-time gate, and exits non-zero when the per-repo lane has gaps. `safedeps doctor --fix` (= `safedeps hooks init` then `safedeps hooks install`) scaffolds a starter policy from `lib/gates/templates/` and activates the hooks. The scaffold is **non-destructive** — an existing repo-owned config is never overwritten — preserving the invariant that safedeps owns *execution*, not *policy*. The scaffolded `pre-commit` delegates to `safedeps scan secrets --staged` (a single canonical scanner path) and is fail-closed: an unresolvable `safedeps` or missing scanner blocks the commit rather than skipping silently.
46
+ **The secret-leak side of the release-time lane is per-repo and opt-in.** Its detection policy lives in the target repo, not in safedeps, so it does nothing until the repo provides a `.gitleaks` config and an active `.githooks/pre-commit`. `safedeps doctor` is the repo-entry diagnostic that closes that gap: it reports each piece of the secret-leak lane (`.gitleaks` policy, `pre-commit`, `core.hooksPath`, scanner availability) plus the global install-time gate, and exits non-zero when the per-repo lane has gaps. `safedeps doctor --fix` (= `safedeps hooks init` then `safedeps hooks install`) scaffolds a starter policy from `lib/gates/templates/` and activates the hooks. The scaffold is **non-destructive** — an existing repo-owned config is never overwritten — preserving the invariant that safedeps owns *execution*, not *policy*. The scaffolded `pre-commit` runs two checks. The secret scan (`safedeps scan secrets --staged`) runs on every commit and is fail-closed: an unresolvable `safedeps` or a missing scanner blocks rather than skipping silently. The npm dependency audit (`safedeps audit npm`) also runs on every commit in a repo with an npm lockfile — not only when the lockfile changes — so a CVE disclosed *after* a package was installed is caught at the next commit by re-querying the advisory DB. The audit separates the security verdict from an availability failure via meaningful exit codes (0 clean / 1 vulnerable / 2 could-not-run): a real finding **blocks** (fail-closed), while an unreachable advisory DB makes the hook **warn and allow the commit** — an explicit, observable availability failover (per the no-silent-fallback invariant: it is logged to the commit output and does not change canonical truth), with CI and the daily re-check re-covering what the offline commit could not verify.
47
47
 
48
48
  **The effect-primary model is npm-only.** `pip`, `cargo`, `go`, `gem`, `maven`, and `nuget` stay on the v2.1 command-gate + reorg model until their closure resolvers land; they are not described as having PostToolUse closure authority.
49
49
 
package/README.ko.md CHANGED
@@ -16,6 +16,19 @@
16
16
 
17
17
  ---
18
18
 
19
+ ## 배포 모델
20
+
21
+ safedeps 에는 두 배포 surface 가 있다:
22
+
23
+ 1. **Agent skill + hooks (canonical)** -- repo 자체가 skill folder 다. `SKILL.md`, hook script, provider/ledger library, install helper 가 한 디렉터리에 함께 있다.
24
+ 2. **npm package (CLI convenience)** -- `@aldegad/safedeps` 는 `safedeps` command 를 설치한다. npm 설치만으로 Claude Code 나 Codex 가 skill 을 자동 discover 하지는 않는다. npm 설치 후에도 hook/skill installer 를 실행하거나 skill folder 를 수동 등록해야 한다.
25
+
26
+ 전체 skill/hook source tree 를 canonical artifact 로 원하면 GitHub release 를 쓴다. versioned global CLI 가 주목적이면 npm 을 쓴다.
27
+
28
+ 용어: safedeps 는 Claude/Codex hook 과 local CLI 로 동작하는 agent security skill 이다. plugin manifest 로 감싸기 전까지는 Codex plugin bundle 이 아니다.
29
+
30
+ ---
31
+
19
32
  ## 두 lane
20
33
 
21
34
  `safedeps` 는 두 보안 lane 을 소유한다 (전체 설계: [`ARCHITECTURE.md`](./ARCHITECTURE.md) §1):
@@ -23,7 +36,7 @@
23
36
  - **install-time** (이 README 의 초점) — advisory check + approved-spec ledger + 빠른 PreToolUse guard + PostToolUse effect enforcement + post-install reorg. 패키지 단위, install 명령과 실제 lockfile effect 주변.
24
37
  - **release-time** — `safedeps gates run`, `safedeps scan secrets [--repo|--worktree|--staged]`, `safedeps audit npm`, `safedeps hooks install|check`. repo 트리 secret scan, 의존성 audit, repo-local git hook 설치/검사 (push/release 전). repo-specific policy(gitleaks config, privacy 경로)는 대상 repo 에 남고 safedeps 는 실행 owner. *(옛 `security-release-gates` 흡수.)*
25
38
 
26
- release-time lane 의 secret 누출 쪽은 **repo 별이고 opt-in** 이다. `safedeps doctor` 가 그 repo-entry 점검이다 — repo 의 `.gitleaks` policy, `.githooks/pre-commit`, 활성 `core.hooksPath`, scanner 가용성을 진단하고(전역 install-time gate 상태도 같이 보고), `safedeps doctor --fix` 가 시작 policy 를 scaffold(`safedeps hooks init`)하고 활성화(`safedeps hooks install`)한다. scaffold 는 비파괴적이라 repo 가 소유한 기존 `.gitleaks.toml` 은 덮어쓰지 않으며, pre-commit hook 은 `safedeps scan secrets --staged` 위임하고 fail-closed 다. [secret 누출 lane (repo 별)](#secret-누출-lane-repo-별) 참고.
39
+ release-time lane 의 secret 누출 쪽은 **repo 별이고 opt-in** 이다. `safedeps doctor` 가 그 repo-entry 점검이다 — repo 의 `.gitleaks` policy, `.githooks/pre-commit`, 활성 `core.hooksPath`, scanner 가용성을 진단하고(전역 install-time gate 상태도 같이 보고), `safedeps doctor --fix` 가 시작 policy 를 scaffold(`safedeps hooks init`)하고 활성화(`safedeps hooks install`)한다. scaffold 는 비파괴적이라 repo 가 소유한 기존 `.gitleaks.toml` 은 덮어쓰지 않으며, pre-commit hook 은 매 커밋 비밀키 스캔(`safedeps scan secrets --staged`)을 돌리고 npm repo 면 매 커밋 의존성 audit(`safedeps audit npm`)도 돌린다 — 실제 취약점은 차단(fail-closed)하고, 어드바이저리 DB 도달 불가 시에는 경고만 하고 커밋을 통과시킨다(관측 가능한 오프라인 failover). [secret 누출 lane (repo 별)](#secret-누출-lane-repo-별) 참고.
27
40
 
28
41
  ---
29
42
 
@@ -106,7 +119,11 @@ lane 구성 요소:
106
119
 
107
120
  - **`safedeps hooks init`** 가 시작용 `.gitleaks.toml`(private repo 면 `.gitleaks.private.toml`)과 `.githooks/pre-commit` 을 scaffold 한다. 기존 파일은 덮지 않고 유지 — policy 는 repo 가 소유한다.
108
121
  - **`safedeps hooks install`** 이 repo-local hook 을 활성화한다(`core.hooksPath = .githooks`).
109
- - **pre-commit hook 은 `safedeps scan secrets --staged` 에 위임**하고 **fail-closed** 다: scanner(로컬 `gitleaks` 또는 Docker)가 못 돌면 silent skip 이 아니라 커밋을 막는다. 의도된 우회는 사람이 소유하는 `git commit --no-verify` 뿐이다.
122
+ - **pre-commit hook 은 검사를 돌린다**:
123
+ - **비밀키 스캔**(`safedeps scan secrets --staged`)을 매 커밋, **fail-closed**. scanner(로컬 `gitleaks` 또는 Docker)가 못 돌면 silent skip 이 아니라 커밋을 막는다.
124
+ - **npm 의존성 audit**(`safedeps audit npm`)을 npm lockfile 이 있는 repo 면 **매 커밋**. 취약한 직접·*transitive* 의존성을 잡는다 — 패키지를 깐 *뒤에* 공개된 CVE("그땐 안전해 보였는데 지금 발견됨")까지 포함해서, 사람이 손으로 절대 못 보는 그것. lockfile 이 바뀔 때만이 아니라 매 커밋 돌리는 게 핵심이다: 어드바이저리 DB 를 다시 조회하니까 이미 깔린 의존성에 *새로* 뜬 CVE 가 바로 다음 커밋에 드러난다. 보안 판정과 가용성 실패는 구분된다 — 실제 취약점은 **차단**(fail-closed)하지만, 어드바이저리 DB 가 **도달 불가**(오프라인/레지스트리 오류)면 **경고만 하고 커밋을 통과**시킨다(관측 가능한 가용성 failover, silent skip 아님). 오프라인 커밋이 못 본 건 CI 와 데일리 re-check 가 다시 메운다.
125
+
126
+ 의도된 우회는 사람이 소유하는 `git commit --no-verify` 뿐이다.
110
127
 
111
128
  scaffold 된 `.gitleaks.toml` 은 **네가 손보는 시작점**이다: gitleaks 기본 ruleset 을 extend 하고, 값이 할당된 `.env` 커밋을 잡는 rule 을 더하며(`.env.example`/`.sample`/`.template` 변형은 allowlist), fixture 용 repo-owned `[allowlist]` 블록을 남겨둔다. safedeps 는 *실행* — `safedeps scan secrets` 로 gitleaks 구동 — 만 소유하고 policy 내용은 소유하지 않는다.
112
129
 
package/README.md CHANGED
@@ -19,6 +19,8 @@ Safedeps has two distribution surfaces:
19
19
 
20
20
  Use the GitHub release when you want the full skill/hook source tree as the canonical artifact. Use npm when you mainly want a versioned global CLI.
21
21
 
22
+ Terminology: safedeps is an agent security skill backed by Claude/Codex hooks and a local CLI. It is not a Codex plugin bundle unless it is later wrapped with a plugin manifest.
23
+
22
24
  ## Two Lanes
23
25
 
24
26
  `safedeps` owns two security lanes (full design in [`ARCHITECTURE.md`](./ARCHITECTURE.md) §1):
@@ -26,7 +28,7 @@ Use the GitHub release when you want the full skill/hook source tree as the cano
26
28
  - **Install-time** (the focus of this README) — advisory check + approved-spec ledger + fast PreToolUse guard + PostToolUse effect enforcement + post-install reorg. Per-package, around the install command and its actual lockfile effect.
27
29
  - **Release-time** — `safedeps gates run`, `safedeps scan secrets [--repo|--worktree|--staged]`, `safedeps audit npm`, `safedeps hooks install|check`. Repo-tree secret scan, dependency audit, and repo-local git hook install/check before push/release. Repo-specific policy (gitleaks config, privacy paths) stays in the target repo; safedeps owns execution. *(Absorbed the former `security-release-gates`.)*
28
30
 
29
- The secret-leak side of the release-time lane is **per-repo and opt-in**. `safedeps doctor` is its repo-entry check: it diagnoses the repo's `.gitleaks` policy, `.githooks/pre-commit`, the active `core.hooksPath`, and scanner availability (and reports the global install-time gate too), then `safedeps doctor --fix` scaffolds a starter policy (`safedeps hooks init`) and activates it (`safedeps hooks install`). The scaffold is non-destructive — an existing repo-owned `.gitleaks.toml` is never overwritten — and the pre-commit hook delegates to `safedeps scan secrets --staged`, fail-closed. See [Secret-Leak Lane (per-repo)](#secret-leak-lane-per-repo).
31
+ The secret-leak side of the release-time lane is **per-repo and opt-in**. `safedeps doctor` is its repo-entry check: it diagnoses the repo's `.gitleaks` policy, `.githooks/pre-commit`, the active `core.hooksPath`, and scanner availability (and reports the global install-time gate too), then `safedeps doctor --fix` scaffolds a starter policy (`safedeps hooks init`) and activates it (`safedeps hooks install`). The scaffold is non-destructive — an existing repo-owned `.gitleaks.toml` is never overwritten — and the pre-commit hook runs a secret scan (`safedeps scan secrets --staged`) plus, on every commit in an npm repo, a dependency audit (`safedeps audit npm`): a real finding blocks (fail-closed), while an unreachable advisory DB only warns and lets the commit through (observable offline failover). See [Secret-Leak Lane (per-repo)](#secret-leak-lane-per-repo).
30
32
 
31
33
  ## How It Works
32
34
 
@@ -175,7 +177,11 @@ What the lane is made of:
175
177
 
176
178
  - **`safedeps hooks init`** scaffolds a starter `.gitleaks.toml` (or `.gitleaks.private.toml` for a private repo) and a `.githooks/pre-commit`. Existing files are kept, never overwritten — the repo owns the policy.
177
179
  - **`safedeps hooks install`** activates the repo-local hooks (`core.hooksPath = .githooks`).
178
- - The **pre-commit hook delegates to `safedeps scan secrets --staged`** and is **fail-closed**: if the scanner (local `gitleaks` or Docker) cannot run, it blocks the commit instead of skipping silently. The only intentional bypass is `git commit --no-verify`, which the human owns.
180
+ - The **pre-commit hook runs two checks**:
181
+ - **Secret scan** (`safedeps scan secrets --staged`) on every commit, **fail-closed**. If the scanner (local `gitleaks` or Docker) cannot run, it blocks the commit instead of skipping silently.
182
+ - **npm dependency audit** (`safedeps audit npm`) on **every commit** in a repo that has an npm lockfile. This catches a vulnerable direct *or transitive* dependency — including a CVE that was published *after* you installed the package ("looked safe then, flagged now"), the kind of thing a human never reviews by hand. Running it every commit (not only when the lockfile changes) is the point: it re-queries the advisory DB so a newly-disclosed CVE on an already-installed dependency surfaces at the very next commit. The verdict and an availability failure are kept apart: a real finding **blocks** (fail-closed), but if the advisory DB is **unreachable** (offline / registry error) the hook **warns and lets the commit through** — an observable availability failover, never a silent skip. (CI and the daily re-check then re-cover what the offline commit could not verify.)
183
+
184
+ The only intentional bypass is `git commit --no-verify`, which the human owns.
179
185
 
180
186
  The scaffolded `.gitleaks.toml` is a **starter you tune**: it extends gitleaks' default ruleset, adds a rule for a committed `.env` with an assigned secret (the `.env.example`/`.sample`/`.template` variants are allowlisted), and leaves a repo-owned `[allowlist]` block for your fixtures. safedeps owns *execution* — running gitleaks via `safedeps scan secrets` — not the policy content.
181
187
 
package/ROADMAP.md CHANGED
@@ -56,7 +56,7 @@ The internal engine keeps the v1 `reorg-guard` assets.
56
56
 
57
57
  ### Release notes
58
58
 
59
- - The npm package version in `package.json` is the single source of truth. `bin/safedeps` `SAFEDEPS_VERSION` tracks it and the smoke test reads `package.json` to compare (current: v2.4.1).
59
+ - The npm package version in `package.json` is the single source of truth. `bin/safedeps` `SAFEDEPS_VERSION` tracks it and the smoke test reads `package.json` to compare (current: v2.5.0).
60
60
  - `npm test` runs the release smoke suite; the full fixture E2E lives under `v2.1-tests`.
61
61
  - The daily re-check uses no LLM tokens. It is opt-in: a macOS `launchd` user agent runs `safedeps re-check --json` daily, installed atomically by `install-safedeps-recheck-agent.mjs`. It writes `~/.safedeps/recheck.log` and `~/.safedeps/recheck-alerts.jsonl` and raises a macOS notification on a new CVE/KEV/revoke/provider-skip. Network is used only for OSV / CISA / GHSA queries.
62
62
 
@@ -140,6 +140,24 @@ The pending state PreToolUse hands to PostToolUse was a single global `current_s
140
140
 
141
141
  ---
142
142
 
143
+ ## v2.5 — pre-commit dependency audit (shipped)
144
+
145
+ Status: shipped as v2.5.0.
146
+
147
+ ### What changed
148
+
149
+ - **Pre-commit dependency audit** — the scaffolded `.githooks/pre-commit` now runs `safedeps audit npm` on **every commit** in a repo with an npm lockfile, alongside the secret scan. It catches a vulnerable direct or *transitive* dependency — including a CVE disclosed *after* the package was installed ("looked safe then, flagged now") — at the next commit, by re-querying the advisory DB instead of waiting for the daily re-check. Real usage drove it: a transitive `hono` advisory that Dependabot missed was caught exactly this way.
150
+ - **Meaningful `audit npm` exit codes** — `0` clean / `1` vulnerable / `2` could-not-run (no lockfile, npm/jq missing, advisory DB unreachable). This separates the **security verdict** from an **availability failure**; npm audit collapses both into exit 1 on its own.
151
+ - **Observable offline failover** — when the advisory DB is unreachable the hook **warns and allows** the commit (exit 2) rather than fail-closing, so a network outage never blocks an offline commit; a real finding (exit 1) still **blocks**. Per the no-silent-fallback invariant the failover is loud (printed to the commit output), and CI / the daily re-check re-cover what the offline commit could not verify.
152
+
153
+ ### Verification
154
+
155
+ - `audit npm` exit-code contract (clean=0 / vulnerable=1 / unreachable=2), deterministic via a fake npm
156
+ - pre-commit blocks a commit carrying a vulnerable dependency; warns + allows when the advisory DB is unreachable
157
+ - existing secret-lane + smoke + e2e regression suite remains green
158
+
159
+ ---
160
+
143
161
  ## v3 (future)
144
162
 
145
163
  ### Ledger tamper resistance
package/SKILL.md CHANGED
@@ -10,7 +10,9 @@ hooks:
10
10
 
11
11
  # Safedeps
12
12
 
13
- Two gates, one skill. You (the agent) are the primary user drive both:
13
+ Two gates, one skill. Safedeps is an agent security skill backed by Claude/Codex hooks and a local CLI. It is not a Codex plugin bundle unless it is later wrapped with a plugin manifest.
14
+
15
+ You (the agent) are the primary user — drive both:
14
16
 
15
17
  - **Install-time gate** — clear every dependency install through an OSV-backed advisory check before it runs.
16
18
  - **Secret-leak gate** — stop a secret or a real `.env` from being committed (per-repo).
@@ -71,7 +73,7 @@ safedeps doctor --fix # scaffold the policy + activate the hooks (non-destruc
71
73
  2. Gaps? Run `safedeps doctor --fix`. It scaffolds `.gitleaks.toml` (or `.gitleaks.private.toml`) and `.githooks/pre-commit`, then activates them. Existing repo files are never overwritten.
72
74
  3. Tune the scaffolded `.gitleaks.toml` for the repo — allowlist fixtures, add rules. You own the policy; safedeps runs it (gitleaks via `safedeps scan secrets`).
73
75
 
74
- The pre-commit hook delegates to `safedeps scan secrets --staged` and is **fail-closed**: no scanner it blocks the commit. The only bypass is the human's `git commit --no-verify`.
76
+ The pre-commit hook runs two checks: a secret scan (`safedeps scan secrets --staged`) on every commit (fail-closed), and an npm dependency audit (`safedeps audit npm`) on every commit in an npm repo — so a CVE published *after* you installed a package is caught at the next commit, not weeks later. `audit npm` exits 0 clean / 1 vulnerable / 2 could-not-run; the hook **blocks** on a real finding (1) but **warns and allows** when the advisory DB is unreachable (2 — observable offline failover). No secret scanner → blocks. The only bypass is the human's `git commit --no-verify`.
75
77
 
76
78
  ---
77
79
 
package/bin/safedeps CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  set -euo pipefail
9
9
 
10
- SAFEDEPS_VERSION="2.4.1"
10
+ SAFEDEPS_VERSION="2.5.0"
11
11
 
12
12
  # ---- repo / lib bootstrap ----------------------------------------------------
13
13
 
@@ -3,7 +3,17 @@ set -euo pipefail
3
3
 
4
4
  # safedeps audit npm — generic npm lockfile audit.
5
5
  # Absorbed from kuma-studio scripts/security/run-npm-audit.sh.
6
- # Missing lockfile stays fail-closed (no reproducible verdict without it).
6
+ #
7
+ # Exit codes are meaningful so a caller (e.g. the pre-commit hook) can tell a
8
+ # security verdict apart from an availability problem — npm audit collapses both
9
+ # into exit 1 on its own:
10
+ # 0 clean — no advisory at or above the audit level
11
+ # 1 vulnerable — at least one advisory at or above the level (BLOCK)
12
+ # 2 could not produce a verdict — no lockfile, npm/jq missing, or the npm
13
+ # advisory database is unreachable (offline/registry error). This is an
14
+ # AVAILABILITY failure, not a clean bill of health; the caller decides
15
+ # whether to fail-closed or warn-and-continue.
16
+ # 64 usage error
7
17
 
8
18
  REPO_ROOT=""
9
19
  AUDIT_LEVEL="${SAFEDEPS_NPM_AUDIT_LEVEL:-${KUMA_NPM_AUDIT_LEVEL:-moderate}}"
@@ -26,11 +36,53 @@ if [ -z "$REPO_ROOT" ]; then REPO_ROOT="$(pwd)"; fi
26
36
  REPO_ROOT="$(cd "$REPO_ROOT" && pwd)"
27
37
  cd "$REPO_ROOT"
28
38
 
29
- if [ ! -f package-lock.json ]; then
30
- cat >&2 <<'EOF'
31
- ERROR: package-lock.json is missing, so npm audit cannot produce a reproducible dependency verdict.
32
- EOF
33
- exit 1
39
+ if [ ! -f package-lock.json ] && [ ! -f npm-shrinkwrap.json ]; then
40
+ printf 'safedeps audit: no package-lock.json/npm-shrinkwrap.json — cannot produce a reproducible verdict.\n' >&2
41
+ exit 2
42
+ fi
43
+
44
+ if ! command -v npm >/dev/null 2>&1; then
45
+ printf 'safedeps audit: npm not found — cannot produce a dependency verdict.\n' >&2
46
+ exit 2
34
47
  fi
35
48
 
36
- exec npm audit --audit-level="$AUDIT_LEVEL"
49
+ # Without jq we cannot reliably separate an offline failure from a real finding,
50
+ # so degrade to a plain, fail-closed audit: any non-zero exit blocks (safe).
51
+ if ! command -v jq >/dev/null 2>&1; then
52
+ npm audit --audit-level="$AUDIT_LEVEL"
53
+ exit $?
54
+ fi
55
+
56
+ # One JSON run. npm audit shares exit 1 between "vulnerable" and "could not run",
57
+ # so we read the verdict from the payload, not the exit code.
58
+ audit_json="$(npm audit --json 2>/dev/null || true)"
59
+
60
+ # Unreachable advisory DB / setup error → npm emits a top-level {"error":...},
61
+ # or no/invalid JSON. Availability failure, not a verdict.
62
+ if [ -z "$audit_json" ] \
63
+ || ! printf '%s' "$audit_json" | jq -e . >/dev/null 2>&1 \
64
+ || printf '%s' "$audit_json" | jq -e 'has("error")' >/dev/null 2>&1; then
65
+ printf 'safedeps audit: could not reach the npm advisory database (offline or registry error).\n' >&2
66
+ exit 2
67
+ fi
68
+
69
+ # Count advisories at or above the configured level.
70
+ n_at_level="$(printf '%s' "$audit_json" | jq --arg lvl "$AUDIT_LEVEL" '
71
+ (.metadata.vulnerabilities // {}) as $v
72
+ | ["low","moderate","high","critical"] as $order
73
+ | (($order | index($lvl)) // 1) as $min
74
+ | reduce $order[$min:][] as $s (0; . + ($v[$s] // 0))
75
+ ')"
76
+
77
+ if [ "${n_at_level:-0}" -gt 0 ]; then
78
+ printf '%s' "$audit_json" | jq -r '
79
+ (.metadata.vulnerabilities // {}) as $v
80
+ | " severities: " +
81
+ ([ "critical","high","moderate","low" ]
82
+ | map(select(($v[.] // 0) > 0) | "\($v[.]) \(.)") | join(", "))
83
+ ' >&2
84
+ printf '%s' "$audit_json" | jq -r '(.vulnerabilities // {}) | keys[] | " - " + .' 2>/dev/null | head -20 >&2 || true
85
+ printf 'safedeps audit: %s advisory(ies) at or above "%s" — blocking. Run `npm audit` for the full report.\n' "$n_at_level" "$AUDIT_LEVEL" >&2
86
+ exit 1
87
+ fi
88
+ exit 0
@@ -1,11 +1,17 @@
1
1
  #!/bin/bash
2
- # .githooks/pre-commit — safedeps secret-leak gate (scaffolded by `safedeps hooks init`).
2
+ # .githooks/pre-commit — safedeps repo gate (scaffolded by `safedeps hooks init`).
3
3
  #
4
- # Blocks a commit when gitleaks finds a secret in the STAGED changes, so a key
5
- # or a real .env never enters the repo's history. The detection POLICY lives in
6
- # this repo's .gitleaks.toml (public) / .gitleaks.private.toml (private);
7
- # safedeps owns execution. This hook delegates to one canonical scanner path:
8
- # `safedeps scan secrets --staged`.
4
+ # Two checks:
5
+ # 1. Secret scan (always, fail-closed) blocks a commit when gitleaks finds a
6
+ # secret in the STAGED changes, so a key or a real .env never enters
7
+ # history. The detection POLICY lives in this repo's .gitleaks.toml
8
+ # (public) / .gitleaks.private.toml (private); safedeps owns execution.
9
+ # 2. Dependency audit (npm) — on every commit when this repo has an npm
10
+ # lockfile. Catches a vulnerable direct or *transitive* dependency,
11
+ # including a CVE published AFTER you installed it ("looked safe then,
12
+ # flagged now"). A real finding blocks (fail-closed); only the advisory DB
13
+ # being unreachable (offline) warns and lets the commit through — an
14
+ # observable availability failover, never a silent skip.
9
15
  #
10
16
  # Intentional bypass (use sparingly — you own the risk): git commit --no-verify
11
17
  set -euo pipefail
@@ -43,7 +49,30 @@ EOF
43
49
  exit 1
44
50
  fi
45
51
 
46
- # Delegate to the single canonical scanner. A non-zero exit means either a
47
- # secret was found OR no scanner (gitleaks/docker) is available — both
48
- # fail-closed and block the commit.
49
- exec "${sd}" scan secrets --staged --root "${repo_root}"
52
+ # 1. Secret scan (always). A non-zero exit means either a secret was found OR
53
+ # no scanner (gitleaks/docker) is available — both fail-closed, block commit.
54
+ "${sd}" scan secrets --staged --root "${repo_root}"
55
+
56
+ # 2. Dependency audit (npm) — every commit when this repo has an npm lockfile.
57
+ # `safedeps audit npm` exit codes: 0 clean / 1 vulnerable / 2 could-not-run.
58
+ # - vulnerable (1) → BLOCK (fail-closed on the security verdict).
59
+ # - unreachable (2) → WARN and ALLOW (availability failover, observable):
60
+ # an offline commit is never silently dropped, and CI / the daily
61
+ # re-check still cover what this commit could not verify.
62
+ if [ -f "${repo_root}/package-lock.json" ] || [ -f "${repo_root}/npm-shrinkwrap.json" ]; then
63
+ if "${sd}" audit npm --root "${repo_root}"; then
64
+ : # clean — no advisory at or above the audit level.
65
+ else
66
+ audit_rc=$?
67
+ if [ "${audit_rc}" -eq 2 ]; then
68
+ {
69
+ printf '\n'
70
+ printf 'safedeps pre-commit: dependency audit could not reach the advisory DB.\n'
71
+ printf ' -> commit ALLOWED without a fresh dependency verdict (offline failover).\n'
72
+ printf ' -> re-run `safedeps audit npm` once online; CI / the daily re-check also cover this.\n'
73
+ } >&2
74
+ else
75
+ exit "${audit_rc}" # vulnerable (or unknown failure) -> fail-closed.
76
+ fi
77
+ fi
78
+ fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aldegad/safedeps",
3
- "version": "2.4.1",
3
+ "version": "2.5.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": {
@@ -219,6 +219,51 @@ function unlinkBin() {
219
219
  removeSymlink(join(HOME, ".local", "bin", "safedeps"));
220
220
  }
221
221
 
222
+ function safedepsOnPath() {
223
+ const dirs = (process.env.PATH || "").split(":").filter(Boolean);
224
+ return dirs.some((d) => {
225
+ try { return existsSync(join(d, "safedeps")); } catch { return false; }
226
+ });
227
+ }
228
+
229
+ // The dependency-install gate is global and now active. The rest of the
230
+ // surface (secret-leak lane, dep audit) is per-repo and opt-in — its policy
231
+ // lives in each repo. Nudge with a recommended setup; never auto-write.
232
+ function printRecommendedSetup() {
233
+ const line = "─".repeat(58);
234
+ const out = [];
235
+ out.push("");
236
+ out.push("Recommended setup");
237
+ out.push(line);
238
+ out.push(" 1. Dependency-install gate ........ ✓ active now (global, all repos)");
239
+ out.push(" Every agent install is checked; for npm the installed");
240
+ out.push(" closure is reorged (rolled back) if it diverges.");
241
+ out.push("");
242
+ out.push(" The rest is per-repo and opt-in. Run these INSIDE a repo:");
243
+ out.push("");
244
+ out.push(" 2. Pre-commit gate ............... recommended");
245
+ out.push(" safedeps doctor # diagnose this repo (read-only)");
246
+ out.push(" safedeps doctor --fix # scaffold .gitleaks.toml + pre-commit, then activate");
247
+ out.push(" → every commit: blocks a secret / real .env (fail-closed).");
248
+ out.push(" → every commit (npm repos): audits deps for vulnerable transitives;");
249
+ out.push(" a real finding blocks, an offline advisory DB only warns + allows.");
250
+ out.push("");
251
+ out.push(" 3. Release / CI gate ............. optional");
252
+ out.push(" safedeps gates run # secret scan + npm dep audit + hook/CI check");
253
+ out.push(" → full-repo sweep for CI / pre-release: scans the whole tree (not just");
254
+ out.push(" the staged diff) and verifies the hooks themselves are installed.");
255
+ out.push("");
256
+ out.push(" Docs: README → \"Two Lanes\"");
257
+ if (!safedepsOnPath()) {
258
+ out.push("");
259
+ out.push(" Note: `safedeps` is not on your PATH. To run the commands above:");
260
+ out.push(" - re-run this installer with --link-bin (adds ~/.local/bin/safedeps), or");
261
+ out.push(" - use the full path: ~/.claude/skills/safedeps/bin/safedeps");
262
+ }
263
+ out.push("");
264
+ console.log(out.join("\n"));
265
+ }
266
+
222
267
  function main() {
223
268
  if (!existsSync(REPO_PRE_HOOK) || !existsSync(REPO_POST_HOOK)) {
224
269
  throw new Error(`hook scripts not found at ${REPO_PRE_HOOK} / ${REPO_POST_HOOK}`);
@@ -242,9 +287,7 @@ function main() {
242
287
  log("uninstall done.");
243
288
  } else {
244
289
  log("install done. New hook events fire on the next session start.");
245
- // The dependency-install gate is global. The secret-leak lane is per-repo
246
- // and stays opt-in (its policy lives in each repo). Nudge, do not auto-write.
247
- log("secret-leak lane is per-repo — in a repo run: safedeps doctor (then `safedeps doctor --fix` to scaffold + activate).");
290
+ printRecommendedSetup();
248
291
  }
249
292
  }
250
293
 
@@ -328,4 +328,64 @@ else
328
328
  printf 'ok - pre-commit gate behavior SKIPPED (needs gitleaks + openssl)\n'
329
329
  fi
330
330
 
331
+ # --- Dependency audit gate (npm) — v2.5.0 -----------------------------------
332
+ # A fake `npm` makes the crucial distinction deterministic and offline: a
333
+ # vulnerable verdict (block) must never be confused with an unreachable advisory
334
+ # DB (warn + allow). If those two collapsed, an offline failover would silently
335
+ # let real vulnerabilities through.
336
+ fakebin="${tmp_root}/fakebin"
337
+ mkdir -p "${fakebin}"
338
+ cat > "${fakebin}/npm" <<'FAKE'
339
+ #!/bin/bash
340
+ [ "${1:-}" = "audit" ] || exit 0
341
+ case "${FAKE_NPM_MODE:-clean}" in
342
+ clean) printf '%s\n' '{"auditReportVersion":2,"vulnerabilities":{},"metadata":{"vulnerabilities":{"info":0,"low":0,"moderate":0,"high":0,"critical":0,"total":0}}}'; exit 0 ;;
343
+ vuln) printf '%s\n' '{"auditReportVersion":2,"vulnerabilities":{"hono":{"name":"hono","severity":"moderate","via":[{"title":"JWT"}]}},"metadata":{"vulnerabilities":{"info":0,"low":0,"moderate":4,"high":0,"critical":0,"total":4}}}'; exit 1 ;;
344
+ offline) printf '%s\n' '{"error":{"code":"ENOTFOUND","summary":"registry unreachable"}}'; exit 1 ;;
345
+ esac
346
+ FAKE
347
+ chmod +x "${fakebin}/npm"
348
+
349
+ if command -v jq >/dev/null 2>&1; then
350
+ audit_repo="${tmp_root}/audit-repo"
351
+ mkdir -p "${audit_repo}"
352
+ printf '{"name":"a","lockfileVersion":3}\n' > "${audit_repo}/package-lock.json"
353
+ run_audit() {
354
+ PATH="${fakebin}:${PATH}" FAKE_NPM_MODE="$1" \
355
+ "${ROOT_DIR}/bin/safedeps" audit npm --root "${audit_repo}" >/dev/null 2>&1
356
+ }
357
+ run_audit clean && rc=0 || rc=$?; [ "${rc}" = "0" ] || fail "audit exit 0 on a clean lockfile (got ${rc})"
358
+ run_audit vuln && rc=0 || rc=$?; [ "${rc}" = "1" ] || fail "audit exit 1 on a vulnerable lockfile (got ${rc})"
359
+ run_audit offline && rc=0 || rc=$?; [ "${rc}" = "2" ] || fail "audit exit 2 when the advisory DB is unreachable (got ${rc})"
360
+ pass "audit npm exit-code contract: clean=0 / vulnerable=1 / unreachable=2"
361
+ else
362
+ printf 'ok - audit exit-code contract SKIPPED (needs jq)\n'
363
+ fi
364
+
365
+ if command -v gitleaks >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
366
+ dep_repo="${tmp_root}/dep-repo"
367
+ mkdir -p "${dep_repo}"
368
+ git -C "${dep_repo}" init -q
369
+ git -C "${dep_repo}" config user.email t@safedeps.test
370
+ git -C "${dep_repo}" config user.name safedeps-e2e
371
+ HOME="${tmp_root}/doc-home" "${ROOT_DIR}/bin/safedeps" doctor --fix --root "${dep_repo}" >/dev/null
372
+ printf '{"name":"a","lockfileVersion":3}\n' > "${dep_repo}/package-lock.json"
373
+ git -C "${dep_repo}" add package-lock.json
374
+
375
+ # Threat: a vulnerable dependency must BLOCK the commit (fail-closed verdict).
376
+ if PATH="${fakebin}:${PATH}" FAKE_NPM_MODE=vuln SAFEDEPS_BIN="${ROOT_DIR}/bin/safedeps" \
377
+ git -C "${dep_repo}" commit -q -m "vuln" 2>/dev/null; then
378
+ fail "pre-commit blocks a commit carrying a vulnerable dependency"
379
+ fi
380
+
381
+ # Availability failover: an unreachable advisory DB must WARN and ALLOW.
382
+ offline_out="$(PATH="${fakebin}:${PATH}" FAKE_NPM_MODE=offline SAFEDEPS_BIN="${ROOT_DIR}/bin/safedeps" \
383
+ git -C "${dep_repo}" commit -m "offline" 2>&1)" \
384
+ || fail "pre-commit allows the commit when the advisory DB is unreachable (offline failover)"
385
+ grep -q "offline failover" <<< "${offline_out}" || fail "offline failover prints an observable warning"
386
+ pass "pre-commit dep gate: blocks on vuln, warns+allows when offline"
387
+ else
388
+ printf 'ok - pre-commit dep gate SKIPPED (needs gitleaks + jq)\n'
389
+ fi
390
+
331
391
  printf 'e2e passed\n'