proof-of-commitment 1.0.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 +88 -0
- package/index.js +264 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# proof-of-commitment
|
|
2
|
+
|
|
3
|
+
> Supply chain risk scorer for npm and PyPI packages. Behavioral signals that can't be faked.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npx proof-of-commitment axios zod chalk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
──────────────────────────────────────────────────────────────────────────
|
|
11
|
+
Package Risk Score Maintainers Downloads Age
|
|
12
|
+
──────────────────────────────────────────────────────────────────────────
|
|
13
|
+
axios 🔴 CRITICAL 89 1 102.0M/wk 11.6y
|
|
14
|
+
└ longevity=25 momentum=25 releases=20 maintainers=4 github=15
|
|
15
|
+
zod 🔴 CRITICAL 83 1 154.0M/wk 6.1y
|
|
16
|
+
└ longevity=25 momentum=25 releases=18 maintainers=4 github=11
|
|
17
|
+
chalk 🔴 CRITICAL 75 1 414.6M/wk 12.7y
|
|
18
|
+
└ longevity=25 momentum=22 releases=13 maintainers=4 github=11
|
|
19
|
+
──────────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
⚠ 3 CRITICAL packages found.
|
|
22
|
+
CRITICAL = sole maintainer + >10M weekly downloads (high-value attack target)
|
|
23
|
+
Full breakdown: https://getcommit.dev/audit?packages=axios,zod,chalk
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What this does
|
|
27
|
+
|
|
28
|
+
`npm audit` finds *known* CVEs — vulnerabilities already catalogued in a database. This scores *structural risk before it becomes a CVE*.
|
|
29
|
+
|
|
30
|
+
The axios attack on April 1st, 2026: `npm audit` showed zero issues beforehand. Proof of Commitment flagged axios as CRITICAL (1 maintainer, 96M downloads/week) — the exact profile that made it a high-value target.
|
|
31
|
+
|
|
32
|
+
**Score dimensions:**
|
|
33
|
+
- **Longevity** (25 pts) — years in production
|
|
34
|
+
- **Download Momentum** (25 pts) — weekly download trend
|
|
35
|
+
- **Release Consistency** (20 pts) — days since last release
|
|
36
|
+
- **Maintainer Depth** (15 pts) — team size
|
|
37
|
+
- **GitHub Backing** (15 pts) — organization/team support
|
|
38
|
+
|
|
39
|
+
**CRITICAL** = sole maintainer + >10M weekly downloads (high-value attack target)
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Score npm packages
|
|
45
|
+
npx proof-of-commitment axios zod chalk lodash express
|
|
46
|
+
|
|
47
|
+
# Score PyPI packages
|
|
48
|
+
npx proof-of-commitment --pypi litellm langchain requests numpy
|
|
49
|
+
|
|
50
|
+
# Auto-detect from package.json or requirements.txt
|
|
51
|
+
npx proof-of-commitment --file package.json
|
|
52
|
+
npx proof-of-commitment --file requirements.txt
|
|
53
|
+
|
|
54
|
+
# Short alias
|
|
55
|
+
npx poc axios zod chalk
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Zero-install MCP server (for Claude, Cursor, Windsurf)
|
|
59
|
+
|
|
60
|
+
Add to your AI tool's config:
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"mcpServers": {
|
|
64
|
+
"proof-of-commitment": {
|
|
65
|
+
"type": "streamable-http",
|
|
66
|
+
"url": "https://poc-backend.amdal-dev.workers.dev/mcp"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Then ask: *"Audit the dependencies in my package.json"* or *"What's the risk profile of vercel/ai?"*
|
|
73
|
+
|
|
74
|
+
## GitHub Action
|
|
75
|
+
|
|
76
|
+
Posts audit results directly on your PR:
|
|
77
|
+
```yaml
|
|
78
|
+
- uses: piiiico/proof-of-commitment@main
|
|
79
|
+
with:
|
|
80
|
+
fail-on-critical: false
|
|
81
|
+
comment-on-pr: true
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Links
|
|
85
|
+
|
|
86
|
+
- **Web demo:** https://getcommit.dev/audit
|
|
87
|
+
- **Live watchlist:** https://getcommit.dev/watchlist (top 25 npm packages by structural risk)
|
|
88
|
+
- **GitHub:** https://github.com/piiiico/proof-of-commitment
|
package/index.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* proof-of-commitment CLI
|
|
4
|
+
* Scores npm/PyPI packages on behavioral commitment signals.
|
|
5
|
+
* Usage: npx proof-of-commitment [packages...] [options]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const API = 'https://poc-backend.amdal-dev.workers.dev/api/audit';
|
|
9
|
+
const WEB = 'https://getcommit.dev/audit';
|
|
10
|
+
|
|
11
|
+
// ANSI color helpers
|
|
12
|
+
const c = {
|
|
13
|
+
reset: '\x1b[0m',
|
|
14
|
+
bold: '\x1b[1m',
|
|
15
|
+
dim: '\x1b[2m',
|
|
16
|
+
red: '\x1b[31m',
|
|
17
|
+
yellow: '\x1b[33m',
|
|
18
|
+
green: '\x1b[32m',
|
|
19
|
+
cyan: '\x1b[36m',
|
|
20
|
+
white: '\x1b[37m',
|
|
21
|
+
bgRed: '\x1b[41m',
|
|
22
|
+
bgYellow: '\x1b[43m',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const NO_COLOR = process.env.NO_COLOR || !process.stdout.isTTY;
|
|
26
|
+
const col = NO_COLOR ? () => '' : (code) => code;
|
|
27
|
+
|
|
28
|
+
function clr(code, text) {
|
|
29
|
+
if (NO_COLOR) return text;
|
|
30
|
+
return `${code}${text}${c.reset}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function riskColor(flags, score) {
|
|
34
|
+
if (flags && flags.includes('CRITICAL')) return c.red + c.bold;
|
|
35
|
+
if (score < 40) return c.yellow + c.bold;
|
|
36
|
+
if (score < 60) return c.yellow;
|
|
37
|
+
return c.green;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function riskLabel(flags, score) {
|
|
41
|
+
if (flags && flags.includes('CRITICAL')) return '🔴 CRITICAL';
|
|
42
|
+
if (score < 40) return '🟠 HIGH';
|
|
43
|
+
if (score < 60) return '🟡 MODERATE';
|
|
44
|
+
if (score < 75) return '🟡 GOOD';
|
|
45
|
+
return '🟢 HEALTHY';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fmtDownloads(n) {
|
|
49
|
+
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B/wk';
|
|
50
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M/wk';
|
|
51
|
+
if (n >= 1e3) return (n / 1e3).toFixed(0) + 'K/wk';
|
|
52
|
+
return n + '/wk';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function padEnd(str, len) {
|
|
56
|
+
const visible = str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
57
|
+
return str + ' '.repeat(Math.max(0, len - visible.length));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function printTable(results) {
|
|
61
|
+
const COL = {
|
|
62
|
+
name: 20, risk: 14, score: 7, maintainers: 12, downloads: 12, age: 8,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const header = [
|
|
66
|
+
padEnd(clr(c.bold, 'Package'), COL.name),
|
|
67
|
+
padEnd(clr(c.bold, 'Risk'), COL.risk),
|
|
68
|
+
padEnd(clr(c.bold, 'Score'), COL.score),
|
|
69
|
+
padEnd(clr(c.bold, 'Maintainers'), COL.maintainers),
|
|
70
|
+
padEnd(clr(c.bold, 'Downloads'), COL.downloads),
|
|
71
|
+
padEnd(clr(c.bold, 'Age'), COL.age),
|
|
72
|
+
].join(' ');
|
|
73
|
+
|
|
74
|
+
const divider = '─'.repeat(COL.name + COL.risk + COL.score + COL.maintainers + COL.downloads + COL.age + 10);
|
|
75
|
+
|
|
76
|
+
console.log('\n' + divider);
|
|
77
|
+
console.log(header);
|
|
78
|
+
console.log(divider);
|
|
79
|
+
|
|
80
|
+
let criticalCount = 0;
|
|
81
|
+
|
|
82
|
+
for (const pkg of results) {
|
|
83
|
+
const rc = riskColor(pkg.riskFlags, pkg.score);
|
|
84
|
+
const label = riskLabel(pkg.riskFlags, pkg.score);
|
|
85
|
+
if (pkg.riskFlags && pkg.riskFlags.includes('CRITICAL')) criticalCount++;
|
|
86
|
+
|
|
87
|
+
const row = [
|
|
88
|
+
padEnd(pkg.name, COL.name),
|
|
89
|
+
padEnd(clr(rc, label), COL.risk),
|
|
90
|
+
padEnd(String(pkg.score), COL.score),
|
|
91
|
+
padEnd(String(pkg.maintainers || '?'), COL.maintainers),
|
|
92
|
+
padEnd(fmtDownloads(pkg.weeklyDownloads || 0), COL.downloads),
|
|
93
|
+
padEnd((pkg.ageYears || '?').toString().replace(/(\.\d).*/, '$1') + 'y', COL.age),
|
|
94
|
+
].join(' ');
|
|
95
|
+
|
|
96
|
+
console.log(row);
|
|
97
|
+
|
|
98
|
+
// Score breakdown if available
|
|
99
|
+
if (pkg.scoreBreakdown) {
|
|
100
|
+
const b = pkg.scoreBreakdown;
|
|
101
|
+
const breakdown = clr(c.dim,
|
|
102
|
+
` └ longevity=${b.longevity} momentum=${b.downloadMomentum} ` +
|
|
103
|
+
`releases=${b.releaseConsistency} maintainers=${b.maintainerDepth} github=${b.githubBacking}`
|
|
104
|
+
);
|
|
105
|
+
console.log(breakdown);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log(divider);
|
|
110
|
+
|
|
111
|
+
if (criticalCount > 0) {
|
|
112
|
+
console.log('\n' + clr(c.red + c.bold, `⚠ ${criticalCount} CRITICAL package${criticalCount > 1 ? 's' : ''} found.`));
|
|
113
|
+
console.log(clr(c.dim, ' CRITICAL = sole maintainer + >10M weekly downloads (high-value attack target)'));
|
|
114
|
+
console.log(clr(c.dim, ` Full breakdown: ${WEB}?packages=${results.map(r => r.name).join(',')}`));
|
|
115
|
+
} else {
|
|
116
|
+
console.log('\n' + clr(c.green, '✓ No CRITICAL packages found.'));
|
|
117
|
+
}
|
|
118
|
+
console.log();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function printHelp() {
|
|
122
|
+
console.log(`
|
|
123
|
+
${clr(c.bold, 'proof-of-commitment')} — supply chain risk scorer
|
|
124
|
+
|
|
125
|
+
${clr(c.bold, 'Usage:')}
|
|
126
|
+
npx proof-of-commitment [packages...] Score npm packages
|
|
127
|
+
npx proof-of-commitment --pypi [pkgs...] Score PyPI packages
|
|
128
|
+
npx proof-of-commitment --file <path> Auto-detect from package.json / requirements.txt
|
|
129
|
+
|
|
130
|
+
${clr(c.bold, 'Examples:')}
|
|
131
|
+
npx proof-of-commitment axios zod chalk
|
|
132
|
+
npx proof-of-commitment --pypi litellm langchain requests
|
|
133
|
+
npx proof-of-commitment --file package.json
|
|
134
|
+
npx proof-of-commitment --file requirements.txt
|
|
135
|
+
|
|
136
|
+
${clr(c.bold, 'Score meaning:')}
|
|
137
|
+
🔴 CRITICAL Sole maintainer + >10M downloads/wk (high-value attack target)
|
|
138
|
+
🟠 HIGH Score < 40
|
|
139
|
+
🟡 MODERATE Score 40–59
|
|
140
|
+
🟡 GOOD Score 60–74
|
|
141
|
+
🟢 HEALTHY Score 75+
|
|
142
|
+
|
|
143
|
+
${clr(c.bold, 'Score dimensions:')} longevity · download momentum · release consistency · maintainer depth · GitHub backing
|
|
144
|
+
|
|
145
|
+
${clr(c.bold, 'Web:')} ${WEB}
|
|
146
|
+
${clr(c.bold, 'MCP:')} ${clr(c.dim, 'Add to Claude Desktop / Cursor for AI-assisted auditing')}
|
|
147
|
+
`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function readPackagesFromFile(filePath) {
|
|
151
|
+
const fs = await import('fs');
|
|
152
|
+
const path = await import('path');
|
|
153
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
154
|
+
const basename = path.basename(filePath).toLowerCase();
|
|
155
|
+
|
|
156
|
+
if (basename === 'package.json' || filePath.endsWith('.json')) {
|
|
157
|
+
const pkg = JSON.parse(content);
|
|
158
|
+
const deps = {
|
|
159
|
+
...pkg.dependencies,
|
|
160
|
+
...pkg.devDependencies,
|
|
161
|
+
};
|
|
162
|
+
return { packages: Object.keys(deps).slice(0, 20), ecosystem: 'npm' };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (basename === 'requirements.txt' || filePath.endsWith('.txt')) {
|
|
166
|
+
const pkgs = content
|
|
167
|
+
.split('\n')
|
|
168
|
+
.map(l => l.trim())
|
|
169
|
+
.filter(l => l && !l.startsWith('#'))
|
|
170
|
+
.map(l => l.replace(/[>=<!\s].*/,'').trim())
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.slice(0, 20);
|
|
173
|
+
return { packages: pkgs, ecosystem: 'pypi' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
throw new Error(`Unsupported file: ${filePath}. Use package.json or requirements.txt`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function main() {
|
|
180
|
+
const args = process.argv.slice(2);
|
|
181
|
+
|
|
182
|
+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
183
|
+
printHelp();
|
|
184
|
+
process.exit(0);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let ecosystem = 'npm';
|
|
188
|
+
let packages = [];
|
|
189
|
+
let filePath = null;
|
|
190
|
+
|
|
191
|
+
let i = 0;
|
|
192
|
+
while (i < args.length) {
|
|
193
|
+
const a = args[i];
|
|
194
|
+
if (a === '--pypi') { ecosystem = 'pypi'; i++; }
|
|
195
|
+
else if (a === '--npm') { ecosystem = 'npm'; i++; }
|
|
196
|
+
else if (a === '--file' || a === '-f') {
|
|
197
|
+
filePath = args[++i];
|
|
198
|
+
i++;
|
|
199
|
+
}
|
|
200
|
+
else if (a.startsWith('--')) {
|
|
201
|
+
console.error(`Unknown flag: ${a}`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
else { packages.push(a); i++; }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (filePath) {
|
|
208
|
+
try {
|
|
209
|
+
const result = await readPackagesFromFile(filePath);
|
|
210
|
+
packages = result.packages;
|
|
211
|
+
ecosystem = result.ecosystem;
|
|
212
|
+
console.log(clr(c.dim, `Detected ${packages.length} packages from ${filePath} (${ecosystem})`));
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.error(`Error reading ${filePath}: ${err.message}`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (packages.length === 0) {
|
|
220
|
+
console.error('No packages specified. Run with --help for usage.');
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (packages.length > 20) {
|
|
225
|
+
console.warn(clr(c.yellow, `Warning: truncating to first 20 packages (got ${packages.length})`));
|
|
226
|
+
packages = packages.slice(0, 20);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const pkgList = packages.join(', ');
|
|
230
|
+
process.stdout.write(clr(c.dim, `Scoring ${packages.length} ${ecosystem} package${packages.length > 1 ? 's' : ''}...`));
|
|
231
|
+
|
|
232
|
+
const t0 = Date.now();
|
|
233
|
+
let data;
|
|
234
|
+
try {
|
|
235
|
+
const res = await fetch(API, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: { 'Content-Type': 'application/json' },
|
|
238
|
+
body: JSON.stringify({ packages, ecosystem }),
|
|
239
|
+
});
|
|
240
|
+
if (!res.ok) {
|
|
241
|
+
const text = await res.text();
|
|
242
|
+
throw new Error(`API error ${res.status}: ${text}`);
|
|
243
|
+
}
|
|
244
|
+
data = await res.json();
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.error(`\nError: ${err.message}`);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
|
251
|
+
process.stdout.write(clr(c.dim, ` done in ${elapsed}s\n`));
|
|
252
|
+
|
|
253
|
+
if (!data.results || data.results.length === 0) {
|
|
254
|
+
console.log('No results returned. Check package names and try again.');
|
|
255
|
+
process.exit(0);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
printTable(data.results);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
main().catch(err => {
|
|
262
|
+
console.error('Unexpected error:', err.message);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "proof-of-commitment",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Supply chain risk scorer for npm and PyPI packages — behavioral signals that can't be faked",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"proof-of-commitment": "./index.js",
|
|
8
|
+
"poc": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"supply-chain",
|
|
17
|
+
"security",
|
|
18
|
+
"npm",
|
|
19
|
+
"pypi",
|
|
20
|
+
"dependencies",
|
|
21
|
+
"audit",
|
|
22
|
+
"risk",
|
|
23
|
+
"behavioral",
|
|
24
|
+
"commitment",
|
|
25
|
+
"maintainer"
|
|
26
|
+
],
|
|
27
|
+
"author": "piiiico",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/piiiico/proof-of-commitment"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://getcommit.dev/audit",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
}
|
|
37
|
+
}
|