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