proof-of-commitment 1.9.0 → 1.11.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/README.md +11 -9
- package/index.js +522 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,24 +51,26 @@ npx proof-of-commitment --file go.sum # full transitive set
|
|
|
51
51
|
|
|
52
52
|
**Web demo (no install):** [getcommit.dev/audit](https://getcommit.dev/audit) — paste your packages, see risk scores in seconds.
|
|
53
53
|
|
|
54
|
-
**
|
|
54
|
+
**Account + monitoring (v1.10.0):**
|
|
55
55
|
```bash
|
|
56
56
|
# Install once, then use the `poc` alias:
|
|
57
57
|
npm install -g proof-of-commitment
|
|
58
58
|
|
|
59
|
-
#
|
|
59
|
+
# Get a free API key at https://getcommit.dev/get-started, then:
|
|
60
|
+
poc login sk_commit_your_key_here
|
|
61
|
+
# ✓ Authenticated — Tier: Free — Usage: 0/200 requests (daily)
|
|
62
|
+
|
|
63
|
+
poc status # check tier + usage anytime
|
|
64
|
+
poc logout # remove saved key
|
|
65
|
+
|
|
66
|
+
# Monitoring (Pro tier — daily scans + alerts):
|
|
60
67
|
poc watch chalk
|
|
61
68
|
poc watch requests --ecosystem pypi
|
|
62
69
|
poc watch serde --ecosystem cargo
|
|
63
|
-
|
|
64
|
-
# View your watchlist with current scores:
|
|
65
|
-
poc watchlist
|
|
66
|
-
|
|
67
|
-
# Remove a package:
|
|
70
|
+
poc watchlist # view scores + risk levels
|
|
68
71
|
poc unwatch chalk
|
|
69
72
|
|
|
70
|
-
#
|
|
71
|
-
# Get a key at https://getcommit.dev/pricing
|
|
73
|
+
# Upgrade to Pro: https://getcommit.dev/pricing
|
|
72
74
|
```
|
|
73
75
|
|
|
74
76
|
Alerts fire on: score drop ≥10 points · package crosses CRITICAL threshold · recovery to HEALTHY.
|
package/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* proof-of-commitment CLI v1.
|
|
3
|
+
* proof-of-commitment CLI v1.11.0
|
|
4
4
|
* Scores npm/PyPI/Cargo/Go packages on behavioral commitment signals.
|
|
5
5
|
* Usage: npx proof-of-commitment [packages...] [options]
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const API = 'https://poc-backend.amdal-dev.workers.dev/api/audit';
|
|
9
|
+
const KEYS_API = 'https://poc-backend.amdal-dev.workers.dev/api/keys';
|
|
9
10
|
const WATCHLIST_API = 'https://poc-backend.amdal-dev.workers.dev/api/watchlist';
|
|
10
11
|
const WEB = 'https://getcommit.dev/audit';
|
|
11
12
|
|
|
@@ -26,6 +27,16 @@ const c = {
|
|
|
26
27
|
|
|
27
28
|
const NO_COLOR = process.env.NO_COLOR || !process.stdout.isTTY;
|
|
28
29
|
|
|
30
|
+
// Synchronous API key check for upsell messaging (avoids async in printTable)
|
|
31
|
+
let _cachedHasKey = false;
|
|
32
|
+
try {
|
|
33
|
+
const _os = await import('os');
|
|
34
|
+
const _fs = await import('fs');
|
|
35
|
+
const _path = await import('path');
|
|
36
|
+
const _cfg = _fs.readFileSync(_path.join(_os.homedir(), '.commit', 'config'), 'utf-8');
|
|
37
|
+
_cachedHasKey = /^api_key\s*=\s*.+$/m.test(_cfg);
|
|
38
|
+
} catch {}
|
|
39
|
+
|
|
29
40
|
function clr(code, text) {
|
|
30
41
|
if (NO_COLOR) return text;
|
|
31
42
|
return `${code}${text}${c.reset}`;
|
|
@@ -174,15 +185,23 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
|
|
|
174
185
|
|
|
175
186
|
// Contextual upsell — show when findings make monitoring relevant
|
|
176
187
|
if (effectiveCritical > 0) {
|
|
177
|
-
|
|
178
|
-
|
|
188
|
+
// Check for API key synchronously via env (fast path)
|
|
189
|
+
const hasKey = !!process.env.COMMIT_API_KEY || _cachedHasKey;
|
|
190
|
+
if (hasKey) {
|
|
191
|
+
console.log(clr(c.dim, `\n 📊 Monitor ${effectiveCritical === 1 ? 'this package' : 'these packages'}: `) +
|
|
192
|
+
clr(c.cyan, `poc watch ${results.find(r => hasCritical(r.riskFlags))?.name || results[0]?.name}`));
|
|
193
|
+
} else {
|
|
194
|
+
console.log(clr(c.dim, `\n 📊 Monitor ${effectiveCritical === 1 ? 'this' : 'these ' + effectiveCritical} CRITICAL ${effectiveCritical === 1 ? 'package' : 'packages'} — get alerted when scores change.`));
|
|
195
|
+
console.log(clr(c.dim, ' Get a free API key: ') + clr(c.cyan, 'https://getcommit.dev/get-started'));
|
|
196
|
+
console.log(clr(c.dim, ' Then run: ') + clr(c.cyan, 'poc login'));
|
|
197
|
+
}
|
|
179
198
|
}
|
|
180
199
|
console.log();
|
|
181
200
|
}
|
|
182
201
|
|
|
183
202
|
function printHelp() {
|
|
184
203
|
console.log(`
|
|
185
|
-
${clr(c.bold, 'proof-of-commitment')} v1.
|
|
204
|
+
${clr(c.bold, 'proof-of-commitment')} v1.11.0 — supply chain risk scorer
|
|
186
205
|
|
|
187
206
|
${clr(c.bold, 'Usage:')}
|
|
188
207
|
npx proof-of-commitment Auto-detect manifest in current dir
|
|
@@ -199,14 +218,24 @@ ${clr(c.bold, 'Usage:')}
|
|
|
199
218
|
npx proof-of-commitment --file go.mod Audit Go direct + indirect deps
|
|
200
219
|
npx proof-of-commitment --file go.sum Audit Go full transitive set
|
|
201
220
|
|
|
202
|
-
${clr(c.bold, '
|
|
221
|
+
${clr(c.bold, 'Reports:')}
|
|
222
|
+
poc report Scan and generate a shareable HTML report + Markdown snippet
|
|
223
|
+
poc report [pkgs] Same flags as scan — packages, --pypi, --cargo, --file, etc.
|
|
224
|
+
Saves audit-report.html to cwd + prints Markdown for GitHub issues
|
|
225
|
+
|
|
226
|
+
${clr(c.bold, 'Account:')}
|
|
227
|
+
poc login [key] Save and validate your API key (interactive or direct)
|
|
228
|
+
poc status Show current tier, usage, and limits
|
|
229
|
+
poc logout Remove saved API key
|
|
230
|
+
|
|
231
|
+
${clr(c.bold, 'Monitoring (Pro):')}
|
|
203
232
|
poc watch <package> [--ecosystem npm|pypi|cargo|golang]
|
|
204
|
-
Add a package to daily monitoring
|
|
233
|
+
Add a package to daily monitoring
|
|
205
234
|
poc watchlist List monitored packages with current scores + risk
|
|
206
235
|
poc unwatch <pkg> Remove a package from monitoring
|
|
207
236
|
|
|
208
|
-
|
|
209
|
-
|
|
237
|
+
Get a free key: https://getcommit.dev/get-started
|
|
238
|
+
Upgrade to Pro: https://getcommit.dev/pricing
|
|
210
239
|
|
|
211
240
|
${clr(c.bold, 'Options:')}
|
|
212
241
|
--json Output results as JSON
|
|
@@ -646,6 +675,174 @@ async function readApiKey() {
|
|
|
646
675
|
return null;
|
|
647
676
|
}
|
|
648
677
|
|
|
678
|
+
/**
|
|
679
|
+
* Write API key to ~/.commit/config, creating dir if needed.
|
|
680
|
+
*/
|
|
681
|
+
async function writeApiKey(key) {
|
|
682
|
+
const os = await import('os');
|
|
683
|
+
const fs = await import('fs');
|
|
684
|
+
const path = await import('path');
|
|
685
|
+
const dir = path.join(os.homedir(), '.commit');
|
|
686
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
687
|
+
|
|
688
|
+
const configPath = path.join(dir, 'config');
|
|
689
|
+
let lines = [];
|
|
690
|
+
try {
|
|
691
|
+
lines = fs.readFileSync(configPath, 'utf-8').split('\n');
|
|
692
|
+
} catch {}
|
|
693
|
+
|
|
694
|
+
// Replace existing api_key line or append
|
|
695
|
+
let found = false;
|
|
696
|
+
lines = lines.map(l => {
|
|
697
|
+
if (/^api_key\s*=/.test(l)) { found = true; return `api_key = ${key}`; }
|
|
698
|
+
return l;
|
|
699
|
+
});
|
|
700
|
+
if (!found) lines.push(`api_key = ${key}`);
|
|
701
|
+
|
|
702
|
+
fs.writeFileSync(configPath, lines.filter(l => l !== '').join('\n') + '\n');
|
|
703
|
+
return configPath;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Remove API key from ~/.commit/config.
|
|
708
|
+
*/
|
|
709
|
+
async function removeApiKey() {
|
|
710
|
+
const os = await import('os');
|
|
711
|
+
const fs = await import('fs');
|
|
712
|
+
const path = await import('path');
|
|
713
|
+
const configPath = path.join(os.homedir(), '.commit', 'config');
|
|
714
|
+
try {
|
|
715
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
716
|
+
const filtered = content.split('\n').filter(l => !/^api_key\s*=/.test(l)).join('\n');
|
|
717
|
+
fs.writeFileSync(configPath, filtered.trim() ? filtered.trim() + '\n' : '');
|
|
718
|
+
return true;
|
|
719
|
+
} catch { return false; }
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Validate an API key against the usage endpoint. Returns tier info or null.
|
|
724
|
+
*/
|
|
725
|
+
async function validateApiKey(key) {
|
|
726
|
+
try {
|
|
727
|
+
const res = await fetch(`${KEYS_API}/usage`, {
|
|
728
|
+
headers: { 'Authorization': `Bearer ${key}` },
|
|
729
|
+
});
|
|
730
|
+
if (!res.ok) return null;
|
|
731
|
+
return await res.json();
|
|
732
|
+
} catch { return null; }
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* poc login [key] — save and validate API key
|
|
737
|
+
*/
|
|
738
|
+
async function cmdLogin(keyArg) {
|
|
739
|
+
let key = keyArg;
|
|
740
|
+
|
|
741
|
+
if (!key) {
|
|
742
|
+
// Check if stdin has data (piped input)
|
|
743
|
+
const { createInterface } = await import('readline');
|
|
744
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
745
|
+
key = await new Promise(resolve => {
|
|
746
|
+
rl.question(clr(c.dim, ' Enter your API key: '), answer => {
|
|
747
|
+
rl.close();
|
|
748
|
+
resolve(answer.trim());
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (!key || !key.startsWith('sk_commit_')) {
|
|
754
|
+
console.error(clr(c.red, '\n Invalid API key format. Keys start with sk_commit_'));
|
|
755
|
+
console.error(clr(c.dim, ' Get one at https://getcommit.dev/get-started\n'));
|
|
756
|
+
process.exit(1);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
process.stdout.write(clr(c.dim, ' Validating...'));
|
|
760
|
+
const info = await validateApiKey(key);
|
|
761
|
+
|
|
762
|
+
if (!info || info.error) {
|
|
763
|
+
console.error(clr(c.red, ' ✗ Invalid or expired API key.'));
|
|
764
|
+
console.error(clr(c.dim, ` ${info?.message || 'Key not recognized.'}`));
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const configPath = await writeApiKey(key);
|
|
769
|
+
console.log(clr(c.green, ' ✓ Authenticated'));
|
|
770
|
+
console.log();
|
|
771
|
+
console.log(clr(c.bold, ` Tier: `) + tierLabel(info.tier));
|
|
772
|
+
console.log(clr(c.bold, ` Usage: `) + `${info.requests_used ?? 0}/${info.requests_limit ?? '?'} requests (${info.period || 'daily'})`);
|
|
773
|
+
console.log(clr(c.bold, ` Resets: `) + (info.period_reset_at || '—'));
|
|
774
|
+
console.log(clr(c.dim, ` Saved to: ${configPath}`));
|
|
775
|
+
console.log();
|
|
776
|
+
|
|
777
|
+
if (info.tier === 'pro' || info.tier === 'enterprise') {
|
|
778
|
+
console.log(clr(c.cyan, ' Pro features unlocked:'));
|
|
779
|
+
console.log(clr(c.dim, ' poc watch <package> Add a package to daily monitoring'));
|
|
780
|
+
console.log(clr(c.dim, ' poc watchlist View monitored packages'));
|
|
781
|
+
console.log(clr(c.dim, ' poc unwatch <package> Remove from monitoring'));
|
|
782
|
+
} else {
|
|
783
|
+
console.log(clr(c.dim, ' Upgrade to Pro for monitoring + alerts: https://getcommit.dev/pricing'));
|
|
784
|
+
}
|
|
785
|
+
console.log();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* poc status — show current auth + usage
|
|
790
|
+
*/
|
|
791
|
+
async function cmdStatus() {
|
|
792
|
+
const key = await readApiKey();
|
|
793
|
+
|
|
794
|
+
if (!key) {
|
|
795
|
+
console.log(clr(c.dim, '\n Not logged in.'));
|
|
796
|
+
console.log(clr(c.dim, ' Run ') + clr(c.cyan, 'poc login') + clr(c.dim, ' to authenticate.'));
|
|
797
|
+
console.log(clr(c.dim, ' Get a free key at https://getcommit.dev/get-started\n'));
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
process.stdout.write(clr(c.dim, ' Checking...'));
|
|
802
|
+
const info = await validateApiKey(key);
|
|
803
|
+
|
|
804
|
+
if (!info || info.error) {
|
|
805
|
+
console.error(clr(c.red, ' ✗ Key invalid or expired.'));
|
|
806
|
+
console.error(clr(c.dim, ' Run ') + clr(c.cyan, 'poc login') + clr(c.dim, ' to re-authenticate.\n'));
|
|
807
|
+
process.exit(1);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
console.log(clr(c.green, ' ✓ Connected'));
|
|
811
|
+
console.log();
|
|
812
|
+
console.log(clr(c.bold, ` Tier: `) + tierLabel(info.tier));
|
|
813
|
+
console.log(clr(c.bold, ` Usage: `) + `${info.requests_used ?? 0}/${info.requests_limit ?? '?'} requests (${info.period || 'daily'})`);
|
|
814
|
+
console.log(clr(c.bold, ` Resets: `) + (info.period_reset_at || '—'));
|
|
815
|
+
console.log(clr(c.bold, ` Prefix: `) + (info.key_prefix || key.slice(0, 19) + '...'));
|
|
816
|
+
console.log();
|
|
817
|
+
|
|
818
|
+
if (info.tier === 'free') {
|
|
819
|
+
const pct = info.requests_limit > 0 ? Math.round((info.requests_used / info.requests_limit) * 100) : 0;
|
|
820
|
+
if (pct >= 80) {
|
|
821
|
+
console.log(clr(c.yellow, ` ⚠ ${pct}% of daily limit used. Upgrade for 10K/month: https://getcommit.dev/pricing`));
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* poc logout — remove saved API key
|
|
828
|
+
*/
|
|
829
|
+
async function cmdLogout() {
|
|
830
|
+
const removed = await removeApiKey();
|
|
831
|
+
if (removed) {
|
|
832
|
+
console.log(clr(c.green, '\n ✓ Logged out. API key removed from ~/.commit/config.'));
|
|
833
|
+
} else {
|
|
834
|
+
console.log(clr(c.dim, '\n No saved API key found.'));
|
|
835
|
+
}
|
|
836
|
+
console.log();
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function tierLabel(tier) {
|
|
840
|
+
if (tier === 'pro') return clr(c.cyan + c.bold, 'Pro');
|
|
841
|
+
if (tier === 'enterprise') return clr(c.magenta + c.bold, 'Enterprise');
|
|
842
|
+
if (tier === 'developer') return clr(c.green + c.bold, 'Developer');
|
|
843
|
+
return clr(c.dim, 'Free');
|
|
844
|
+
}
|
|
845
|
+
|
|
649
846
|
/**
|
|
650
847
|
* Handle 402 upgrade response from watchlist endpoints.
|
|
651
848
|
*/
|
|
@@ -803,6 +1000,257 @@ async function cmdUnwatch(pkg, ecosystem) {
|
|
|
803
1000
|
}
|
|
804
1001
|
}
|
|
805
1002
|
|
|
1003
|
+
/**
|
|
1004
|
+
* Generate a self-contained HTML report from audit results.
|
|
1005
|
+
* Returns the full HTML string.
|
|
1006
|
+
*/
|
|
1007
|
+
function buildHtmlReport(results, { ecosystem, scannedFrom, totalScanned } = {}) {
|
|
1008
|
+
const ts = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
|
|
1009
|
+
const topPkgs = results.slice(0, 20).map(r => r.name).join(',');
|
|
1010
|
+
const webUrl = `${WEB}?packages=${encodeURIComponent(topPkgs)}`;
|
|
1011
|
+
|
|
1012
|
+
const criticalCount = results.filter(r => hasCritical(r.riskFlags)).length;
|
|
1013
|
+
const healthyCount = results.filter(r => !hasCritical(r.riskFlags) && (r.score || 0) >= 75).length;
|
|
1014
|
+
|
|
1015
|
+
function riskBadge(pkg) {
|
|
1016
|
+
if (hasCritical(pkg.riskFlags)) return '<span class="badge critical">CRITICAL</span>';
|
|
1017
|
+
if ((pkg.score || 100) < 40) return '<span class="badge high">HIGH</span>';
|
|
1018
|
+
if ((pkg.score || 100) < 60) return '<span class="badge moderate">MODERATE</span>';
|
|
1019
|
+
if ((pkg.score || 100) < 75) return '<span class="badge good">GOOD</span>';
|
|
1020
|
+
return '<span class="badge healthy">HEALTHY</span>';
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function provBadge(pkg) {
|
|
1024
|
+
if (pkg.ecosystem === 'golang') return '';
|
|
1025
|
+
return pkg.hasProvenance ? '<span class="prov">🔐</span>' : '';
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function fmtDl(n) {
|
|
1029
|
+
if (!n) return '—';
|
|
1030
|
+
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B/wk';
|
|
1031
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M/wk';
|
|
1032
|
+
if (n >= 1e3) return (n / 1e3).toFixed(0) + 'K/wk';
|
|
1033
|
+
return n + '/wk';
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const rows = results.map(pkg => {
|
|
1037
|
+
const isGo = pkg.ecosystem === 'golang';
|
|
1038
|
+
return `<tr class="${hasCritical(pkg.riskFlags) ? 'row-critical' : ''}">
|
|
1039
|
+
<td class="pkg-name">${escHtml(pkg.name)}${provBadge(pkg)}</td>
|
|
1040
|
+
<td>${riskBadge(pkg)}</td>
|
|
1041
|
+
<td class="score">${pkg.score ?? '?'}</td>
|
|
1042
|
+
<td>${pkg.maintainers === 35 ? '30+' : (pkg.maintainers ?? '?')}</td>
|
|
1043
|
+
<td>${isGo ? '—' : fmtDl(pkg.weeklyDownloads)}</td>
|
|
1044
|
+
<td>${pkg.ageYears ? pkg.ageYears.toString().replace(/(\.\d).*/, '$1') + 'y' : '?'}</td>
|
|
1045
|
+
</tr>`;
|
|
1046
|
+
}).join('\n');
|
|
1047
|
+
|
|
1048
|
+
const summaryLabel = criticalCount > 0
|
|
1049
|
+
? `⚠ ${criticalCount} CRITICAL package${criticalCount > 1 ? 's' : ''} found`
|
|
1050
|
+
: `✓ No CRITICAL packages`;
|
|
1051
|
+
|
|
1052
|
+
const summaryClass = criticalCount > 0 ? 'summary-critical' : 'summary-ok';
|
|
1053
|
+
|
|
1054
|
+
const scannedLine = scannedFrom
|
|
1055
|
+
? `<span>Scanned from <code>${escHtml(scannedFrom)}</code></span> · `
|
|
1056
|
+
: '';
|
|
1057
|
+
const totalLine = totalScanned && totalScanned > results.length
|
|
1058
|
+
? `showing top ${results.length} of ${totalScanned} packages · `
|
|
1059
|
+
: `${results.length} package${results.length !== 1 ? 's' : ''} · `;
|
|
1060
|
+
|
|
1061
|
+
return `<!DOCTYPE html>
|
|
1062
|
+
<html lang="en">
|
|
1063
|
+
<head>
|
|
1064
|
+
<meta charset="UTF-8">
|
|
1065
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1066
|
+
<title>Supply chain audit — proof-of-commitment</title>
|
|
1067
|
+
<style>
|
|
1068
|
+
:root { --red:#ef4444;--orange:#f97316;--yellow:#eab308;--green:#22c55e;--cyan:#06b6d4;--bg:#0f172a;--surface:#1e293b;--border:#334155;--text:#f1f5f9;--muted:#94a3b8; }
|
|
1069
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1070
|
+
body { font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; background: var(--bg); color: var(--text); padding: 2rem; line-height: 1.5; font-size: 14px; }
|
|
1071
|
+
.header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
|
|
1072
|
+
.logo { font-size: 1.1rem; font-weight: bold; color: var(--cyan); }
|
|
1073
|
+
.logo a { color: inherit; text-decoration: none; }
|
|
1074
|
+
.web-link { margin-left: auto; }
|
|
1075
|
+
.web-link a { color: var(--cyan); text-decoration: none; font-size: 0.85rem; border: 1px solid var(--border); padding: 0.3rem 0.75rem; border-radius: 4px; }
|
|
1076
|
+
.web-link a:hover { background: var(--surface); }
|
|
1077
|
+
.summary { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1.5rem; font-weight: bold; }
|
|
1078
|
+
.summary-critical { background: rgba(239,68,68,0.15); border: 1px solid var(--red); color: var(--red); }
|
|
1079
|
+
.summary-ok { background: rgba(34,197,94,0.1); border: 1px solid var(--green); color: var(--green); }
|
|
1080
|
+
.meta { color: var(--muted); font-size: 0.8rem; margin-bottom: 1.5rem; }
|
|
1081
|
+
.meta code { background: var(--surface); padding: 0.1rem 0.3rem; border-radius: 3px; }
|
|
1082
|
+
table { width: 100%; border-collapse: collapse; }
|
|
1083
|
+
thead { border-bottom: 1px solid var(--border); }
|
|
1084
|
+
th { text-align: left; padding: 0.5rem 0.75rem; color: var(--muted); font-weight: normal; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
1085
|
+
td { padding: 0.5rem 0.75rem; border-bottom: 1px solid rgba(51,65,85,0.5); }
|
|
1086
|
+
tr.row-critical { background: rgba(239,68,68,0.07); }
|
|
1087
|
+
.pkg-name { font-weight: bold; }
|
|
1088
|
+
.prov { margin-left: 0.4rem; font-size: 0.9em; }
|
|
1089
|
+
.badge { display: inline-block; padding: 0.1rem 0.5rem; border-radius: 3px; font-size: 0.75rem; font-weight: bold; }
|
|
1090
|
+
.badge.critical { background: rgba(239,68,68,0.2); color: var(--red); border: 1px solid var(--red); }
|
|
1091
|
+
.badge.high { background: rgba(249,115,22,0.15); color: var(--orange); border: 1px solid var(--orange); }
|
|
1092
|
+
.badge.moderate { background: rgba(234,179,8,0.15); color: var(--yellow); border: 1px solid var(--yellow); }
|
|
1093
|
+
.badge.good { background: rgba(234,179,8,0.1); color: var(--yellow); border: 1px solid rgba(234,179,8,0.5); }
|
|
1094
|
+
.badge.healthy { background: rgba(34,197,94,0.1); color: var(--green); border: 1px solid var(--green); }
|
|
1095
|
+
.score { color: var(--muted); }
|
|
1096
|
+
.footer { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--border); color: var(--muted); font-size: 0.8rem; display: flex; gap: 1.5rem; flex-wrap: wrap; }
|
|
1097
|
+
.footer a { color: var(--cyan); text-decoration: none; }
|
|
1098
|
+
.md-section { margin-top: 2rem; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 1rem; }
|
|
1099
|
+
.md-label { color: var(--muted); font-size: 0.75rem; margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
1100
|
+
.md-copy { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 0.75rem; font-size: 0.8rem; white-space: pre; overflow-x: auto; color: var(--text); }
|
|
1101
|
+
.copy-btn { float: right; cursor: pointer; background: var(--border); border: none; color: var(--text); padding: 0.2rem 0.6rem; border-radius: 3px; font-size: 0.75rem; font-family: inherit; }
|
|
1102
|
+
.copy-btn:hover { background: var(--cyan); color: var(--bg); }
|
|
1103
|
+
</style>
|
|
1104
|
+
</head>
|
|
1105
|
+
<body>
|
|
1106
|
+
<div class="header">
|
|
1107
|
+
<div class="logo"><a href="${WEB}" target="_blank">proof-of-commitment</a></div>
|
|
1108
|
+
<div class="web-link"><a href="${webUrl}" target="_blank">🔗 Open in browser →</a></div>
|
|
1109
|
+
</div>
|
|
1110
|
+
<div class="summary ${summaryClass}">${summaryLabel}</div>
|
|
1111
|
+
<div class="meta">${scannedLine}${totalLine}${ecosystem || 'npm'} · generated ${ts}</div>
|
|
1112
|
+
<table>
|
|
1113
|
+
<thead><tr>
|
|
1114
|
+
<th>Package</th><th>Risk</th><th>Score</th><th>Publishers</th><th>Downloads</th><th>Age</th>
|
|
1115
|
+
</tr></thead>
|
|
1116
|
+
<tbody>
|
|
1117
|
+
${rows}
|
|
1118
|
+
</tbody>
|
|
1119
|
+
</table>
|
|
1120
|
+
<div class="md-section">
|
|
1121
|
+
<div class="md-label">Copy for GitHub issues / Slack <button class="copy-btn" onclick="copyMd()">Copy</button></div>
|
|
1122
|
+
<div class="md-copy" id="md-content">${escHtml(buildMarkdown(results, { ecosystem, scannedFrom, totalScanned, webUrl }))}</div>
|
|
1123
|
+
</div>
|
|
1124
|
+
<div class="footer">
|
|
1125
|
+
<span>Generated by <a href="${WEB}" target="_blank">proof-of-commitment</a></span>
|
|
1126
|
+
<span><a href="https://github.com/piiiico/commit-action" target="_blank">GitHub Action</a></span>
|
|
1127
|
+
<span><a href="https://getcommit.dev/pricing" target="_blank">Commit Pro</a></span>
|
|
1128
|
+
</div>
|
|
1129
|
+
<script>
|
|
1130
|
+
function copyMd() {
|
|
1131
|
+
const text = document.getElementById('md-content').textContent;
|
|
1132
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
1133
|
+
const btn = document.querySelector('.copy-btn');
|
|
1134
|
+
btn.textContent = '✓ Copied';
|
|
1135
|
+
setTimeout(() => btn.textContent = 'Copy', 2000);
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
</script>
|
|
1139
|
+
</body>
|
|
1140
|
+
</html>`;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function escHtml(str) {
|
|
1144
|
+
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function buildMarkdown(results, { ecosystem, scannedFrom, totalScanned, webUrl } = {}) {
|
|
1148
|
+
const criticalCount = results.filter(r => hasCritical(r.riskFlags)).length;
|
|
1149
|
+
const summaryLine = criticalCount > 0
|
|
1150
|
+
? `⚠ **${criticalCount} CRITICAL package${criticalCount > 1 ? 's' : ''} found**`
|
|
1151
|
+
: `✅ No CRITICAL packages`;
|
|
1152
|
+
|
|
1153
|
+
function riskEmoji(pkg) {
|
|
1154
|
+
if (hasCritical(pkg.riskFlags)) return '🔴 CRITICAL';
|
|
1155
|
+
if ((pkg.score || 100) < 40) return '🟠 HIGH';
|
|
1156
|
+
if ((pkg.score || 100) < 60) return '🟡 MODERATE';
|
|
1157
|
+
if ((pkg.score || 100) < 75) return '🟡 GOOD';
|
|
1158
|
+
return '🟢 HEALTHY';
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const header = `| Package | Risk | Score | Publishers | Downloads |
|
|
1162
|
+
|---------|------|-------|------------|-----------|`;
|
|
1163
|
+
|
|
1164
|
+
function fmtDl(n) {
|
|
1165
|
+
if (!n) return '—';
|
|
1166
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M/wk';
|
|
1167
|
+
if (n >= 1e3) return (n / 1e3).toFixed(0) + 'K/wk';
|
|
1168
|
+
return n + '/wk';
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const rows = results.map(pkg => {
|
|
1172
|
+
const maintDisplay = pkg.maintainers === 35 ? '30+' : (pkg.maintainers ?? '?');
|
|
1173
|
+
const dlDisplay = pkg.ecosystem === 'golang' ? '—' : fmtDl(pkg.weeklyDownloads);
|
|
1174
|
+
return `| ${pkg.name}${pkg.hasProvenance ? ' 🔐' : ''} | ${riskEmoji(pkg)} | ${pkg.score ?? '?'} | ${maintDisplay} | ${dlDisplay} |`;
|
|
1175
|
+
}).join('\n');
|
|
1176
|
+
|
|
1177
|
+
const scannedNote = scannedFrom ? ` (from \`${scannedFrom}\`)` : '';
|
|
1178
|
+
const totalNote = totalScanned && totalScanned > results.length ? `, top ${results.length} of ${totalScanned}` : '';
|
|
1179
|
+
const footer = `\n*Scanned ${results.length} ${ecosystem || 'npm'} package${results.length !== 1 ? 's' : ''}${scannedNote}${totalNote} with [proof-of-commitment](https://getcommit.dev) · [Full report](${webUrl || WEB})*`;
|
|
1180
|
+
|
|
1181
|
+
return `## Supply chain audit\n\n${summaryLine}\n\n${header}\n${rows}${footer}`;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* poc report — generate shareable HTML report + Markdown snippet
|
|
1186
|
+
*/
|
|
1187
|
+
async function cmdReport(packages, ecosystem, { filePath, isLockfile, totalScanned } = {}) {
|
|
1188
|
+
const fs = await import('fs');
|
|
1189
|
+
|
|
1190
|
+
process.stdout.write(clr(c.dim, `Scoring ${packages.length} ${ecosystem} package${packages.length > 1 ? 's' : ''}...`));
|
|
1191
|
+
const t0 = Date.now();
|
|
1192
|
+
|
|
1193
|
+
let allResults;
|
|
1194
|
+
try {
|
|
1195
|
+
if (packages.length <= 20) {
|
|
1196
|
+
const res = await fetch(API, {
|
|
1197
|
+
method: 'POST',
|
|
1198
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1199
|
+
body: JSON.stringify({ packages, ecosystem }),
|
|
1200
|
+
});
|
|
1201
|
+
if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`);
|
|
1202
|
+
const data = await res.json();
|
|
1203
|
+
allResults = data.results || [];
|
|
1204
|
+
} else {
|
|
1205
|
+
allResults = await auditBatched(packages, ecosystem);
|
|
1206
|
+
}
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
console.error(`\nError: ${err.message}`);
|
|
1209
|
+
process.exit(1);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
|
1213
|
+
process.stdout.write(clr(c.dim, ` done in ${elapsed}s\n\n`));
|
|
1214
|
+
|
|
1215
|
+
// Sort: CRITICAL first, then by score ascending
|
|
1216
|
+
allResults.sort((a, b) => {
|
|
1217
|
+
const aCrit = hasCritical(a.riskFlags) ? 1 : 0;
|
|
1218
|
+
const bCrit = hasCritical(b.riskFlags) ? 1 : 0;
|
|
1219
|
+
if (aCrit !== bCrit) return bCrit - aCrit;
|
|
1220
|
+
return (a.score || 100) - (b.score || 100);
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
const displayResults = allResults.slice(0, 50);
|
|
1224
|
+
const topPkgs = displayResults.slice(0, 20).map(r => r.name).join(',');
|
|
1225
|
+
const webUrl = `${WEB}?packages=${encodeURIComponent(topPkgs)}`;
|
|
1226
|
+
|
|
1227
|
+
// Save HTML report
|
|
1228
|
+
const outFile = 'audit-report.html';
|
|
1229
|
+
const html = buildHtmlReport(displayResults, {
|
|
1230
|
+
ecosystem,
|
|
1231
|
+
scannedFrom: filePath ? filePath.split('/').pop() : null,
|
|
1232
|
+
totalScanned: totalScanned || allResults.length,
|
|
1233
|
+
});
|
|
1234
|
+
fs.writeFileSync(outFile, html);
|
|
1235
|
+
|
|
1236
|
+
// Print Markdown snippet
|
|
1237
|
+
const md = buildMarkdown(displayResults, {
|
|
1238
|
+
ecosystem,
|
|
1239
|
+
scannedFrom: filePath ? filePath.split('/').pop() : null,
|
|
1240
|
+
totalScanned: totalScanned || allResults.length,
|
|
1241
|
+
webUrl,
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
console.log(clr(c.bold, 'Markdown snippet') + clr(c.dim, ' (paste into GitHub issues, PRs, Slack):'));
|
|
1245
|
+
console.log(clr(c.dim, '─'.repeat(60)));
|
|
1246
|
+
console.log(md);
|
|
1247
|
+
console.log(clr(c.dim, '─'.repeat(60)));
|
|
1248
|
+
console.log();
|
|
1249
|
+
console.log(clr(c.green, ` ✓ HTML report saved → ${outFile}`));
|
|
1250
|
+
console.log(clr(c.cyan, ` 🔗 Web report: ${webUrl}`));
|
|
1251
|
+
console.log();
|
|
1252
|
+
}
|
|
1253
|
+
|
|
806
1254
|
async function main() {
|
|
807
1255
|
const args = process.argv.slice(2);
|
|
808
1256
|
|
|
@@ -811,9 +1259,74 @@ async function main() {
|
|
|
811
1259
|
process.exit(0);
|
|
812
1260
|
}
|
|
813
1261
|
|
|
814
|
-
//
|
|
1262
|
+
// Subcommands
|
|
815
1263
|
const subcmd = args[0];
|
|
816
1264
|
|
|
1265
|
+
if (subcmd === 'login') {
|
|
1266
|
+
const keyArg = args[1] || null;
|
|
1267
|
+
await cmdLogin(keyArg);
|
|
1268
|
+
process.exit(0);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
if (subcmd === 'status') {
|
|
1272
|
+
await cmdStatus();
|
|
1273
|
+
process.exit(0);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (subcmd === 'logout') {
|
|
1277
|
+
await cmdLogout();
|
|
1278
|
+
process.exit(0);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
if (subcmd === 'report') {
|
|
1282
|
+
// Parse report args (same flags as main scan)
|
|
1283
|
+
const reportArgs = args.slice(1);
|
|
1284
|
+
let ecosystem = 'npm';
|
|
1285
|
+
let packages = [];
|
|
1286
|
+
let filePath = null;
|
|
1287
|
+
let totalInFile = 0;
|
|
1288
|
+
|
|
1289
|
+
let ri = 0;
|
|
1290
|
+
while (ri < reportArgs.length) {
|
|
1291
|
+
const a = reportArgs[ri];
|
|
1292
|
+
if (a === '--pypi') { ecosystem = 'pypi'; ri++; }
|
|
1293
|
+
else if (a === '--npm') { ecosystem = 'npm'; ri++; }
|
|
1294
|
+
else if (a === '--cargo') { ecosystem = 'cargo'; ri++; }
|
|
1295
|
+
else if (a === '--golang' || a === '--go') { ecosystem = 'golang'; ri++; }
|
|
1296
|
+
else if (a === '--file' || a === '-f') { filePath = reportArgs[++ri]; ri++; }
|
|
1297
|
+
else if (a.startsWith('--')) { console.error(`Unknown flag: ${a}`); process.exit(1); }
|
|
1298
|
+
else { packages.push(a); ri++; }
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
if (!filePath && packages.length === 0) {
|
|
1302
|
+
const detected = await autodetectManifest(process.cwd());
|
|
1303
|
+
if (detected) {
|
|
1304
|
+
filePath = detected;
|
|
1305
|
+
console.log(clr(c.dim, `Auto-detected manifest: ${detected}`));
|
|
1306
|
+
} else {
|
|
1307
|
+
console.error('No packages specified and no manifest found. Run: poc report [packages...] or --file <manifest>');
|
|
1308
|
+
process.exit(1);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (filePath) {
|
|
1313
|
+
try {
|
|
1314
|
+
const result = await readPackagesFromFile(filePath);
|
|
1315
|
+
packages = result.packages;
|
|
1316
|
+
ecosystem = result.ecosystem;
|
|
1317
|
+
totalInFile = result.totalInFile || packages.length;
|
|
1318
|
+
console.log(clr(c.dim, `Detected ${totalInFile} packages from ${filePath} (${ecosystem})`));
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
console.error(`Error reading ${filePath}: ${err.message}`);
|
|
1321
|
+
process.exit(1);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
if (packages.length === 0) { console.error('No packages found.'); process.exit(1); }
|
|
1326
|
+
await cmdReport(packages, ecosystem, { filePath, totalScanned: totalInFile || packages.length });
|
|
1327
|
+
process.exit(0);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
817
1330
|
if (subcmd === 'watch') {
|
|
818
1331
|
const pkg = args[1];
|
|
819
1332
|
if (!pkg) { console.error('Usage: poc watch <package> [--ecosystem npm|pypi|cargo|golang]'); process.exit(1); }
|