@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 +1 -1
- package/README.ko.md +19 -2
- package/README.md +8 -2
- package/ROADMAP.md +19 -1
- package/SKILL.md +4 -2
- package/bin/safedeps +1 -1
- package/lib/gates/audit.sh +59 -7
- package/lib/gates/templates/pre-commit.tmpl +39 -10
- package/package.json +1 -1
- package/scripts/install/install-safedeps-hooks.mjs +46 -3
- package/scripts/test/e2e.sh +60 -0
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`
|
|
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`
|
|
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 은
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
package/lib/gates/audit.sh
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
|
2
|
+
# .githooks/pre-commit — safedeps repo gate (scaffolded by `safedeps hooks init`).
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
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
|
-
#
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
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
|
@@ -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
|
-
|
|
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
|
|
package/scripts/test/e2e.sh
CHANGED
|
@@ -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'
|