proof-of-commitment 1.20.1 → 1.21.1
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/index.js +234 -2
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* proof-of-commitment CLI v1.
|
|
3
|
+
* proof-of-commitment CLI v1.21.1
|
|
4
4
|
* Scores npm/PyPI/Cargo/Go packages on behavioral commitment signals.
|
|
5
5
|
* Usage: npx proof-of-commitment [packages...] [options]
|
|
6
6
|
*/
|
|
@@ -348,6 +348,7 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
|
|
|
348
348
|
console.log(clr(c.cyan, `\n 🔗 Full report: ${WEB}?packages=${encodeURIComponent(topPkgs)}&${utm}`));
|
|
349
349
|
console.log(clr(c.cyan, ` 🤖 GitHub Action: github.com/piiiico/commit-action — block CRITICAL packages in CI`));
|
|
350
350
|
console.log(clr(c.dim, ` 📋 Add to this project: `) + clr(c.cyan, `poc init`) + clr(c.dim, ` — creates workflow + README badge`));
|
|
351
|
+
console.log(clr(c.dim, ` 🛡️ Protect every install: `) + clr(c.cyan, `poc hook`) + clr(c.dim, ` — Cursor hook, blocks CRITICAL before npm/pip/cargo runs`));
|
|
351
352
|
|
|
352
353
|
// Per-package profile URLs — drive traffic to permanent, indexable pages
|
|
353
354
|
const ecoPath = { npm: 'npm', pypi: 'pypi', cargo: 'cargo', golang: 'go' };
|
|
@@ -478,7 +479,7 @@ async function inlineSignup(results) {
|
|
|
478
479
|
|
|
479
480
|
function printHelp() {
|
|
480
481
|
console.log(`
|
|
481
|
-
${clr(c.bold, 'proof-of-commitment')} v1.
|
|
482
|
+
${clr(c.bold, 'proof-of-commitment')} v1.21.1 — supply chain risk scorer
|
|
482
483
|
|
|
483
484
|
${clr(c.bold, 'Usage:')}
|
|
484
485
|
npx proof-of-commitment Auto-detect manifest in current dir
|
|
@@ -504,6 +505,11 @@ ${clr(c.bold, 'Reports:')}
|
|
|
504
505
|
poc report [pkgs] Same flags as scan — packages, --pypi, --cargo, --file, etc.
|
|
505
506
|
Saves audit-report.html to cwd + prints Markdown for GitHub issues
|
|
506
507
|
|
|
508
|
+
${clr(c.bold, 'IDE Hooks:')}
|
|
509
|
+
poc hook Install Cursor beforeShellExecution hook (blocks CRITICAL packages)
|
|
510
|
+
poc hook --global Install for all Cursor projects (~/.cursor/hooks.json)
|
|
511
|
+
poc hook --uninstall Remove the hook
|
|
512
|
+
|
|
507
513
|
${clr(c.bold, 'Account:')}
|
|
508
514
|
poc login [key] Save and validate your API key (interactive or direct)
|
|
509
515
|
poc status Show current tier, usage, and limits
|
|
@@ -1125,6 +1131,227 @@ async function cmdLogout() {
|
|
|
1125
1131
|
console.log();
|
|
1126
1132
|
}
|
|
1127
1133
|
|
|
1134
|
+
/**
|
|
1135
|
+
* poc hook [--cursor] [--global] [--uninstall]
|
|
1136
|
+
* Install a beforeShellExecution hook for Cursor that scores packages before install.
|
|
1137
|
+
* Writes a standalone Node.js hook script to ~/.commit/cursor-hook.js and
|
|
1138
|
+
* configures .cursor/hooks.json (project) or ~/.cursor/hooks.json (--global).
|
|
1139
|
+
*/
|
|
1140
|
+
async function cmdHook(args) {
|
|
1141
|
+
const os = await import('os');
|
|
1142
|
+
const fs = await import('fs');
|
|
1143
|
+
const path = await import('path');
|
|
1144
|
+
|
|
1145
|
+
const isGlobal = args.includes('--global') || args.includes('-g');
|
|
1146
|
+
const uninstall = args.includes('--uninstall') || args.includes('--remove');
|
|
1147
|
+
|
|
1148
|
+
// ── Hook script (plain Node.js, no external deps) ─────────────────────
|
|
1149
|
+
const hookScript = `#!/usr/bin/env node
|
|
1150
|
+
/**
|
|
1151
|
+
* Commit supply chain hook for Cursor (auto-generated by \`poc hook\`)
|
|
1152
|
+
* Intercepts npm/pip/cargo/go install commands and scores packages
|
|
1153
|
+
* against getcommit.dev before they run.
|
|
1154
|
+
*
|
|
1155
|
+
* CRITICAL packages are blocked. HIGH packages trigger confirmation.
|
|
1156
|
+
* Docs: https://getcommit.dev/docs/cursor-hook
|
|
1157
|
+
*/
|
|
1158
|
+
const API = 'https://poc-backend.amdal-dev.workers.dev/api/audit';
|
|
1159
|
+
const fs = require('fs');
|
|
1160
|
+
const path = require('path');
|
|
1161
|
+
|
|
1162
|
+
function readKey() {
|
|
1163
|
+
try {
|
|
1164
|
+
if (process.env.COMMIT_API_KEY) return process.env.COMMIT_API_KEY.trim();
|
|
1165
|
+
const cfg = fs.readFileSync(path.join(require('os').homedir(), '.commit', 'config'), 'utf-8');
|
|
1166
|
+
const m = cfg.match(/^api_key\\s*=\\s*(.+)$/m);
|
|
1167
|
+
return m ? m[1].trim() : '';
|
|
1168
|
+
} catch { return ''; }
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function parseInstall(cmd) {
|
|
1172
|
+
const t = cmd.trim();
|
|
1173
|
+
let m;
|
|
1174
|
+
// npm / pnpm / yarn
|
|
1175
|
+
m = t.match(/^(?:npm\\s+(?:i|install|add)|pnpm\\s+(?:i|install|add)|yarn\\s+add)\\s+(.+)/);
|
|
1176
|
+
if (m) return { eco: 'npm', pkgs: m[1].split(/\\s+/).filter(a => !a.startsWith('-') && a !== 'install' && a !== 'add') };
|
|
1177
|
+
// pip
|
|
1178
|
+
m = t.match(/^(?:pip3?\\s+install|uv\\s+pip\\s+install|python3?\\s+-m\\s+pip\\s+install)\\s+(.+)/);
|
|
1179
|
+
if (m) return { eco: 'pypi', pkgs: m[1].split(/\\s+/).filter(a => !a.startsWith('-')).map(a => a.split('==')[0].split('>=')[0]) };
|
|
1180
|
+
// cargo
|
|
1181
|
+
m = t.match(/^cargo\\s+(?:add|install)\\s+(.+)/);
|
|
1182
|
+
if (m) return { eco: 'cargo', pkgs: m[1].split(/\\s+/).filter(a => !a.startsWith('-')) };
|
|
1183
|
+
// go
|
|
1184
|
+
m = t.match(/^go\\s+(?:get|install)\\s+(.+)/);
|
|
1185
|
+
if (m) return { eco: 'golang', pkgs: m[1].split(/\\s+/).filter(a => !a.startsWith('-')).map(a => a.split('@')[0]) };
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
async function main() {
|
|
1190
|
+
let input;
|
|
1191
|
+
try { input = JSON.parse(fs.readFileSync('/dev/stdin', 'utf-8')); } catch { process.stdout.write(JSON.stringify({ permission: 'allow' })); return; }
|
|
1192
|
+
const parsed = parseInstall(input.command || '');
|
|
1193
|
+
if (!parsed || parsed.pkgs.length === 0) { process.stdout.write(JSON.stringify({ permission: 'allow' })); return; }
|
|
1194
|
+
|
|
1195
|
+
const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' };
|
|
1196
|
+
const key = readKey();
|
|
1197
|
+
if (key) headers['Authorization'] = 'Bearer ' + key;
|
|
1198
|
+
|
|
1199
|
+
try {
|
|
1200
|
+
const ctrl = new AbortController();
|
|
1201
|
+
const timer = setTimeout(() => ctrl.abort(), 4000);
|
|
1202
|
+
const res = await fetch(API, { method: 'POST', headers, body: JSON.stringify({ packages: parsed.pkgs, ecosystem: parsed.eco }), signal: ctrl.signal });
|
|
1203
|
+
clearTimeout(timer);
|
|
1204
|
+
if (!res.ok && res.status !== 429) { process.stdout.write(JSON.stringify({ permission: 'allow' })); return; }
|
|
1205
|
+
const data = await res.json();
|
|
1206
|
+
const results = data.results || data.packages_already_scored || [];
|
|
1207
|
+
|
|
1208
|
+
const critical = results.filter(r => (r.riskFlags || []).some(f => f.startsWith('CRITICAL')));
|
|
1209
|
+
const high = results.filter(r => (r.riskFlags || []).some(f => f.startsWith('HIGH')));
|
|
1210
|
+
const url = 'https://getcommit.dev/audit?packages=' + parsed.pkgs.join(',') + '&ecosystem=' + parsed.eco;
|
|
1211
|
+
|
|
1212
|
+
// v1.21.1: detect rate-limit hit and surface signup CTA + unscored-package warning.
|
|
1213
|
+
// Without this, hook silently allowed unscored packages on 429 (false sense of security)
|
|
1214
|
+
// and the conversion driver (signup URL in 429 body) never reached the user.
|
|
1215
|
+
const rateLimited = res.status === 429;
|
|
1216
|
+
// Force cursor-hook attribution — backend default is audit-cli-429 which misattributes.
|
|
1217
|
+
const rlUrl = rateLimited ? 'https://getcommit.dev/get-started?ref=cursor-hook-429&utm_source=cli' : '';
|
|
1218
|
+
const unscored = rateLimited ? Math.max(0, parsed.pkgs.length - results.length) : 0;
|
|
1219
|
+
const rlNote = rateLimited
|
|
1220
|
+
? '\\n\\n\\u26A0 Commit free limit reached'
|
|
1221
|
+
+ (unscored > 0 ? ' \\u2014 ' + unscored + ' of ' + parsed.pkgs.length + ' package(s) NOT audited' : '')
|
|
1222
|
+
+ '\\n Free key (200/day, no card): ' + rlUrl
|
|
1223
|
+
: '';
|
|
1224
|
+
|
|
1225
|
+
if (critical.length > 0) {
|
|
1226
|
+
const lines = critical.map(r => ' \\u{1F534} ' + r.name + ' (score ' + (r.score||'?') + ') \\u2014 ' + (r.riskFlags||[]).slice(0,1).join(', '));
|
|
1227
|
+
process.stdout.write(JSON.stringify({
|
|
1228
|
+
permission: 'deny',
|
|
1229
|
+
user_message: '\\u{1F534} Commit blocked: ' + critical.map(r=>r.name).join(', ') + ' flagged CRITICAL\\n\\n' + lines.join('\\n') + '\\n\\n\\u2192 ' + url + rlNote,
|
|
1230
|
+
agent_message: 'Package install blocked by Commit. CRITICAL = sole publisher + high downloads (attack surface of axios/node-ipc incidents). ' + critical.map(r=>r.name).join(', ') + '. Report: ' + url,
|
|
1231
|
+
}));
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
if (high.length > 0) {
|
|
1235
|
+
const lines = high.map(r => ' \\u{1F7E1} ' + r.name + ' (score ' + (r.score||'?') + ') \\u2014 ' + (r.riskFlags||[]).slice(0,1).join(', '));
|
|
1236
|
+
process.stdout.write(JSON.stringify({
|
|
1237
|
+
permission: 'ask',
|
|
1238
|
+
user_message: '\\u{1F7E1} Commit: ' + high.map(r=>r.name).join(', ') + ' scored HIGH risk\\n\\n' + lines.join('\\n') + '\\n\\nProceed? \\u2192 ' + url + rlNote,
|
|
1239
|
+
}));
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
// Rate-limited with no critical/high in the scored partial: still alert user.
|
|
1243
|
+
// If unscored packages remain, this is a security signal (could be CRITICAL we missed).
|
|
1244
|
+
// If all packages scored clean, this is a conversion signal (drive them to sign up).
|
|
1245
|
+
if (rateLimited) {
|
|
1246
|
+
const head = unscored > 0
|
|
1247
|
+
? '\\u26A0 Commit free limit reached \\u2014 ' + unscored + ' of ' + parsed.pkgs.length + ' package(s) NOT audited'
|
|
1248
|
+
: '\\u2713 ' + parsed.pkgs.join(', ') + ' look clean (free-tier audit)';
|
|
1249
|
+
process.stdout.write(JSON.stringify({
|
|
1250
|
+
permission: 'ask',
|
|
1251
|
+
user_message: head + '\\n\\nFree API key (200/day, no card, 30s):\\n ' + rlUrl + '\\n\\nProceed anyway?',
|
|
1252
|
+
}));
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
process.stdout.write(JSON.stringify({ permission: 'allow' }));
|
|
1256
|
+
} catch { process.stdout.write(JSON.stringify({ permission: 'allow' })); }
|
|
1257
|
+
}
|
|
1258
|
+
main();
|
|
1259
|
+
`;
|
|
1260
|
+
|
|
1261
|
+
const commitDir = path.join(os.homedir(), '.commit');
|
|
1262
|
+
const hookPath = path.join(commitDir, 'cursor-hook.js');
|
|
1263
|
+
|
|
1264
|
+
// ── Uninstall ──────────────────────────────────────────────────────────
|
|
1265
|
+
if (uninstall) {
|
|
1266
|
+
let removed = false;
|
|
1267
|
+
if (fs.existsSync(hookPath)) {
|
|
1268
|
+
fs.unlinkSync(hookPath);
|
|
1269
|
+
removed = true;
|
|
1270
|
+
}
|
|
1271
|
+
// Remove from hooks.json
|
|
1272
|
+
for (const hooksDir of [
|
|
1273
|
+
path.join(process.cwd(), '.cursor'),
|
|
1274
|
+
path.join(os.homedir(), '.cursor'),
|
|
1275
|
+
]) {
|
|
1276
|
+
const hooksFile = path.join(hooksDir, 'hooks.json');
|
|
1277
|
+
if (fs.existsSync(hooksFile)) {
|
|
1278
|
+
try {
|
|
1279
|
+
const cfg = JSON.parse(fs.readFileSync(hooksFile, 'utf-8'));
|
|
1280
|
+
const hooks = cfg.hooks?.beforeShellExecution || [];
|
|
1281
|
+
const filtered = hooks.filter(h => !h.command?.includes('cursor-hook.js'));
|
|
1282
|
+
if (filtered.length !== hooks.length) {
|
|
1283
|
+
cfg.hooks.beforeShellExecution = filtered;
|
|
1284
|
+
fs.writeFileSync(hooksFile, JSON.stringify(cfg, null, 2) + '\n');
|
|
1285
|
+
removed = true;
|
|
1286
|
+
console.log(clr(c.dim, ` Updated: ${hooksFile}`));
|
|
1287
|
+
}
|
|
1288
|
+
} catch {}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
if (removed) {
|
|
1292
|
+
console.log(clr(c.green, '\n ✓ Cursor hook uninstalled.'));
|
|
1293
|
+
} else {
|
|
1294
|
+
console.log(clr(c.dim, '\n No hook found to remove.'));
|
|
1295
|
+
}
|
|
1296
|
+
console.log();
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// ── Install ────────────────────────────────────────────────────────────
|
|
1301
|
+
// 1. Write hook script
|
|
1302
|
+
if (!fs.existsSync(commitDir)) fs.mkdirSync(commitDir, { recursive: true });
|
|
1303
|
+
fs.writeFileSync(hookPath, hookScript, { mode: 0o755 });
|
|
1304
|
+
|
|
1305
|
+
// 2. Configure hooks.json
|
|
1306
|
+
const hooksDir = isGlobal
|
|
1307
|
+
? path.join(os.homedir(), '.cursor')
|
|
1308
|
+
: path.join(process.cwd(), '.cursor');
|
|
1309
|
+
if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true });
|
|
1310
|
+
|
|
1311
|
+
const hooksFile = path.join(hooksDir, 'hooks.json');
|
|
1312
|
+
let cfg = { version: 1, hooks: {} };
|
|
1313
|
+
if (fs.existsSync(hooksFile)) {
|
|
1314
|
+
try { cfg = JSON.parse(fs.readFileSync(hooksFile, 'utf-8')); } catch {}
|
|
1315
|
+
}
|
|
1316
|
+
if (!cfg.hooks) cfg.hooks = {};
|
|
1317
|
+
if (!cfg.hooks.beforeShellExecution) cfg.hooks.beforeShellExecution = [];
|
|
1318
|
+
|
|
1319
|
+
// Avoid duplicates
|
|
1320
|
+
const existing = cfg.hooks.beforeShellExecution.some(h => h.command?.includes('cursor-hook.js'));
|
|
1321
|
+
if (!existing) {
|
|
1322
|
+
cfg.hooks.beforeShellExecution.push({
|
|
1323
|
+
command: `node ${hookPath}`,
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
fs.writeFileSync(hooksFile, JSON.stringify(cfg, null, 2) + '\n');
|
|
1327
|
+
|
|
1328
|
+
// 3. Report
|
|
1329
|
+
console.log(clr(c.green, '\n ✓ Cursor supply chain hook installed'));
|
|
1330
|
+
console.log();
|
|
1331
|
+
console.log(clr(c.bold, ' What happens now:'));
|
|
1332
|
+
console.log(clr(c.dim, ' Every ') + clr(c.cyan, 'npm install') + clr(c.dim, ', ') +
|
|
1333
|
+
clr(c.cyan, 'pip install') + clr(c.dim, ', ') + clr(c.cyan, 'cargo add') + clr(c.dim, ', and ') +
|
|
1334
|
+
clr(c.cyan, 'go get') + clr(c.dim, ' in Cursor'));
|
|
1335
|
+
console.log(clr(c.dim, ' is scored against Commit before it runs.'));
|
|
1336
|
+
console.log(clr(c.dim, ' CRITICAL packages are blocked. HIGH packages ask for confirmation.'));
|
|
1337
|
+
console.log();
|
|
1338
|
+
console.log(clr(c.bold, ' Files:'));
|
|
1339
|
+
console.log(clr(c.dim, ` Hook script: ${hookPath}`));
|
|
1340
|
+
console.log(clr(c.dim, ` Config: ${hooksFile}`));
|
|
1341
|
+
console.log();
|
|
1342
|
+
|
|
1343
|
+
const key = await readApiKey();
|
|
1344
|
+
if (!key) {
|
|
1345
|
+
console.log(clr(c.yellow, ' ⚠ No API key found.') + clr(c.dim, ' Anonymous limit: 15 audits/day.'));
|
|
1346
|
+
console.log(clr(c.dim, ' Get a free key (200/day): ') + clr(c.cyan, 'poc login'));
|
|
1347
|
+
console.log(clr(c.dim, ' Or: ') + clr(c.cyan, 'https://getcommit.dev/get-started?ref=cursor-hook&utm_source=cli'));
|
|
1348
|
+
console.log();
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
console.log(clr(c.dim, ' Uninstall: ') + clr(c.cyan, 'poc hook --uninstall'));
|
|
1352
|
+
console.log();
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1128
1355
|
function tierLabel(tier) {
|
|
1129
1356
|
if (tier === 'pro') return clr(c.cyan + c.bold, 'Pro');
|
|
1130
1357
|
if (tier === 'enterprise') return clr(c.magenta + c.bold, 'Enterprise');
|
|
@@ -1723,6 +1950,11 @@ async function main() {
|
|
|
1723
1950
|
process.exit(0);
|
|
1724
1951
|
}
|
|
1725
1952
|
|
|
1953
|
+
if (subcmd === 'hook') {
|
|
1954
|
+
await cmdHook(args.slice(1));
|
|
1955
|
+
process.exit(0);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1726
1958
|
if (subcmd === 'report') {
|
|
1727
1959
|
// Parse report args (same flags as main scan)
|
|
1728
1960
|
const reportArgs = args.slice(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proof-of-commitment",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.1",
|
|
4
4
|
"mcpName": "io.github.piiiico/proof-of-commitment",
|
|
5
5
|
"description": "Supply chain risk scorer for npm, PyPI, Cargo, and Go packages — behavioral signals that can't be faked",
|
|
6
6
|
"type": "module",
|