proof-of-commitment 1.10.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.
Files changed (3) hide show
  1. package/README.md +11 -9
  2. package/index.js +307 -2
  3. 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
- **Commit Pro — daily monitoring + alerts (v1.9.0):**
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
- # Add packages to daily monitoring (requires Pro API key):
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
- # API key: set COMMIT_API_KEY env or add api_key=<key> to ~/.commit/config
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,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * proof-of-commitment CLI v1.10.0
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
  */
@@ -201,7 +201,7 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
201
201
 
202
202
  function printHelp() {
203
203
  console.log(`
204
- ${clr(c.bold, 'proof-of-commitment')} v1.10.0 — supply chain risk scorer
204
+ ${clr(c.bold, 'proof-of-commitment')} v1.11.0 — supply chain risk scorer
205
205
 
206
206
  ${clr(c.bold, 'Usage:')}
207
207
  npx proof-of-commitment Auto-detect manifest in current dir
@@ -218,6 +218,11 @@ ${clr(c.bold, 'Usage:')}
218
218
  npx proof-of-commitment --file go.mod Audit Go direct + indirect deps
219
219
  npx proof-of-commitment --file go.sum Audit Go full transitive set
220
220
 
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
+
221
226
  ${clr(c.bold, 'Account:')}
222
227
  poc login [key] Save and validate your API key (interactive or direct)
223
228
  poc status Show current tier, usage, and limits
@@ -995,6 +1000,257 @@ async function cmdUnwatch(pkg, ecosystem) {
995
1000
  }
996
1001
  }
997
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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
+
998
1254
  async function main() {
999
1255
  const args = process.argv.slice(2);
1000
1256
 
@@ -1022,6 +1278,55 @@ async function main() {
1022
1278
  process.exit(0);
1023
1279
  }
1024
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
+
1025
1330
  if (subcmd === 'watch') {
1026
1331
  const pkg = args[1];
1027
1332
  if (!pkg) { console.error('Usage: poc watch <package> [--ecosystem npm|pypi|cargo|golang]'); process.exit(1); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proof-of-commitment",
3
- "version": "1.10.0",
3
+ "version": "1.11.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": {