proof-of-commitment 1.5.0 → 1.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.md +70 -261
- package/index.js +148 -153
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,294 +1,103 @@
|
|
|
1
|
-
#
|
|
1
|
+
# proof-of-commitment
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Supply chain risk scorer for npm, PyPI, Cargo (Rust), and Go modules. Behavioral signals that can't be faked.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
An MCP server and web tool that scores npm packages, PyPI packages, Rust crates, and GitHub repos on **behavioral commitment** — signals that are harder to fake than stars, READMEs, or download counts.
|
|
8
|
-
|
|
9
|
-
## The supply chain problem
|
|
10
|
-
|
|
11
|
-
26 of the 91 npm packages with >10M weekly downloads have a **single npm publisher**. Together they account for over 3 billion downloads per week. `npm audit` doesn't surface this. Stars don't either.
|
|
12
|
-
|
|
13
|
-
Four packages in a typical Node.js project are CRITICAL right now:
|
|
14
|
-
- **chalk** — 413M downloads/week, **1 npm publisher**
|
|
15
|
-
- **zod** — 163M downloads/week, **1 npm publisher** (30+ GitHub contributors)
|
|
16
|
-
- **lodash** — 145M downloads/week, **1 npm publisher**
|
|
17
|
-
- **axios** — 99M downloads/week, **1 npm publisher** (attacked March 30, 2026)
|
|
18
|
-
|
|
19
|
-
They won't appear in your `package.json` either — but these are in almost every project:
|
|
20
|
-
- **minimatch** — 562M downloads/week, **1 npm publisher**
|
|
21
|
-
- **glob** — 333M downloads/week, **1 npm publisher**
|
|
22
|
-
- **cross-spawn** — 190M downloads/week, **1 npm publisher**
|
|
23
|
-
|
|
24
|
-
Behavioral signals surface this. Stars and READMEs don't.
|
|
25
|
-
|
|
26
|
-
## Try it now
|
|
27
|
-
|
|
28
|
-
**Terminal (zero install):**
|
|
29
|
-
```bash
|
|
5
|
+
```
|
|
30
6
|
npx proof-of-commitment axios zod chalk
|
|
31
|
-
# scan your own project:
|
|
32
|
-
npx proof-of-commitment --file package.json
|
|
33
|
-
# scan ALL transitive dependencies via lock file (finds the hidden CRITICAL packages):
|
|
34
|
-
npx proof-of-commitment --file package-lock.json # npm
|
|
35
|
-
npx proof-of-commitment --file yarn.lock # yarn
|
|
36
|
-
npx proof-of-commitment --file pnpm-lock.yaml # pnpm
|
|
37
|
-
# pnpm monorepo — scans all workspace packages, deduplicates:
|
|
38
|
-
npx proof-of-commitment --file pnpm-workspace.yaml # pnpm workspaces
|
|
39
|
-
# JSON output for CI/CD pipelines (exits 1 if CRITICAL found):
|
|
40
|
-
npx proof-of-commitment --file package-lock.json --json | jq '.criticalCount'
|
|
41
|
-
# PyPI too:
|
|
42
|
-
npx proof-of-commitment --pypi litellm langchain requests
|
|
43
|
-
# Cargo (Rust) via API:
|
|
44
|
-
curl -s https://poc-backend.amdal-dev.workers.dev/api/audit \
|
|
45
|
-
-X POST -H "Content-Type: application/json" \
|
|
46
|
-
-d '{"packages":["serde","tokio","reqwest"],"ecosystem":"cargo"}'
|
|
47
7
|
```
|
|
48
8
|
|
|
49
|
-
**Web demo (no install):** [getcommit.dev/audit](https://getcommit.dev/audit) — paste your packages, see risk scores in seconds.
|
|
50
|
-
|
|
51
|
-
**MCP server (zero install):**
|
|
52
|
-
|
|
53
|
-
```json
|
|
54
|
-
{
|
|
55
|
-
"mcpServers": {
|
|
56
|
-
"proof-of-commitment": {
|
|
57
|
-
"type": "streamable-http",
|
|
58
|
-
"url": "https://poc-backend.amdal-dev.workers.dev/mcp"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
9
|
```
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
pull_request:
|
|
81
|
-
paths: ['package.json', 'package-lock.json', 'bun.lock']
|
|
82
|
-
|
|
83
|
-
jobs:
|
|
84
|
-
audit:
|
|
85
|
-
runs-on: ubuntu-latest
|
|
86
|
-
permissions:
|
|
87
|
-
pull-requests: write
|
|
88
|
-
steps:
|
|
89
|
-
- uses: actions/checkout@v4
|
|
90
|
-
- uses: piiiico/commit-action@v1
|
|
91
|
-
with:
|
|
92
|
-
fail-on-critical: true # blocks merges on CRITICAL packages
|
|
93
|
-
comment-on-pr: true # posts results as a PR comment
|
|
10
|
+
──────────────────────────────────────────────────────────────────────────
|
|
11
|
+
Package Risk Score Publishers Downloads Age
|
|
12
|
+
──────────────────────────────────────────────────────────────────────────
|
|
13
|
+
axios 🔴 CRITICAL 89 1 102.0M/wk 11.6y
|
|
14
|
+
↳ 30+ GitHub contributors — publish-access concentration risk despite active community
|
|
15
|
+
└ longevity=25 momentum=25 releases=20 publishers=4 github=15
|
|
16
|
+
zod 🔴 CRITICAL 83 1 154.0M/wk 6.1y
|
|
17
|
+
↳ 30+ GitHub contributors — publish-access concentration risk despite active community
|
|
18
|
+
└ longevity=25 momentum=25 releases=18 publishers=4 github=11
|
|
19
|
+
chalk 🔴 CRITICAL 75 1 414.6M/wk 12.7y
|
|
20
|
+
↳ 30+ GitHub contributors — publish-access concentration risk despite active community
|
|
21
|
+
└ longevity=25 momentum=22 releases=13 publishers=4 github=11
|
|
22
|
+
──────────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
⚠ 3 CRITICAL packages found.
|
|
25
|
+
CRITICAL = sole npm publisher + >10M weekly downloads (publish-access concentration risk)
|
|
26
|
+
Full breakdown: https://getcommit.dev/audit?packages=axios,zod,chalk
|
|
94
27
|
```
|
|
95
28
|
|
|
96
|
-
|
|
29
|
+
## What this does
|
|
97
30
|
|
|
98
|
-
|
|
31
|
+
`npm audit` finds *known* CVEs — vulnerabilities already catalogued in a database. This scores *structural risk before it becomes a CVE*.
|
|
99
32
|
|
|
100
|
-
|
|
101
|
-
|-------|---------|-------------|
|
|
102
|
-
| `packages` | _(auto)_ | Comma-separated package names (auto-detected from `package.json`/`requirements.txt` if not set) |
|
|
103
|
-
| `packages-file` | _(auto)_ | Path to `package.json` or `requirements.txt` (default: auto-detect in workspace root) |
|
|
104
|
-
| `fail-on-critical` | `true` | Fail the workflow if CRITICAL packages are found |
|
|
105
|
-
| `max-packages` | `20` | Max packages to audit when auto-detecting |
|
|
106
|
-
| `include-dev-dependencies` | `false` | Include `devDependencies` from `package.json` |
|
|
107
|
-
| `comment-on-pr` | `true` | Post audit results as a PR comment (requires `pull-requests: write` permission) |
|
|
108
|
-
| `api-key` | _(none)_ | [Commit Pro](https://getcommit.dev/pricing) API key — enables batch requests and 10K requests/month |
|
|
109
|
-
| `api-url` | _(prod)_ | Override API endpoint (useful for self-hosting) |
|
|
33
|
+
The axios attack on April 1st, 2026: `npm audit` showed zero issues beforehand. Proof of Commitment flagged axios as CRITICAL (1 npm publisher, 96M downloads/week) — the exact publish-access concentration profile that made it a high-value target.
|
|
110
34
|
|
|
111
|
-
**
|
|
35
|
+
**Score dimensions:**
|
|
36
|
+
- **Longevity** (25 pts) — years in production
|
|
37
|
+
- **Download Momentum** (25 pts) — weekly download trend
|
|
38
|
+
- **Release Consistency** (20 pts) — days since last release
|
|
39
|
+
- **Publisher Depth** (15 pts) — npm publish-access holders
|
|
40
|
+
- **GitHub Backing** (15 pts) — organization/team support
|
|
112
41
|
|
|
113
|
-
**
|
|
42
|
+
**CRITICAL** = sole npm publisher + >10M weekly downloads (publish-access concentration risk)
|
|
114
43
|
|
|
115
|
-
|
|
44
|
+
## Usage
|
|
116
45
|
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
| chalk | 🔴 CRITICAL | 75 | 1 | 380M | 12.7y |
|
|
121
|
-
| zod | 🔴 CRITICAL | 83 | 1 | 133M | 6.1y |
|
|
122
|
-
| axios | 🔴 CRITICAL | 89 | 1 | 93M | 11.6y |
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
## README Badges
|
|
126
|
-
|
|
127
|
-
Add a Commit Trust badge to any npm package you maintain or depend on:
|
|
128
|
-
|
|
129
|
-
```markdown
|
|
130
|
-

|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Examples:
|
|
46
|
+
```bash
|
|
47
|
+
# Score npm packages
|
|
48
|
+
npx proof-of-commitment axios zod chalk lodash express
|
|
134
49
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
| chalk | `` |
|
|
138
|
-
| react | `` |
|
|
139
|
-
| express | `` |
|
|
140
|
-
| @babel/core | `` |
|
|
50
|
+
# Score PyPI packages
|
|
51
|
+
npx proof-of-commitment --pypi litellm langchain requests numpy
|
|
141
52
|
|
|
142
|
-
|
|
53
|
+
# Score Rust crates
|
|
54
|
+
npx proof-of-commitment --cargo serde tokio reqwest
|
|
143
55
|
|
|
144
|
-
|
|
56
|
+
# Score Go modules (full module path required — host/owner/repo)
|
|
57
|
+
npx proof-of-commitment --golang github.com/gin-gonic/gin golang.org/x/net
|
|
145
58
|
|
|
146
|
-
|
|
59
|
+
# Auto-detect from manifest / lock / module file
|
|
60
|
+
npx proof-of-commitment --file package.json # npm
|
|
61
|
+
npx proof-of-commitment --file package-lock.json # full transitive
|
|
62
|
+
npx proof-of-commitment --file requirements.txt # PyPI
|
|
63
|
+
npx proof-of-commitment --file Cargo.toml # Rust direct deps
|
|
64
|
+
npx proof-of-commitment --file go.mod # Go direct + indirect
|
|
65
|
+
npx proof-of-commitment --file go.sum # Go full transitive set
|
|
147
66
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-

|
|
151
|
-

|
|
67
|
+
# Short alias
|
|
68
|
+
npx poc axios zod chalk
|
|
152
69
|
```
|
|
153
70
|
|
|
154
|
-
|
|
71
|
+
**Go modules note:** Go has no centralized download counter and no publisher registry. Scoring is GitHub-primary: longevity, release cadence, GitHub contributor count, OpenSSF Scorecard, and stars (popularity proxy). The "publishers" column in Go output shows GitHub push-access contributors — the closest equivalent to npm publishers.
|
|
155
72
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
```bash
|
|
159
|
-
curl https://poc-backend.amdal-dev.workers.dev/api/audit \
|
|
160
|
-
-X POST \
|
|
161
|
-
-H "Content-Type: application/json" \
|
|
162
|
-
-d '{"packages": ["axios", "zod", "chalk", "lodash", "express"]}'
|
|
163
|
-
```
|
|
73
|
+
## Zero-install MCP server (for Claude, Cursor, Windsurf)
|
|
164
74
|
|
|
75
|
+
Add to your AI tool's config:
|
|
165
76
|
```json
|
|
166
77
|
{
|
|
167
|
-
"
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
"
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
"maintainers": 1,
|
|
174
|
-
"weeklyDownloads": 398397580,
|
|
175
|
-
"ageYears": 12.7,
|
|
176
|
-
"trend": "stable",
|
|
177
|
-
"riskFlags": ["CRITICAL"],
|
|
178
|
-
"scorecardScore": 3.6, // null if no GitHub repo
|
|
179
|
-
"hasDangerousWorkflow": false // null if no Scorecard data
|
|
180
|
-
},
|
|
181
|
-
...
|
|
182
|
-
]
|
|
78
|
+
"mcpServers": {
|
|
79
|
+
"proof-of-commitment": {
|
|
80
|
+
"type": "streamable-http",
|
|
81
|
+
"url": "https://poc-backend.amdal-dev.workers.dev/mcp"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
183
84
|
}
|
|
184
85
|
```
|
|
185
86
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
| Tool | Description |
|
|
189
|
-
|------|-------------|
|
|
190
|
-
| `audit_dependencies` | Batch risk audit for up to 20 npm/PyPI/Cargo packages |
|
|
191
|
-
| `lookup_npm_package` | Single npm package behavioral profile |
|
|
192
|
-
| `lookup_pypi_package` | Single PyPI package behavioral profile |
|
|
193
|
-
| `lookup_cargo_crate` | Single Rust crate behavioral profile (crates.io) |
|
|
194
|
-
| `lookup_github_repo` | GitHub repo commitment score (longevity, commit frequency, contributor depth) |
|
|
195
|
-
| `lookup_business` | Norwegian business register — operating years, employees, financials |
|
|
196
|
-
| `lookup_business_by_org` | Same, by org number |
|
|
197
|
-
| `query_commitment` | Browser extension behavioral data (unique verified visitors, repeat rate) |
|
|
198
|
-
|
|
199
|
-
## What the score measures
|
|
200
|
-
|
|
201
|
-
Each package is scored 0–100 across:
|
|
202
|
-
|
|
203
|
-
- **Longevity** — How long has the package existed? Abandoned packages get reactivated for attacks.
|
|
204
|
-
- **Publisher depth** — Single npm publisher + millions of weekly downloads = the attack surface LiteLLM exploited. (Publisher = person with npm publish access, distinct from GitHub contributors.)
|
|
205
|
-
- **Release consistency** — Regular releases signal active oversight. Long gaps = vulnerability accumulation.
|
|
206
|
-
- **Download trend** — Growing packages attract more scrutiny (and attacks). Stable = lower profile.
|
|
207
|
-
- **OpenSSF Scorecard** — Process security (code review enforcement, branch protection, CI/CD safety). Separate from behavioral signals. High Scorecard ≠ safe from credential theft attacks.
|
|
208
|
-
|
|
209
|
-
> Both axios (8.1/10 Scorecard) and chalk (3.6/10 Scorecard) score CRITICAL on behavioral signals. They measure different attack surfaces — Scorecard catches process gaps, behavioral signals catch publisher concentration.
|
|
210
|
-
|
|
211
|
-
**Risk flags:**
|
|
212
|
-
- `CRITICAL` — single npm publisher + >10M weekly downloads (exact LiteLLM/axios attack profile)
|
|
213
|
-
- `HIGH` — package <1yr old + rapid adoption
|
|
214
|
-
- `WARN` — no release in 12+ months
|
|
215
|
-
|
|
216
|
-
## Real data points
|
|
217
|
-
|
|
218
|
-
```
|
|
219
|
-
# packages you know about:
|
|
220
|
-
chalk — score 75, 1 publisher, 413M/week ⚑ CRITICAL
|
|
221
|
-
zod — score 86, 1 publisher, 163M/week ⚑ CRITICAL (30+ GitHub contributors)
|
|
222
|
-
lodash — score 81, 1 publisher, 145M/week ⚑ CRITICAL
|
|
223
|
-
axios — score 86, 1 publisher, 99M/week ⚑ CRITICAL (attacked Mar 30 2026)
|
|
224
|
-
express — score 90, 5 publishers, 95M/week
|
|
225
|
-
|
|
226
|
-
# packages probably not in your package.json, definitely in your lock file:
|
|
227
|
-
minimatch — score 78, 1 publisher, 562M/week ⚑ CRITICAL
|
|
228
|
-
glob — score 80, 1 publisher, 333M/week ⚑ CRITICAL
|
|
229
|
-
cross-spawn — score 72, 1 publisher, 190M/week ⚑ CRITICAL
|
|
87
|
+
Then ask: *"Audit the dependencies in my package.json"* or *"What's the risk profile of vercel/ai?"*
|
|
230
88
|
|
|
231
|
-
|
|
232
|
-
litellm — score 74, 1 publisher ⚑ CRITICAL (supply chain attack Mar 2026)
|
|
89
|
+
## GitHub Action
|
|
233
90
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
91
|
+
Posts audit results directly on your PR:
|
|
92
|
+
```yaml
|
|
93
|
+
- uses: piiiico/proof-of-commitment@main
|
|
94
|
+
with:
|
|
95
|
+
fail-on-critical: false
|
|
96
|
+
comment-on-pr: true
|
|
238
97
|
```
|
|
239
98
|
|
|
240
|
-
##
|
|
241
|
-
|
|
242
|
-
The LiteLLM attack (March 2026) and axios attack (March 30, 2026) followed the same pattern: stolen credentials → malicious package pushed → 97M+ machines exposed. Both packages scored CRITICAL by these metrics *before* the attacks.
|
|
99
|
+
## Links
|
|
243
100
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
| Layer | Technology |
|
|
249
|
-
|-------|-----------|
|
|
250
|
-
| Backend | Cloudflare Workers + D1 |
|
|
251
|
-
| MCP | Model Context Protocol SDK |
|
|
252
|
-
| Data | npm registry, PyPI, crates.io, GitHub API, Brønnøysund (NO) |
|
|
253
|
-
| Landing | Astro + Cloudflare Pages |
|
|
254
|
-
|
|
255
|
-
## Roadmap
|
|
256
|
-
|
|
257
|
-
Planned, not promised. The project is early-stage — contributions welcome on any of these.
|
|
258
|
-
|
|
259
|
-
| Feature | Status | Notes |
|
|
260
|
-
|---------|--------|-------|
|
|
261
|
-
| **Cargo (Rust) registry support** | ✅ Live | MCP tool, REST API, badge endpoint — `ecosystem: "cargo"` |
|
|
262
|
-
| **Go modules support** | Planned | pkg.go.dev API + GitHub backing score |
|
|
263
|
-
| **Score breakdown visualization** | Planned | Chart component for the 5 dimensions on getcommit.dev/audit |
|
|
264
|
-
| **`--json` flag for CLI** | ✅ Live | `npx proof-of-commitment --file package-lock.json --json \| jq '.criticalCount'` |
|
|
265
|
-
| **pnpm workspace monorepo support** | ✅ Live | `--file pnpm-workspace.yaml` or auto-detected from `pnpm-lock.yaml` |
|
|
266
|
-
| **Historical score tracking** | Planned | Trend charts — was this package getting riskier over time? |
|
|
267
|
-
| **Org-level dashboards** | Planned | Aggregate risk view across all repos in a GitHub org |
|
|
268
|
-
|
|
269
|
-
See [open issues](https://github.com/piiiico/proof-of-commitment/issues) for things you can help with today.
|
|
270
|
-
|
|
271
|
-
## The broader vision
|
|
272
|
-
|
|
273
|
-
Supply chain auditing is the first tool. The underlying primitive is a **commitment graph** — behavioral signals that replace content-based trust across any domain.
|
|
274
|
-
|
|
275
|
-
When content is free to fake (reviews, stars, READMEs), commitment becomes the signal. A publisher who has shipped 847 releases over 12 years is a different kind of commitment than one who published once in 2023.
|
|
276
|
-
|
|
277
|
-
The same logic applies to websites, businesses, and AI agents. Two card networks have independently named this gap: Mastercard Verifiable Intent §9.2 explicitly lists behavioral trust as "not covered." Visa TAP identifies agents without answering whether to trust them.
|
|
278
|
-
|
|
279
|
-
Proof of Commitment is the trust layer they're pointing at.
|
|
280
|
-
|
|
281
|
-
→ [getcommit.dev](https://getcommit.dev)
|
|
282
|
-
|
|
283
|
-
## Run locally
|
|
284
|
-
|
|
285
|
-
```bash
|
|
286
|
-
bun install
|
|
287
|
-
bun run dev:backend # local server with SQLite
|
|
288
|
-
bun run test:e2e # E2E test with mock World ID
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
Deploy:
|
|
292
|
-
```bash
|
|
293
|
-
bun run deploy # deploys to Cloudflare Workers
|
|
294
|
-
```
|
|
101
|
+
- **Web demo:** https://getcommit.dev/audit
|
|
102
|
+
- **Live watchlist:** https://getcommit.dev/watchlist (top 25 npm packages by structural risk)
|
|
103
|
+
- **GitHub:** https://github.com/piiiico/proof-of-commitment
|
package/index.js
CHANGED
|
@@ -87,12 +87,23 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
|
|
|
87
87
|
const label = riskLabel(pkg.riskFlags, pkg.score);
|
|
88
88
|
if (pkg.riskFlags && pkg.riskFlags.includes('CRITICAL')) criticalInDisplay++;
|
|
89
89
|
|
|
90
|
+
// Go modules have no download data — show "—" instead. The maintainers
|
|
91
|
+
// column for Go shows GitHub contributor count (the closest equivalent
|
|
92
|
+
// to publish access since Go has no centralized publisher concept).
|
|
93
|
+
const isGo = pkg.ecosystem === 'golang';
|
|
94
|
+
const dlDisplay = isGo
|
|
95
|
+
? '—'
|
|
96
|
+
: fmtDownloads(pkg.weeklyDownloads || 0);
|
|
97
|
+
const maintDisplay = pkg.maintainers === 35
|
|
98
|
+
? '30+'
|
|
99
|
+
: String(pkg.maintainers || '?');
|
|
100
|
+
|
|
90
101
|
const row = [
|
|
91
102
|
padEnd(pkg.name, COL.name),
|
|
92
103
|
padEnd(clr(rc, label), COL.risk),
|
|
93
104
|
padEnd(String(pkg.score), COL.score),
|
|
94
|
-
padEnd(
|
|
95
|
-
padEnd(
|
|
105
|
+
padEnd(maintDisplay, COL.maintainers),
|
|
106
|
+
padEnd(dlDisplay, COL.downloads),
|
|
96
107
|
padEnd((pkg.ageYears || '?').toString().replace(/(\.\d).*/, '$1') + 'y', COL.age),
|
|
97
108
|
].join(' ');
|
|
98
109
|
|
|
@@ -104,13 +115,18 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
|
|
|
104
115
|
console.log(clr(c.dim, ` ↳ ${ghCount} GitHub contributors — publish-access concentration risk despite active community`));
|
|
105
116
|
}
|
|
106
117
|
|
|
107
|
-
// Score breakdown if available
|
|
118
|
+
// Score breakdown if available — keys differ across ecosystems
|
|
108
119
|
if (pkg.scoreBreakdown) {
|
|
109
120
|
const b = pkg.scoreBreakdown;
|
|
110
|
-
const breakdown =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
const breakdown = isGo
|
|
122
|
+
? clr(c.dim,
|
|
123
|
+
` └ longevity=${b.longevity} releases=${b.releaseConsistency} ` +
|
|
124
|
+
`contributors=${b.maintainerDepth} github=${b.githubBacking} stars=${b.popularityProxy}`
|
|
125
|
+
)
|
|
126
|
+
: clr(c.dim,
|
|
127
|
+
` └ longevity=${b.longevity} momentum=${b.downloadMomentum} ` +
|
|
128
|
+
`releases=${b.releaseConsistency} publishers=${b.maintainerDepth} github=${b.githubBacking}`
|
|
129
|
+
);
|
|
114
130
|
console.log(breakdown);
|
|
115
131
|
}
|
|
116
132
|
}
|
|
@@ -138,25 +154,33 @@ function printHelp() {
|
|
|
138
154
|
${clr(c.bold, 'proof-of-commitment')} — supply chain risk scorer
|
|
139
155
|
|
|
140
156
|
${clr(c.bold, 'Usage:')}
|
|
141
|
-
npx proof-of-commitment [packages...]
|
|
142
|
-
npx proof-of-commitment --pypi [pkgs...]
|
|
143
|
-
npx proof-of-commitment --
|
|
144
|
-
npx proof-of-commitment --
|
|
145
|
-
npx proof-of-commitment --file
|
|
146
|
-
npx proof-of-commitment --file
|
|
147
|
-
npx proof-of-commitment --file
|
|
148
|
-
npx proof-of-commitment --file
|
|
157
|
+
npx proof-of-commitment [packages...] Score npm packages
|
|
158
|
+
npx proof-of-commitment --pypi [pkgs...] Score PyPI packages
|
|
159
|
+
npx proof-of-commitment --cargo [crates...] Score Rust crates
|
|
160
|
+
npx proof-of-commitment --golang [modules...] Score Go modules (full path required)
|
|
161
|
+
npx proof-of-commitment --file package.json Audit direct dependencies
|
|
162
|
+
npx proof-of-commitment --file package-lock.json Audit ALL dependencies (lock file)
|
|
163
|
+
npx proof-of-commitment --file yarn.lock Audit from yarn lock file
|
|
164
|
+
npx proof-of-commitment --file pnpm-lock.yaml Audit from pnpm lock file
|
|
165
|
+
npx proof-of-commitment --file requirements.txt Audit Python packages
|
|
166
|
+
npx proof-of-commitment --file Cargo.toml Audit Rust direct dependencies
|
|
167
|
+
npx proof-of-commitment --file go.mod Audit Go direct + indirect deps
|
|
168
|
+
npx proof-of-commitment --file go.sum Audit Go full transitive set
|
|
149
169
|
|
|
150
170
|
${clr(c.bold, 'Options:')}
|
|
151
171
|
--json Output results as JSON (exits 1 if any CRITICAL found — useful in CI)
|
|
152
172
|
--pypi Score PyPI packages instead of npm
|
|
153
|
-
--
|
|
173
|
+
--cargo Score Rust crates from crates.io
|
|
174
|
+
--golang Score Go modules from proxy.golang.org (use full path: github.com/owner/repo)
|
|
175
|
+
--file, -f Read packages from package.json, lock file, requirements.txt, Cargo.toml, or go.mod/go.sum
|
|
154
176
|
|
|
155
177
|
${clr(c.bold, 'Examples:')}
|
|
156
178
|
npx proof-of-commitment axios zod chalk
|
|
157
179
|
npx proof-of-commitment --pypi litellm langchain requests
|
|
158
|
-
npx proof-of-commitment --
|
|
180
|
+
npx proof-of-commitment --cargo serde tokio reqwest
|
|
181
|
+
npx proof-of-commitment --golang github.com/gin-gonic/gin golang.org/x/net
|
|
159
182
|
npx proof-of-commitment --file package-lock.json # scans ALL transitive deps
|
|
183
|
+
npx proof-of-commitment --file go.sum # scans full Go module graph
|
|
160
184
|
npx proof-of-commitment axios chalk --json | jq '.criticalCount'
|
|
161
185
|
|
|
162
186
|
${clr(c.bold, 'Score meaning:')}
|
|
@@ -166,7 +190,8 @@ ${clr(c.bold, 'Score meaning:')}
|
|
|
166
190
|
🟡 GOOD Score 60–74
|
|
167
191
|
🟢 HEALTHY Score 75+
|
|
168
192
|
|
|
169
|
-
${clr(c.bold, 'Score dimensions:')} longevity · download momentum · release consistency · publisher depth · GitHub backing
|
|
193
|
+
${clr(c.bold, 'Score dimensions (npm/PyPI/Cargo):')} longevity · download momentum · release consistency · publisher depth · GitHub backing
|
|
194
|
+
${clr(c.bold, 'Score dimensions (Go):')} longevity · release consistency · maintainer depth · GitHub backing · stars (Go has no centralized download counter)
|
|
170
195
|
|
|
171
196
|
${clr(c.bold, 'Web:')} ${WEB}
|
|
172
197
|
${clr(c.bold, 'MCP:')} ${clr(c.dim, 'Add to Claude Desktop / Cursor for AI-assisted auditing')}
|
|
@@ -230,123 +255,11 @@ function parseLockPnpm(content) {
|
|
|
230
255
|
return [...pkgs];
|
|
231
256
|
}
|
|
232
257
|
|
|
233
|
-
/**
|
|
234
|
-
* Parse pnpm-workspace.yaml and resolve all workspace package dependencies.
|
|
235
|
-
* Reads package.json from each workspace, merges deps, deduplicates.
|
|
236
|
-
* Internal workspace packages are excluded from the audit list.
|
|
237
|
-
*/
|
|
238
|
-
async function parsePnpmWorkspace(filePath) {
|
|
239
|
-
const fs = await import('fs');
|
|
240
|
-
const path = await import('path');
|
|
241
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
242
|
-
const dir = path.dirname(path.resolve(filePath));
|
|
243
|
-
|
|
244
|
-
// Parse packages list from YAML
|
|
245
|
-
// Format: "packages:\n - 'apps/*'\n - 'packages/*'"
|
|
246
|
-
const patterns = [];
|
|
247
|
-
const lines = content.split('\n');
|
|
248
|
-
let inPackages = false;
|
|
249
|
-
for (const line of lines) {
|
|
250
|
-
if (/^packages\s*:/.test(line)) { inPackages = true; continue; }
|
|
251
|
-
if (inPackages) {
|
|
252
|
-
if (/^\s+-/.test(line)) {
|
|
253
|
-
const pattern = line.replace(/^\s+-\s*/, '').replace(/['"]/g, '').trim();
|
|
254
|
-
if (pattern) patterns.push(pattern);
|
|
255
|
-
} else if (/^\S/.test(line) && line.trim()) {
|
|
256
|
-
inPackages = false;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (patterns.length === 0) {
|
|
262
|
-
throw new Error('No workspace packages found in pnpm-workspace.yaml');
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Split into includes and excludes
|
|
266
|
-
const excludes = patterns.filter(p => p.startsWith('!')).map(p => p.slice(1));
|
|
267
|
-
const includes = patterns.filter(p => !p.startsWith('!'));
|
|
268
|
-
|
|
269
|
-
// Resolve patterns to directories containing package.json
|
|
270
|
-
const workspaceDirs = [];
|
|
271
|
-
for (const pattern of includes) {
|
|
272
|
-
if (pattern.endsWith('/*') || pattern.endsWith('/**')) {
|
|
273
|
-
// Glob: 'apps/*' -> list all subdirectories of apps/
|
|
274
|
-
const parentDir = path.join(dir, pattern.replace(/\/\*\*?$/, ''));
|
|
275
|
-
try {
|
|
276
|
-
const entries = fs.readdirSync(parentDir, { withFileTypes: true });
|
|
277
|
-
for (const entry of entries) {
|
|
278
|
-
if (entry.isDirectory()) {
|
|
279
|
-
const fullPath = path.join(parentDir, entry.name);
|
|
280
|
-
const isExcluded = excludes.some(ex => fullPath.includes(ex) || entry.name === ex);
|
|
281
|
-
if (!isExcluded) workspaceDirs.push(fullPath);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
} catch { /* directory doesn't exist — skip */ }
|
|
285
|
-
} else {
|
|
286
|
-
// Direct path: 'packages/shared'
|
|
287
|
-
const fullPath = path.join(dir, pattern);
|
|
288
|
-
try {
|
|
289
|
-
fs.statSync(fullPath);
|
|
290
|
-
workspaceDirs.push(fullPath);
|
|
291
|
-
} catch { /* doesn't exist — skip */ }
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Collect workspace package names (to exclude from audit — they're internal)
|
|
296
|
-
const workspacePackageNames = new Set();
|
|
297
|
-
for (const wsDir of workspaceDirs) {
|
|
298
|
-
try {
|
|
299
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(wsDir, 'package.json'), 'utf-8'));
|
|
300
|
-
if (pkg.name) workspacePackageNames.add(pkg.name);
|
|
301
|
-
} catch { /* no package.json — skip */ }
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Read root package.json + all workspace package.json files, merge deps
|
|
305
|
-
const allDeps = new Set();
|
|
306
|
-
let workspaceCount = 0;
|
|
307
|
-
|
|
308
|
-
// Root
|
|
309
|
-
try {
|
|
310
|
-
const rootPkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
|
|
311
|
-
if (rootPkg.dependencies) Object.keys(rootPkg.dependencies).forEach(d => allDeps.add(d));
|
|
312
|
-
if (rootPkg.devDependencies) Object.keys(rootPkg.devDependencies).forEach(d => allDeps.add(d));
|
|
313
|
-
} catch { /* no root package.json */ }
|
|
314
|
-
|
|
315
|
-
// Workspaces
|
|
316
|
-
for (const wsDir of workspaceDirs) {
|
|
317
|
-
try {
|
|
318
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(wsDir, 'package.json'), 'utf-8'));
|
|
319
|
-
if (pkg.dependencies) Object.keys(pkg.dependencies).forEach(d => allDeps.add(d));
|
|
320
|
-
if (pkg.devDependencies) Object.keys(pkg.devDependencies).forEach(d => allDeps.add(d));
|
|
321
|
-
workspaceCount++;
|
|
322
|
-
} catch { /* no package.json in workspace dir */ }
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Remove internal workspace packages from audit list
|
|
326
|
-
for (const name of workspacePackageNames) {
|
|
327
|
-
allDeps.delete(name);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return {
|
|
331
|
-
packages: [...allDeps],
|
|
332
|
-
ecosystem: 'npm',
|
|
333
|
-
lockfile: false,
|
|
334
|
-
totalInFile: allDeps.size,
|
|
335
|
-
workspaceCount,
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
258
|
async function readPackagesFromFile(filePath) {
|
|
340
259
|
const fs = await import('fs');
|
|
341
260
|
const path = await import('path');
|
|
342
|
-
const basename = path.basename(filePath).toLowerCase();
|
|
343
|
-
|
|
344
|
-
// pnpm-workspace.yaml — monorepo workspace scanner
|
|
345
|
-
if (basename === 'pnpm-workspace.yaml' || basename === 'pnpm-workspace.yml') {
|
|
346
|
-
return parsePnpmWorkspace(filePath);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
261
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
262
|
+
const basename = path.basename(filePath).toLowerCase();
|
|
350
263
|
|
|
351
264
|
// package-lock.json
|
|
352
265
|
if (basename === 'package-lock.json') {
|
|
@@ -360,20 +273,8 @@ async function readPackagesFromFile(filePath) {
|
|
|
360
273
|
return { packages: pkgs, ecosystem: 'npm', lockfile: true, totalInFile: pkgs.length };
|
|
361
274
|
}
|
|
362
275
|
|
|
363
|
-
// pnpm-lock.yaml
|
|
276
|
+
// pnpm-lock.yaml
|
|
364
277
|
if (basename === 'pnpm-lock.yaml' || basename === 'pnpm-lock.yml') {
|
|
365
|
-
const dir = path.dirname(path.resolve(filePath));
|
|
366
|
-
const workspaceFile = path.join(dir, 'pnpm-workspace.yaml');
|
|
367
|
-
let hasWorkspace = false;
|
|
368
|
-
try { fs.statSync(workspaceFile); hasWorkspace = true; } catch {}
|
|
369
|
-
|
|
370
|
-
if (hasWorkspace) {
|
|
371
|
-
// Monorepo detected — use workspace-aware parsing
|
|
372
|
-
const result = await parsePnpmWorkspace(workspaceFile);
|
|
373
|
-
result.monorepoDetected = true;
|
|
374
|
-
return result;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
278
|
const pkgs = parseLockPnpm(content);
|
|
378
279
|
return { packages: pkgs, ecosystem: 'npm', lockfile: true, totalInFile: pkgs.length };
|
|
379
280
|
}
|
|
@@ -399,7 +300,106 @@ async function readPackagesFromFile(filePath) {
|
|
|
399
300
|
return { packages: pkgs, ecosystem: 'pypi', lockfile: false };
|
|
400
301
|
}
|
|
401
302
|
|
|
402
|
-
|
|
303
|
+
// Cargo.toml
|
|
304
|
+
if (basename === 'cargo.toml') {
|
|
305
|
+
// Crude TOML parse — extract [dependencies] section keys
|
|
306
|
+
const pkgs = parseCargoToml(content);
|
|
307
|
+
return { packages: pkgs, ecosystem: 'cargo', lockfile: false };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// go.mod — Go module file
|
|
311
|
+
if (basename === 'go.mod') {
|
|
312
|
+
const pkgs = parseGoMod(content);
|
|
313
|
+
return { packages: pkgs, ecosystem: 'golang', lockfile: false, totalInFile: pkgs.length };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// go.sum — Go module checksum file (lockfile-equivalent — captures full transitive set)
|
|
317
|
+
if (basename === 'go.sum') {
|
|
318
|
+
const pkgs = parseGoSum(content);
|
|
319
|
+
return { packages: pkgs, ecosystem: 'golang', lockfile: true, totalInFile: pkgs.length };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
throw new Error(`Unsupported file: ${basename}. Supported: package.json, package-lock.json, yarn.lock, pnpm-lock.yaml, requirements.txt, Cargo.toml, go.mod, go.sum`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Parse a Cargo.toml — extract direct deps from [dependencies] / [dev-dependencies].
|
|
327
|
+
* Crude — only handles the common "name = version" / "name = { ... }" lines under
|
|
328
|
+
* [dependencies] / [dev-dependencies]. Doesn't expand workspace members.
|
|
329
|
+
*/
|
|
330
|
+
function parseCargoToml(content) {
|
|
331
|
+
const pkgs = new Set();
|
|
332
|
+
const lines = content.split('\n');
|
|
333
|
+
let inDeps = false;
|
|
334
|
+
for (const raw of lines) {
|
|
335
|
+
const line = raw.trim();
|
|
336
|
+
if (line.startsWith('[')) {
|
|
337
|
+
inDeps = /^\[(dev-)?dependencies\]/.test(line);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (!inDeps || !line || line.startsWith('#')) continue;
|
|
341
|
+
const match = line.match(/^([a-zA-Z0-9_-]+)\s*=/);
|
|
342
|
+
if (match) pkgs.add(match[1]);
|
|
343
|
+
}
|
|
344
|
+
return [...pkgs];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Parse a go.mod file — extract direct + indirect dependencies from require blocks.
|
|
349
|
+
* Format:
|
|
350
|
+
* require (
|
|
351
|
+
* github.com/gin-gonic/gin v1.9.1
|
|
352
|
+
* golang.org/x/net v0.20.0 // indirect
|
|
353
|
+
* )
|
|
354
|
+
* require github.com/foo/bar v1.0.0
|
|
355
|
+
*/
|
|
356
|
+
function parseGoMod(content) {
|
|
357
|
+
const pkgs = new Set();
|
|
358
|
+
const lines = content.split('\n');
|
|
359
|
+
let inRequireBlock = false;
|
|
360
|
+
|
|
361
|
+
for (const raw of lines) {
|
|
362
|
+
const line = raw.trim();
|
|
363
|
+
if (!line || line.startsWith('//')) continue;
|
|
364
|
+
|
|
365
|
+
// Block-form require: "require ("
|
|
366
|
+
if (/^require\s*\(\s*$/.test(line)) {
|
|
367
|
+
inRequireBlock = true;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (inRequireBlock && line === ')') {
|
|
371
|
+
inRequireBlock = false;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Inside a block: "<modulepath> <version>" optionally followed by comments
|
|
376
|
+
if (inRequireBlock) {
|
|
377
|
+
const match = line.match(/^([^\s]+)\s+v[^\s]+/);
|
|
378
|
+
if (match) pkgs.add(match[1]);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Single-line require: "require <modulepath> <version>"
|
|
383
|
+
const single = line.match(/^require\s+([^\s]+)\s+v[^\s]+/);
|
|
384
|
+
if (single) pkgs.add(single[1]);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return [...pkgs];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Parse a go.sum file — module path is the first column, version + suffix follow.
|
|
392
|
+
* Each module appears multiple times (once per artifact); dedupe.
|
|
393
|
+
*/
|
|
394
|
+
function parseGoSum(content) {
|
|
395
|
+
const pkgs = new Set();
|
|
396
|
+
for (const raw of content.split('\n')) {
|
|
397
|
+
const line = raw.trim();
|
|
398
|
+
if (!line) continue;
|
|
399
|
+
const match = line.match(/^([^\s]+)\s+v[^\s/]+/);
|
|
400
|
+
if (match) pkgs.add(match[1]);
|
|
401
|
+
}
|
|
402
|
+
return [...pkgs];
|
|
403
403
|
}
|
|
404
404
|
|
|
405
405
|
/**
|
|
@@ -465,6 +465,8 @@ async function main() {
|
|
|
465
465
|
const a = args[i];
|
|
466
466
|
if (a === '--pypi') { ecosystem = 'pypi'; i++; }
|
|
467
467
|
else if (a === '--npm') { ecosystem = 'npm'; i++; }
|
|
468
|
+
else if (a === '--cargo') { ecosystem = 'cargo'; i++; }
|
|
469
|
+
else if (a === '--golang' || a === '--go') { ecosystem = 'golang'; i++; }
|
|
468
470
|
else if (a === '--json') { jsonOutput = true; i++; }
|
|
469
471
|
else if (a === '--file' || a === '-f') {
|
|
470
472
|
filePath = args[++i];
|
|
@@ -484,14 +486,7 @@ async function main() {
|
|
|
484
486
|
ecosystem = result.ecosystem;
|
|
485
487
|
isLockfile = result.lockfile || false;
|
|
486
488
|
totalInFile = result.totalInFile || packages.length;
|
|
487
|
-
if (
|
|
488
|
-
if (!jsonOutput) console.log(clr(c.dim, `Monorepo: ${result.workspaceCount} workspace${result.workspaceCount > 1 ? 's' : ''} → ${totalInFile} unique external dependencies (${ecosystem})`));
|
|
489
|
-
if (result.monorepoDetected && !jsonOutput) {
|
|
490
|
-
console.log(clr(c.dim, ` (auto-detected pnpm-workspace.yaml next to ${filePath})`));
|
|
491
|
-
}
|
|
492
|
-
} else {
|
|
493
|
-
if (!jsonOutput) console.log(clr(c.dim, `Detected ${totalInFile} packages from ${filePath} (${ecosystem})`));
|
|
494
|
-
}
|
|
489
|
+
if (!jsonOutput) console.log(clr(c.dim, `Detected ${totalInFile} packages from ${filePath} (${ecosystem})`));
|
|
495
490
|
} catch (err) {
|
|
496
491
|
console.error(`Error reading ${filePath}: ${err.message}`);
|
|
497
492
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proof-of-commitment",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Supply chain risk scorer for npm, PyPI, and
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "Supply chain risk scorer for npm, PyPI, Cargo, and Go packages — behavioral signals that can't be faked",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"proof-of-commitment": "./index.js",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"pypi",
|
|
20
20
|
"cargo",
|
|
21
21
|
"rust",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
22
|
+
"golang",
|
|
23
|
+
"go",
|
|
24
|
+
"go-modules",
|
|
25
25
|
"dependencies",
|
|
26
26
|
"audit",
|
|
27
27
|
"risk",
|