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