proof-of-commitment 1.25.1 โ 1.27.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/index.js +185 -11
- package/package.json +4 -2
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.26.0
|
|
4
4
|
* Scores npm/PyPI/Cargo/Go packages on behavioral commitment signals.
|
|
5
5
|
* Usage: npx proof-of-commitment [packages...] [options]
|
|
6
6
|
*/
|
|
@@ -310,6 +310,157 @@ function riskLabel(flags, score) {
|
|
|
310
310
|
return '๐ข HEALTHY';
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Format audit results as SARIF 2.1.0 for GitHub Code Scanning / security dashboards.
|
|
315
|
+
*
|
|
316
|
+
* Maps risk levels: CRITICAL โ error, HIGH (score<40) โ warning,
|
|
317
|
+
* MODERATE (score<60) โ note. Each package produces one result entry.
|
|
318
|
+
* Compromised packages get a separate "compromised" rule.
|
|
319
|
+
*
|
|
320
|
+
* When --file was used, locations point to that file at line 1.
|
|
321
|
+
* Otherwise, a logical package-name location is used.
|
|
322
|
+
*/
|
|
323
|
+
function formatSarif(results, { filePath, ecosystem, version } = {}) {
|
|
324
|
+
const rules = [];
|
|
325
|
+
const ruleIndex = {};
|
|
326
|
+
|
|
327
|
+
function ensureRule(id, shortDescription, fullDescription, level) {
|
|
328
|
+
if (ruleIndex[id] != null) return ruleIndex[id];
|
|
329
|
+
const idx = rules.length;
|
|
330
|
+
ruleIndex[id] = idx;
|
|
331
|
+
rules.push({
|
|
332
|
+
id,
|
|
333
|
+
shortDescription: { text: shortDescription },
|
|
334
|
+
fullDescription: { text: fullDescription },
|
|
335
|
+
defaultConfiguration: { level },
|
|
336
|
+
helpUri: 'https://getcommit.dev/docs/',
|
|
337
|
+
});
|
|
338
|
+
return idx;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Pre-define rules
|
|
342
|
+
ensureRule(
|
|
343
|
+
'commit/critical',
|
|
344
|
+
'CRITICAL: sole publisher with high download volume',
|
|
345
|
+
'Package has a single npm/registry publisher controlling millions of weekly downloads โ the exact attack surface exploited in the axios and LiteLLM supply chain compromises.',
|
|
346
|
+
'error'
|
|
347
|
+
);
|
|
348
|
+
ensureRule(
|
|
349
|
+
'commit/high',
|
|
350
|
+
'HIGH: behavioral risk score below 40',
|
|
351
|
+
'Package scores below 40 on behavioral commitment signals, indicating elevated supply chain risk from low maintenance activity, publisher concentration, or rapid adoption without established track record.',
|
|
352
|
+
'warning'
|
|
353
|
+
);
|
|
354
|
+
ensureRule(
|
|
355
|
+
'commit/moderate',
|
|
356
|
+
'MODERATE: behavioral risk score below 60',
|
|
357
|
+
'Package scores below 60 on behavioral commitment signals. Not immediately dangerous but worth monitoring.',
|
|
358
|
+
'note'
|
|
359
|
+
);
|
|
360
|
+
ensureRule(
|
|
361
|
+
'commit/compromised',
|
|
362
|
+
'COMPROMISED: confirmed supply chain attack',
|
|
363
|
+
'Package was involved in a confirmed supply chain attack. Verify you are on a clean version.',
|
|
364
|
+
'error'
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const sarifResults = [];
|
|
368
|
+
|
|
369
|
+
for (const pkg of results) {
|
|
370
|
+
const isCritical = hasCritical(pkg.riskFlags);
|
|
371
|
+
const score = typeof pkg.score === 'number' ? pkg.score : null;
|
|
372
|
+
|
|
373
|
+
// Determine primary rule
|
|
374
|
+
let ruleId, level;
|
|
375
|
+
if (isCritical) {
|
|
376
|
+
ruleId = 'commit/critical';
|
|
377
|
+
level = 'error';
|
|
378
|
+
} else if (score !== null && score < 40) {
|
|
379
|
+
ruleId = 'commit/high';
|
|
380
|
+
level = 'warning';
|
|
381
|
+
} else if (score !== null && score < 60) {
|
|
382
|
+
ruleId = 'commit/moderate';
|
|
383
|
+
level = 'note';
|
|
384
|
+
} else {
|
|
385
|
+
// Healthy โ skip unless compromised
|
|
386
|
+
if (!pkg.compromised) continue;
|
|
387
|
+
ruleId = 'commit/compromised';
|
|
388
|
+
level = 'error';
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const dlStr = pkg.weeklyDownloads
|
|
392
|
+
? ` (${fmtDl(pkg.weeklyDownloads)} downloads/week)`
|
|
393
|
+
: '';
|
|
394
|
+
const pubStr = pkg.maintainers
|
|
395
|
+
? `, ${pkg.maintainers} publisher${pkg.maintainers > 1 ? 's' : ''}`
|
|
396
|
+
: '';
|
|
397
|
+
const scoreStr = score !== null ? `Score: ${score}/100` : '';
|
|
398
|
+
|
|
399
|
+
const messageText = `${pkg.name}: ${scoreStr}${pubStr}${dlStr}. ` +
|
|
400
|
+
`${isCritical ? 'Sole publisher with high download volume โ publish-access concentration risk.' : ''} ` +
|
|
401
|
+
`https://getcommit.dev/${pkg.ecosystem || ecosystem || 'npm'}/${encodeURIComponent(pkg.name)}`;
|
|
402
|
+
|
|
403
|
+
const location = filePath
|
|
404
|
+
? { physicalLocation: { artifactLocation: { uri: filePath }, region: { startLine: 1 } } }
|
|
405
|
+
: { logicalLocations: [{ name: pkg.name, kind: 'module' }] };
|
|
406
|
+
|
|
407
|
+
sarifResults.push({
|
|
408
|
+
ruleId,
|
|
409
|
+
ruleIndex: ruleIndex[ruleId],
|
|
410
|
+
level,
|
|
411
|
+
message: { text: messageText.trim() },
|
|
412
|
+
locations: [location],
|
|
413
|
+
properties: {
|
|
414
|
+
ecosystem: pkg.ecosystem || ecosystem || 'npm',
|
|
415
|
+
score: pkg.score,
|
|
416
|
+
maintainers: pkg.maintainers,
|
|
417
|
+
weeklyDownloads: pkg.weeklyDownloads,
|
|
418
|
+
ageYears: pkg.ageYears,
|
|
419
|
+
hasProvenance: pkg.hasProvenance || false,
|
|
420
|
+
riskFlags: pkg.riskFlags || [],
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Separate result for compromised packages
|
|
425
|
+
if (pkg.compromised && ruleId !== 'commit/compromised') {
|
|
426
|
+
const atk = pkg.compromised;
|
|
427
|
+
sarifResults.push({
|
|
428
|
+
ruleId: 'commit/compromised',
|
|
429
|
+
ruleIndex: ruleIndex['commit/compromised'],
|
|
430
|
+
level: 'error',
|
|
431
|
+
message: {
|
|
432
|
+
text: `${pkg.name}: confirmed supply chain attack โ ${atk.attack || 'unknown'} (${atk.date || '?'}). ${atk.url || ''}`,
|
|
433
|
+
},
|
|
434
|
+
locations: [location],
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
$schema: 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
441
|
+
version: '2.1.0',
|
|
442
|
+
runs: [{
|
|
443
|
+
tool: {
|
|
444
|
+
driver: {
|
|
445
|
+
name: 'Commit',
|
|
446
|
+
semanticVersion: version || '1.25.0',
|
|
447
|
+
informationUri: 'https://getcommit.dev',
|
|
448
|
+
rules,
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
results: sarifResults,
|
|
452
|
+
}],
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Short download formatter for SARIF messages (no /wk suffix)
|
|
457
|
+
function fmtDl(n) {
|
|
458
|
+
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
|
|
459
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
|
460
|
+
if (n >= 1e3) return (n / 1e3).toFixed(0) + 'K';
|
|
461
|
+
return String(n);
|
|
462
|
+
}
|
|
463
|
+
|
|
313
464
|
function fmtDownloads(n) {
|
|
314
465
|
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B/wk';
|
|
315
466
|
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M/wk';
|
|
@@ -632,6 +783,7 @@ ${clr(c.bold, 'Monitoring (free: 3 packages weekly ยท Developer $15/mo: 15 daily
|
|
|
632
783
|
|
|
633
784
|
${clr(c.bold, 'Options:')}
|
|
634
785
|
--json Output results as JSON
|
|
786
|
+
--sarif Output results as SARIF 2.1.0 (for GitHub Code Scanning)
|
|
635
787
|
--fail-on=<level> Exit 1 when findings meet the threshold. Levels:
|
|
636
788
|
critical any CRITICAL package (publish-access concentration)
|
|
637
789
|
risky any CRITICAL or HIGH (score < 40) package
|
|
@@ -661,6 +813,7 @@ ${clr(c.bold, 'Examples:')}
|
|
|
661
813
|
npx proof-of-commitment --file package-lock.json # scans ALL transitive deps
|
|
662
814
|
npx proof-of-commitment --file go.sum # scans full Go module graph
|
|
663
815
|
npx proof-of-commitment axios chalk --json | jq '.criticalCount'
|
|
816
|
+
npx proof-of-commitment --sarif > results.sarif # GitHub Code Scanning format
|
|
664
817
|
npx proof-of-commitment --fail-on=critical # CI-friendly hard gate
|
|
665
818
|
|
|
666
819
|
${clr(c.bold, 'CI integration (GitHub Actions):')}
|
|
@@ -693,7 +846,7 @@ ${clr(c.bold, 'Score dimensions (npm/PyPI/Cargo):')} longevity ยท download momen
|
|
|
693
846
|
${clr(c.bold, 'Score dimensions (Go):')} longevity ยท release consistency ยท maintainer depth ยท GitHub backing ยท stars
|
|
694
847
|
|
|
695
848
|
${clr(c.bold, 'MCP:')} https://poc-backend.amdal-dev.workers.dev/mcp โ connect from Claude Desktop / Cursor / Cline.
|
|
696
|
-
|
|
849
|
+
Anonymous: 15 queries/IP/UTC day. Free API key (instant, no card): 200/day. ${clr(c.dim, '(Authorization: Bearer sk_commit_โฆ)')}
|
|
697
850
|
|
|
698
851
|
${clr(c.bold, 'Web:')} ${WEB}
|
|
699
852
|
`);
|
|
@@ -2319,8 +2472,11 @@ async function main() {
|
|
|
2319
2472
|
let isLockfile = false;
|
|
2320
2473
|
let totalInFile = 0;
|
|
2321
2474
|
let jsonOutput = false;
|
|
2475
|
+
let sarifOutput = false;
|
|
2322
2476
|
// null means "default later" โ depends on output mode and CI env.
|
|
2323
2477
|
let failOn = null;
|
|
2478
|
+
// Set after arg-parse: true when JSON or SARIF suppresses interactive output.
|
|
2479
|
+
let structuredOutput = false;
|
|
2324
2480
|
|
|
2325
2481
|
let i = 0;
|
|
2326
2482
|
while (i < args.length) {
|
|
@@ -2330,6 +2486,7 @@ async function main() {
|
|
|
2330
2486
|
else if (a === '--cargo') { ecosystem = 'cargo'; i++; }
|
|
2331
2487
|
else if (a === '--golang' || a === '--go') { ecosystem = 'golang'; i++; }
|
|
2332
2488
|
else if (a === '--json') { jsonOutput = true; i++; }
|
|
2489
|
+
else if (a === '--sarif') { sarifOutput = true; i++; }
|
|
2333
2490
|
else if (a.startsWith('--fail-on=')) {
|
|
2334
2491
|
try { failOn = parseFailOn(a.slice('--fail-on='.length)); }
|
|
2335
2492
|
catch (err) { console.error(err.message); process.exit(2); }
|
|
@@ -2351,12 +2508,14 @@ async function main() {
|
|
|
2351
2508
|
else { packages.push(a); i++; }
|
|
2352
2509
|
}
|
|
2353
2510
|
|
|
2511
|
+
structuredOutput = jsonOutput || sarifOutput;
|
|
2512
|
+
|
|
2354
2513
|
// Zero-arg auto-detect: if no positional packages and no --file, look for a manifest in cwd.
|
|
2355
2514
|
if (!filePath && packages.length === 0) {
|
|
2356
2515
|
const detected = await autodetectManifest(process.cwd());
|
|
2357
2516
|
if (detected) {
|
|
2358
2517
|
filePath = detected;
|
|
2359
|
-
if (!
|
|
2518
|
+
if (!structuredOutput) console.log(clr(c.dim, `Auto-detected manifest: ${detected}`));
|
|
2360
2519
|
} else {
|
|
2361
2520
|
// No positional packages, no --file, and no manifest in cwd โ print help.
|
|
2362
2521
|
// This preserves the prior "bare invocation" UX rather than failing silently.
|
|
@@ -2372,7 +2531,7 @@ async function main() {
|
|
|
2372
2531
|
ecosystem = result.ecosystem;
|
|
2373
2532
|
isLockfile = result.lockfile || false;
|
|
2374
2533
|
totalInFile = result.totalInFile || packages.length;
|
|
2375
|
-
if (!
|
|
2534
|
+
if (!structuredOutput) console.log(clr(c.dim, `Detected ${totalInFile} packages from ${filePath} (${ecosystem})`));
|
|
2376
2535
|
} catch (err) {
|
|
2377
2536
|
console.error(`Error reading ${filePath}: ${err.message}`);
|
|
2378
2537
|
process.exit(1);
|
|
@@ -2387,12 +2546,12 @@ async function main() {
|
|
|
2387
2546
|
// Resolve fail-on default.
|
|
2388
2547
|
// - User passed --fail-on=X โ use X (already set).
|
|
2389
2548
|
// - CI env (CI=true or =1) โ 'critical' (hard gate by default in CI).
|
|
2390
|
-
// - --json output (no CI)
|
|
2549
|
+
// - --json/--sarif output (no CI) โ 'critical' (machine-readable = CI-like).
|
|
2391
2550
|
// - interactive table output โ 'none' (backward-compatible for casual users).
|
|
2392
2551
|
if (failOn === null) {
|
|
2393
2552
|
const ciEnv = process.env.CI;
|
|
2394
2553
|
const inCI = ciEnv === 'true' || ciEnv === '1';
|
|
2395
|
-
if (inCI ||
|
|
2554
|
+
if (inCI || structuredOutput) failOn = 'critical';
|
|
2396
2555
|
else failOn = 'none';
|
|
2397
2556
|
}
|
|
2398
2557
|
|
|
@@ -2402,7 +2561,7 @@ async function main() {
|
|
|
2402
2561
|
let apiCta = null;
|
|
2403
2562
|
|
|
2404
2563
|
if (packages.length <= 20) {
|
|
2405
|
-
if (!
|
|
2564
|
+
if (!structuredOutput) process.stdout.write(clr(c.dim, `Scoring ${packages.length} ${ecosystem} package${packages.length > 1 ? 's' : ''}...`));
|
|
2406
2565
|
|
|
2407
2566
|
try {
|
|
2408
2567
|
const res = await fetch(API, {
|
|
@@ -2424,11 +2583,11 @@ async function main() {
|
|
|
2424
2583
|
}
|
|
2425
2584
|
|
|
2426
2585
|
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
|
2427
|
-
if (!
|
|
2586
|
+
if (!structuredOutput) process.stdout.write(clr(c.dim, ` done in ${elapsed}s\n`));
|
|
2428
2587
|
|
|
2429
2588
|
} else {
|
|
2430
2589
|
const batches = Math.ceil(packages.length / 20);
|
|
2431
|
-
if (!
|
|
2590
|
+
if (!structuredOutput) process.stdout.write(clr(c.dim, `Scanning ${packages.length} packages (${batches} batches in parallel)...`));
|
|
2432
2591
|
|
|
2433
2592
|
let lastPct = 0;
|
|
2434
2593
|
try {
|
|
@@ -2449,7 +2608,13 @@ async function main() {
|
|
|
2449
2608
|
}
|
|
2450
2609
|
|
|
2451
2610
|
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
|
2452
|
-
if (!
|
|
2611
|
+
if (!structuredOutput) process.stdout.write(clr(c.dim, ` done in ${elapsed}s\n`));
|
|
2612
|
+
|
|
2613
|
+
if (sarifOutput) {
|
|
2614
|
+
const sarif = formatSarif(allResults, { filePath, ecosystem, version: '1.26.0' });
|
|
2615
|
+
console.log(JSON.stringify(sarif, null, 2));
|
|
2616
|
+
process.exit(shouldFail(allResults, failOn) ? 1 : 0);
|
|
2617
|
+
}
|
|
2453
2618
|
|
|
2454
2619
|
if (jsonOutput) {
|
|
2455
2620
|
const criticalCount = allResults.filter(r => hasCritical(r.riskFlags)).length;
|
|
@@ -2479,7 +2644,10 @@ async function main() {
|
|
|
2479
2644
|
}
|
|
2480
2645
|
|
|
2481
2646
|
if (!allResults || allResults.length === 0) {
|
|
2482
|
-
if (
|
|
2647
|
+
if (sarifOutput) {
|
|
2648
|
+
const sarif = formatSarif([], { filePath, ecosystem, version: '1.26.0' });
|
|
2649
|
+
console.log(JSON.stringify(sarif, null, 2));
|
|
2650
|
+
} else if (jsonOutput) {
|
|
2483
2651
|
console.log(JSON.stringify({ totalScanned: 0, criticalCount: 0, provenanceCount: 0, failOn, results: [] }, null, 2));
|
|
2484
2652
|
} else {
|
|
2485
2653
|
console.log('No results returned. Check package names and try again.');
|
|
@@ -2487,6 +2655,12 @@ async function main() {
|
|
|
2487
2655
|
process.exit(0);
|
|
2488
2656
|
}
|
|
2489
2657
|
|
|
2658
|
+
if (sarifOutput) {
|
|
2659
|
+
const sarif = formatSarif(allResults, { filePath, ecosystem, version: '1.26.0' });
|
|
2660
|
+
console.log(JSON.stringify(sarif, null, 2));
|
|
2661
|
+
process.exit(shouldFail(allResults, failOn) ? 1 : 0);
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2490
2664
|
if (jsonOutput) {
|
|
2491
2665
|
const criticalCount = allResults.filter(r => hasCritical(r.riskFlags)).length;
|
|
2492
2666
|
const provenanceCount = allResults.filter(r => r.hasProvenance).length;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proof-of-commitment",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
4
4
|
"mcpName": "io.github.piiiico/proof-of-commitment",
|
|
5
5
|
"description": "Supply chain security risk scorer for npm, PyPI, Cargo, and Go packages โ behavioral signals that can't be faked",
|
|
6
6
|
"type": "module",
|
|
@@ -42,7 +42,9 @@
|
|
|
42
42
|
"dependency-audit",
|
|
43
43
|
"lockfile",
|
|
44
44
|
"devsecops",
|
|
45
|
-
"ci"
|
|
45
|
+
"ci",
|
|
46
|
+
"sarif",
|
|
47
|
+
"code-scanning"
|
|
46
48
|
],
|
|
47
49
|
"author": "piiiico",
|
|
48
50
|
"license": "MIT",
|