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.
- package/index.js +158 -16
- 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.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.
|
|
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
|
|
196
|
-
--
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
207
|
-
npx proof-of-commitment --file go.sum
|
|
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, '
|
|
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.
|
|
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(
|
|
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(
|
|
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 => {
|