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.
Files changed (2) hide show
  1. package/index.js +185 -11
  2. 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.25.1
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
- Free tier: 100 queries/IP/UTC day. Power users: API key for 200/day. ${clr(c.dim, '(Authorization: Bearer sk_commit_โ€ฆ)')}
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 (!jsonOutput) console.log(clr(c.dim, `Auto-detected manifest: ${detected}`));
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 (!jsonOutput) console.log(clr(c.dim, `Detected ${totalInFile} packages from ${filePath} (${ecosystem})`));
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) โ†’ 'critical' (preserves v1.7.x behavior).
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 || jsonOutput) failOn = 'critical';
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 (!jsonOutput) process.stdout.write(clr(c.dim, `Scoring ${packages.length} ${ecosystem} package${packages.length > 1 ? 's' : ''}...`));
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 (!jsonOutput) process.stdout.write(clr(c.dim, ` done in ${elapsed}s\n`));
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 (!jsonOutput) process.stdout.write(clr(c.dim, `Scanning ${packages.length} packages (${batches} batches in parallel)...`));
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 (!jsonOutput) process.stdout.write(clr(c.dim, ` done in ${elapsed}s\n`));
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 (jsonOutput) {
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.25.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",