proof-of-commitment 1.7.0 → 1.8.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 +158 -16
  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.7.0
3
+ * proof-of-commitment CLI v1.8.0
4
4
  * Scores npm/PyPI/Cargo/Go packages on behavioral commitment signals.
5
5
  * Usage: npx proof-of-commitment [packages...] [options]
6
6
  */
@@ -175,9 +175,10 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
175
175
 
176
176
  function printHelp() {
177
177
  console.log(`
178
- ${clr(c.bold, 'proof-of-commitment')} v1.7.0 — supply chain risk scorer
178
+ ${clr(c.bold, 'proof-of-commitment')} v1.8.0 — supply chain risk scorer
179
179
 
180
180
  ${clr(c.bold, 'Usage:')}
181
+ npx proof-of-commitment Auto-detect manifest in current dir
181
182
  npx proof-of-commitment [packages...] Score npm packages
182
183
  npx proof-of-commitment --pypi [pkgs...] Score PyPI packages
183
184
  npx proof-of-commitment --cargo [crates...] Score Rust crates
@@ -192,20 +193,52 @@ ${clr(c.bold, 'Usage:')}
192
193
  npx proof-of-commitment --file go.sum Audit Go full transitive set
193
194
 
194
195
  ${clr(c.bold, 'Options:')}
195
- --json Output results as JSON (exits 1 if any CRITICAL found — useful in CI)
196
- --pypi Score PyPI packages instead of npm
197
- --cargo Score Rust crates from crates.io
198
- --golang Score Go modules from proxy.golang.org (use full path: github.com/owner/repo)
199
- --file, -f Read packages from package.json, lock file, requirements.txt, Cargo.toml, or go.mod/go.sum
196
+ --json Output results as JSON
197
+ --fail-on=<level> Exit 1 when findings meet the threshold. Levels:
198
+ critical any CRITICAL package (publish-access concentration)
199
+ risky any CRITICAL or HIGH (score < 40) package
200
+ none always exit 0
201
+ Defaults: 'critical' in CI (env CI=true) and for --json output;
202
+ 'none' for interactive table output (backward-compatible).
203
+ --pypi Score PyPI packages instead of npm
204
+ --cargo Score Rust crates from crates.io
205
+ --golang Score Go modules from proxy.golang.org (use full path: github.com/owner/repo)
206
+ --file, -f Read packages from package.json, lock file, requirements.txt, Cargo.toml, or go.mod/go.sum
207
+
208
+ ${clr(c.bold, 'Auto-detect (no args):')}
209
+ Running 'npx proof-of-commitment' with no arguments scans the most-recently-modified
210
+ manifest in the current directory. Detection order (highest transitive coverage first):
211
+ npm: package-lock.json · yarn.lock · pnpm-lock.yaml · pnpm-workspace.yaml · package.json
212
+ pypi: requirements.txt
213
+ cargo: Cargo.toml
214
+ golang: go.sum · go.mod
215
+ When multiple ecosystems are present, the file with the most recent mtime wins.
200
216
 
201
217
  ${clr(c.bold, 'Examples:')}
218
+ npx proof-of-commitment # scans cwd manifest
202
219
  npx proof-of-commitment axios zod chalk
203
220
  npx proof-of-commitment --pypi litellm langchain requests
204
221
  npx proof-of-commitment --cargo serde tokio reqwest
205
222
  npx proof-of-commitment --golang github.com/gin-gonic/gin golang.org/x/net
206
- npx proof-of-commitment --file package-lock.json # scans ALL transitive deps
207
- npx proof-of-commitment --file go.sum # scans full Go module graph
223
+ npx proof-of-commitment --file package-lock.json # scans ALL transitive deps
224
+ npx proof-of-commitment --file go.sum # scans full Go module graph
208
225
  npx proof-of-commitment axios chalk --json | jq '.criticalCount'
226
+ npx proof-of-commitment --fail-on=critical # CI-friendly hard gate
227
+
228
+ ${clr(c.bold, 'CI integration (GitHub Actions):')}
229
+ # .github/workflows/supply-chain.yml
230
+ jobs:
231
+ audit:
232
+ runs-on: ubuntu-latest
233
+ steps:
234
+ - uses: actions/checkout@v4
235
+ - uses: actions/setup-node@v4
236
+ with: { node-version: '20' }
237
+ - run: npx -y proof-of-commitment --fail-on=critical
238
+
239
+ # Block PRs when a dependency hits CRITICAL.
240
+ # Use --fail-on=risky to also block HIGH-risk (score < 40) packages.
241
+ # Alternative: piiiico/commit-action@v1 (annotated PR checks).
209
242
 
210
243
  ${clr(c.bold, 'Score meaning:')}
211
244
  🔴 CRITICAL Sole publisher + >10M downloads/wk (publish-access concentration risk)
@@ -221,9 +254,7 @@ ${clr(c.bold, 'Provenance (npm):')}
221
254
  ${clr(c.bold, 'Score dimensions (npm/PyPI/Cargo):')} longevity · download momentum · release consistency · publisher depth · GitHub backing · provenance
222
255
  ${clr(c.bold, 'Score dimensions (Go):')} longevity · release consistency · maintainer depth · GitHub backing · stars
223
256
 
224
- ${clr(c.bold, 'CI integration:')}
225
- GitHub Action: ${clr(c.cyan, 'github.com/piiiico/commit-action')} — fails PRs on CRITICAL packages
226
- MCP server: Add to Claude Desktop / Cursor for AI-assisted auditing
257
+ ${clr(c.bold, 'MCP:')} Add to Claude Desktop / Cursor for AI-assisted auditing — see homepage.
227
258
 
228
259
  ${clr(c.bold, 'Web:')} ${WEB}
229
260
  `);
@@ -474,6 +505,54 @@ function parseGoSum(content) {
474
505
  return [...pkgs];
475
506
  }
476
507
 
508
+ /**
509
+ * Auto-detect the most authoritative manifest in the current directory.
510
+ *
511
+ * Candidate set (ordered within ecosystem by transitive coverage — first preferred):
512
+ * npm: package-lock.json, yarn.lock, pnpm-lock.yaml, pnpm-workspace.yaml, package.json
513
+ * pypi: requirements.txt
514
+ * cargo: Cargo.toml
515
+ * golang: go.sum, go.mod
516
+ *
517
+ * Selection: among files that exist, prefer the one with the most recent mtime.
518
+ * Ties (same mtime) resolved by the candidate list order above.
519
+ * Returns the basename of the chosen file, or null if no manifest is present.
520
+ */
521
+ async function autodetectManifest(cwd) {
522
+ const fs = await import('fs');
523
+ const path = await import('path');
524
+
525
+ const candidates = [
526
+ 'package-lock.json',
527
+ 'yarn.lock',
528
+ 'pnpm-lock.yaml',
529
+ 'pnpm-lock.yml',
530
+ 'pnpm-workspace.yaml',
531
+ 'pnpm-workspace.yml',
532
+ 'package.json',
533
+ 'requirements.txt',
534
+ 'Cargo.toml',
535
+ 'go.sum',
536
+ 'go.mod',
537
+ ];
538
+
539
+ const found = [];
540
+ for (let idx = 0; idx < candidates.length; idx++) {
541
+ const name = candidates[idx];
542
+ const full = path.join(cwd, name);
543
+ try {
544
+ const stat = fs.statSync(full);
545
+ if (stat.isFile()) found.push({ name, mtime: stat.mtimeMs, order: idx });
546
+ } catch {}
547
+ }
548
+
549
+ if (found.length === 0) return null;
550
+
551
+ // Sort: newest mtime first; ties resolved by candidate-list order.
552
+ found.sort((a, b) => (b.mtime - a.mtime) || (a.order - b.order));
553
+ return found[0].name;
554
+ }
555
+
477
556
  /**
478
557
  * Audit packages in batches of 20, in parallel.
479
558
  */
@@ -516,10 +595,25 @@ async function auditBatched(packages, ecosystem, { onProgress } = {}) {
516
595
  return all;
517
596
  }
518
597
 
598
+ /** Parse --fail-on=<level>. Returns one of 'critical' | 'risky' | 'none'. */
599
+ function parseFailOn(raw) {
600
+ const v = String(raw || '').toLowerCase();
601
+ if (v === 'critical' || v === 'risky' || v === 'none') return v;
602
+ throw new Error(`Invalid --fail-on value: '${raw}'. Expected: critical, risky, or none.`);
603
+ }
604
+
605
+ /** Decide exit code given results + fail-on threshold. */
606
+ function shouldFail(results, failOn) {
607
+ if (failOn === 'none') return false;
608
+ if (failOn === 'critical') return results.some(r => hasCritical(r.riskFlags));
609
+ if (failOn === 'risky') return results.some(r => hasCritical(r.riskFlags) || (typeof r.score === 'number' && r.score < 40));
610
+ return false;
611
+ }
612
+
519
613
  async function main() {
520
614
  const args = process.argv.slice(2);
521
615
 
522
- if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
616
+ if (args.includes('--help') || args.includes('-h')) {
523
617
  printHelp();
524
618
  process.exit(0);
525
619
  }
@@ -530,6 +624,8 @@ async function main() {
530
624
  let isLockfile = false;
531
625
  let totalInFile = 0;
532
626
  let jsonOutput = false;
627
+ // null means "default later" — depends on output mode and CI env.
628
+ let failOn = null;
533
629
 
534
630
  let i = 0;
535
631
  while (i < args.length) {
@@ -539,6 +635,16 @@ async function main() {
539
635
  else if (a === '--cargo') { ecosystem = 'cargo'; i++; }
540
636
  else if (a === '--golang' || a === '--go') { ecosystem = 'golang'; i++; }
541
637
  else if (a === '--json') { jsonOutput = true; i++; }
638
+ else if (a.startsWith('--fail-on=')) {
639
+ try { failOn = parseFailOn(a.slice('--fail-on='.length)); }
640
+ catch (err) { console.error(err.message); process.exit(2); }
641
+ i++;
642
+ }
643
+ else if (a === '--fail-on') {
644
+ try { failOn = parseFailOn(args[++i]); }
645
+ catch (err) { console.error(err.message); process.exit(2); }
646
+ i++;
647
+ }
542
648
  else if (a === '--file' || a === '-f') {
543
649
  filePath = args[++i];
544
650
  i++;
@@ -550,6 +656,20 @@ async function main() {
550
656
  else { packages.push(a); i++; }
551
657
  }
552
658
 
659
+ // Zero-arg auto-detect: if no positional packages and no --file, look for a manifest in cwd.
660
+ if (!filePath && packages.length === 0) {
661
+ const detected = await autodetectManifest(process.cwd());
662
+ if (detected) {
663
+ filePath = detected;
664
+ if (!jsonOutput) console.log(clr(c.dim, `Auto-detected manifest: ${detected}`));
665
+ } else {
666
+ // No positional packages, no --file, and no manifest in cwd → print help.
667
+ // This preserves the prior "bare invocation" UX rather than failing silently.
668
+ printHelp();
669
+ process.exit(0);
670
+ }
671
+ }
672
+
553
673
  if (filePath) {
554
674
  try {
555
675
  const result = await readPackagesFromFile(filePath);
@@ -569,6 +689,18 @@ async function main() {
569
689
  process.exit(1);
570
690
  }
571
691
 
692
+ // Resolve fail-on default.
693
+ // - User passed --fail-on=X → use X (already set).
694
+ // - CI env (CI=true or =1) → 'critical' (hard gate by default in CI).
695
+ // - --json output (no CI) → 'critical' (preserves v1.7.x behavior).
696
+ // - interactive table output → 'none' (backward-compatible for casual users).
697
+ if (failOn === null) {
698
+ const ciEnv = process.env.CI;
699
+ const inCI = ciEnv === 'true' || ciEnv === '1';
700
+ if (inCI || jsonOutput) failOn = 'critical';
701
+ else failOn = 'none';
702
+ }
703
+
572
704
  const t0 = Date.now();
573
705
 
574
706
  let allResults;
@@ -626,9 +758,10 @@ async function main() {
626
758
  totalScanned: allResults.length,
627
759
  criticalCount,
628
760
  provenanceCount,
761
+ failOn,
629
762
  results: allResults,
630
763
  }, null, 2));
631
- process.exit(criticalCount > 0 ? 1 : 0);
764
+ process.exit(shouldFail(allResults, failOn) ? 1 : 0);
632
765
  }
633
766
 
634
767
  // Lock files: show top 25 highest-risk
@@ -636,12 +769,16 @@ async function main() {
636
769
  const displayed = allResults.slice(0, MAX_DISPLAY);
637
770
  const criticalTotal = allResults.filter(r => hasCritical(r.riskFlags)).length;
638
771
  printTable(displayed, { totalScanned: allResults.length, totalCritical: criticalTotal, lockfile: true });
772
+ if (shouldFail(allResults, failOn)) {
773
+ console.error(clr(c.red + c.bold, `\n✗ --fail-on=${failOn} threshold met. Exit 1.`));
774
+ process.exit(1);
775
+ }
639
776
  return;
640
777
  }
641
778
 
642
779
  if (!allResults || allResults.length === 0) {
643
780
  if (jsonOutput) {
644
- console.log(JSON.stringify({ totalScanned: 0, criticalCount: 0, provenanceCount: 0, results: [] }, null, 2));
781
+ console.log(JSON.stringify({ totalScanned: 0, criticalCount: 0, provenanceCount: 0, failOn, results: [] }, null, 2));
645
782
  } else {
646
783
  console.log('No results returned. Check package names and try again.');
647
784
  }
@@ -655,12 +792,17 @@ async function main() {
655
792
  totalScanned: allResults.length,
656
793
  criticalCount,
657
794
  provenanceCount,
795
+ failOn,
658
796
  results: allResults,
659
797
  }, null, 2));
660
- process.exit(criticalCount > 0 ? 1 : 0);
798
+ process.exit(shouldFail(allResults, failOn) ? 1 : 0);
661
799
  }
662
800
 
663
801
  printTable(allResults);
802
+ if (shouldFail(allResults, failOn)) {
803
+ console.error(clr(c.red + c.bold, `✗ --fail-on=${failOn} threshold met. Exit 1.`));
804
+ process.exit(1);
805
+ }
664
806
  }
665
807
 
666
808
  main().catch(err => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proof-of-commitment",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
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": {