proof-of-commitment 1.10.0 → 1.12.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 +450 -2
- 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,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* proof-of-commitment CLI v1.
|
|
3
|
+
* proof-of-commitment CLI v1.12.0
|
|
4
4
|
* Scores npm/PyPI/Cargo/Go packages on behavioral commitment signals.
|
|
5
5
|
* Usage: npx proof-of-commitment [packages...] [options]
|
|
6
6
|
*/
|
|
@@ -182,6 +182,7 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
|
|
|
182
182
|
const topPkgs = results.slice(0, 10).map(r => r.name).join(',');
|
|
183
183
|
console.log(clr(c.cyan, `\n 🔗 Full report: ${WEB}?packages=${encodeURIComponent(topPkgs)}`));
|
|
184
184
|
console.log(clr(c.cyan, ` 🤖 GitHub Action: github.com/piiiico/commit-action — block CRITICAL packages in CI`));
|
|
185
|
+
console.log(clr(c.dim, ` 📋 Add to this project: `) + clr(c.cyan, `poc init`) + clr(c.dim, ` — creates workflow + README badge`));
|
|
185
186
|
|
|
186
187
|
// Contextual upsell — show when findings make monitoring relevant
|
|
187
188
|
if (effectiveCritical > 0) {
|
|
@@ -201,7 +202,7 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
|
|
|
201
202
|
|
|
202
203
|
function printHelp() {
|
|
203
204
|
console.log(`
|
|
204
|
-
${clr(c.bold, 'proof-of-commitment')} v1.
|
|
205
|
+
${clr(c.bold, 'proof-of-commitment')} v1.12.0 — supply chain risk scorer
|
|
205
206
|
|
|
206
207
|
${clr(c.bold, 'Usage:')}
|
|
207
208
|
npx proof-of-commitment Auto-detect manifest in current dir
|
|
@@ -218,6 +219,15 @@ ${clr(c.bold, 'Usage:')}
|
|
|
218
219
|
npx proof-of-commitment --file go.mod Audit Go direct + indirect deps
|
|
219
220
|
npx proof-of-commitment --file go.sum Audit Go full transitive set
|
|
220
221
|
|
|
222
|
+
${clr(c.bold, 'Setup:')}
|
|
223
|
+
poc init Add a GitHub Action + README badge to the current project
|
|
224
|
+
Auto-detects ecosystem. Blocks CRITICAL packages on every PR.
|
|
225
|
+
|
|
226
|
+
${clr(c.bold, 'Reports:')}
|
|
227
|
+
poc report Scan and generate a shareable HTML report + Markdown snippet
|
|
228
|
+
poc report [pkgs] Same flags as scan — packages, --pypi, --cargo, --file, etc.
|
|
229
|
+
Saves audit-report.html to cwd + prints Markdown for GitHub issues
|
|
230
|
+
|
|
221
231
|
${clr(c.bold, 'Account:')}
|
|
222
232
|
poc login [key] Save and validate your API key (interactive or direct)
|
|
223
233
|
poc status Show current tier, usage, and limits
|
|
@@ -995,6 +1005,390 @@ async function cmdUnwatch(pkg, ecosystem) {
|
|
|
995
1005
|
}
|
|
996
1006
|
}
|
|
997
1007
|
|
|
1008
|
+
/**
|
|
1009
|
+
* Generate a self-contained HTML report from audit results.
|
|
1010
|
+
* Returns the full HTML string.
|
|
1011
|
+
*/
|
|
1012
|
+
function buildHtmlReport(results, { ecosystem, scannedFrom, totalScanned } = {}) {
|
|
1013
|
+
const ts = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
|
|
1014
|
+
const topPkgs = results.slice(0, 20).map(r => r.name).join(',');
|
|
1015
|
+
const webUrl = `${WEB}?packages=${encodeURIComponent(topPkgs)}`;
|
|
1016
|
+
|
|
1017
|
+
const criticalCount = results.filter(r => hasCritical(r.riskFlags)).length;
|
|
1018
|
+
const healthyCount = results.filter(r => !hasCritical(r.riskFlags) && (r.score || 0) >= 75).length;
|
|
1019
|
+
|
|
1020
|
+
function riskBadge(pkg) {
|
|
1021
|
+
if (hasCritical(pkg.riskFlags)) return '<span class="badge critical">CRITICAL</span>';
|
|
1022
|
+
if ((pkg.score || 100) < 40) return '<span class="badge high">HIGH</span>';
|
|
1023
|
+
if ((pkg.score || 100) < 60) return '<span class="badge moderate">MODERATE</span>';
|
|
1024
|
+
if ((pkg.score || 100) < 75) return '<span class="badge good">GOOD</span>';
|
|
1025
|
+
return '<span class="badge healthy">HEALTHY</span>';
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function provBadge(pkg) {
|
|
1029
|
+
if (pkg.ecosystem === 'golang') return '';
|
|
1030
|
+
return pkg.hasProvenance ? '<span class="prov">🔐</span>' : '';
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function fmtDl(n) {
|
|
1034
|
+
if (!n) return '—';
|
|
1035
|
+
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B/wk';
|
|
1036
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M/wk';
|
|
1037
|
+
if (n >= 1e3) return (n / 1e3).toFixed(0) + 'K/wk';
|
|
1038
|
+
return n + '/wk';
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const rows = results.map(pkg => {
|
|
1042
|
+
const isGo = pkg.ecosystem === 'golang';
|
|
1043
|
+
return `<tr class="${hasCritical(pkg.riskFlags) ? 'row-critical' : ''}">
|
|
1044
|
+
<td class="pkg-name">${escHtml(pkg.name)}${provBadge(pkg)}</td>
|
|
1045
|
+
<td>${riskBadge(pkg)}</td>
|
|
1046
|
+
<td class="score">${pkg.score ?? '?'}</td>
|
|
1047
|
+
<td>${pkg.maintainers === 35 ? '30+' : (pkg.maintainers ?? '?')}</td>
|
|
1048
|
+
<td>${isGo ? '—' : fmtDl(pkg.weeklyDownloads)}</td>
|
|
1049
|
+
<td>${pkg.ageYears ? pkg.ageYears.toString().replace(/(\.\d).*/, '$1') + 'y' : '?'}</td>
|
|
1050
|
+
</tr>`;
|
|
1051
|
+
}).join('\n');
|
|
1052
|
+
|
|
1053
|
+
const summaryLabel = criticalCount > 0
|
|
1054
|
+
? `⚠ ${criticalCount} CRITICAL package${criticalCount > 1 ? 's' : ''} found`
|
|
1055
|
+
: `✓ No CRITICAL packages`;
|
|
1056
|
+
|
|
1057
|
+
const summaryClass = criticalCount > 0 ? 'summary-critical' : 'summary-ok';
|
|
1058
|
+
|
|
1059
|
+
const scannedLine = scannedFrom
|
|
1060
|
+
? `<span>Scanned from <code>${escHtml(scannedFrom)}</code></span> · `
|
|
1061
|
+
: '';
|
|
1062
|
+
const totalLine = totalScanned && totalScanned > results.length
|
|
1063
|
+
? `showing top ${results.length} of ${totalScanned} packages · `
|
|
1064
|
+
: `${results.length} package${results.length !== 1 ? 's' : ''} · `;
|
|
1065
|
+
|
|
1066
|
+
return `<!DOCTYPE html>
|
|
1067
|
+
<html lang="en">
|
|
1068
|
+
<head>
|
|
1069
|
+
<meta charset="UTF-8">
|
|
1070
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1071
|
+
<title>Supply chain audit — proof-of-commitment</title>
|
|
1072
|
+
<style>
|
|
1073
|
+
:root { --red:#ef4444;--orange:#f97316;--yellow:#eab308;--green:#22c55e;--cyan:#06b6d4;--bg:#0f172a;--surface:#1e293b;--border:#334155;--text:#f1f5f9;--muted:#94a3b8; }
|
|
1074
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1075
|
+
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; }
|
|
1076
|
+
.header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
|
|
1077
|
+
.logo { font-size: 1.1rem; font-weight: bold; color: var(--cyan); }
|
|
1078
|
+
.logo a { color: inherit; text-decoration: none; }
|
|
1079
|
+
.web-link { margin-left: auto; }
|
|
1080
|
+
.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; }
|
|
1081
|
+
.web-link a:hover { background: var(--surface); }
|
|
1082
|
+
.summary { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1.5rem; font-weight: bold; }
|
|
1083
|
+
.summary-critical { background: rgba(239,68,68,0.15); border: 1px solid var(--red); color: var(--red); }
|
|
1084
|
+
.summary-ok { background: rgba(34,197,94,0.1); border: 1px solid var(--green); color: var(--green); }
|
|
1085
|
+
.meta { color: var(--muted); font-size: 0.8rem; margin-bottom: 1.5rem; }
|
|
1086
|
+
.meta code { background: var(--surface); padding: 0.1rem 0.3rem; border-radius: 3px; }
|
|
1087
|
+
table { width: 100%; border-collapse: collapse; }
|
|
1088
|
+
thead { border-bottom: 1px solid var(--border); }
|
|
1089
|
+
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; }
|
|
1090
|
+
td { padding: 0.5rem 0.75rem; border-bottom: 1px solid rgba(51,65,85,0.5); }
|
|
1091
|
+
tr.row-critical { background: rgba(239,68,68,0.07); }
|
|
1092
|
+
.pkg-name { font-weight: bold; }
|
|
1093
|
+
.prov { margin-left: 0.4rem; font-size: 0.9em; }
|
|
1094
|
+
.badge { display: inline-block; padding: 0.1rem 0.5rem; border-radius: 3px; font-size: 0.75rem; font-weight: bold; }
|
|
1095
|
+
.badge.critical { background: rgba(239,68,68,0.2); color: var(--red); border: 1px solid var(--red); }
|
|
1096
|
+
.badge.high { background: rgba(249,115,22,0.15); color: var(--orange); border: 1px solid var(--orange); }
|
|
1097
|
+
.badge.moderate { background: rgba(234,179,8,0.15); color: var(--yellow); border: 1px solid var(--yellow); }
|
|
1098
|
+
.badge.good { background: rgba(234,179,8,0.1); color: var(--yellow); border: 1px solid rgba(234,179,8,0.5); }
|
|
1099
|
+
.badge.healthy { background: rgba(34,197,94,0.1); color: var(--green); border: 1px solid var(--green); }
|
|
1100
|
+
.score { color: var(--muted); }
|
|
1101
|
+
.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; }
|
|
1102
|
+
.footer a { color: var(--cyan); text-decoration: none; }
|
|
1103
|
+
.md-section { margin-top: 2rem; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 1rem; }
|
|
1104
|
+
.md-label { color: var(--muted); font-size: 0.75rem; margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
1105
|
+
.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); }
|
|
1106
|
+
.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; }
|
|
1107
|
+
.copy-btn:hover { background: var(--cyan); color: var(--bg); }
|
|
1108
|
+
</style>
|
|
1109
|
+
</head>
|
|
1110
|
+
<body>
|
|
1111
|
+
<div class="header">
|
|
1112
|
+
<div class="logo"><a href="${WEB}" target="_blank">proof-of-commitment</a></div>
|
|
1113
|
+
<div class="web-link"><a href="${webUrl}" target="_blank">🔗 Open in browser →</a></div>
|
|
1114
|
+
</div>
|
|
1115
|
+
<div class="summary ${summaryClass}">${summaryLabel}</div>
|
|
1116
|
+
<div class="meta">${scannedLine}${totalLine}${ecosystem || 'npm'} · generated ${ts}</div>
|
|
1117
|
+
<table>
|
|
1118
|
+
<thead><tr>
|
|
1119
|
+
<th>Package</th><th>Risk</th><th>Score</th><th>Publishers</th><th>Downloads</th><th>Age</th>
|
|
1120
|
+
</tr></thead>
|
|
1121
|
+
<tbody>
|
|
1122
|
+
${rows}
|
|
1123
|
+
</tbody>
|
|
1124
|
+
</table>
|
|
1125
|
+
<div class="md-section">
|
|
1126
|
+
<div class="md-label">Copy for GitHub issues / Slack <button class="copy-btn" onclick="copyMd()">Copy</button></div>
|
|
1127
|
+
<div class="md-copy" id="md-content">${escHtml(buildMarkdown(results, { ecosystem, scannedFrom, totalScanned, webUrl }))}</div>
|
|
1128
|
+
</div>
|
|
1129
|
+
<div class="footer">
|
|
1130
|
+
<span>Generated by <a href="${WEB}" target="_blank">proof-of-commitment</a></span>
|
|
1131
|
+
<span><a href="https://github.com/piiiico/commit-action" target="_blank">GitHub Action</a></span>
|
|
1132
|
+
<span><a href="https://getcommit.dev/pricing" target="_blank">Commit Pro</a></span>
|
|
1133
|
+
</div>
|
|
1134
|
+
<script>
|
|
1135
|
+
function copyMd() {
|
|
1136
|
+
const text = document.getElementById('md-content').textContent;
|
|
1137
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
1138
|
+
const btn = document.querySelector('.copy-btn');
|
|
1139
|
+
btn.textContent = '✓ Copied';
|
|
1140
|
+
setTimeout(() => btn.textContent = 'Copy', 2000);
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
</script>
|
|
1144
|
+
</body>
|
|
1145
|
+
</html>`;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function escHtml(str) {
|
|
1149
|
+
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function buildMarkdown(results, { ecosystem, scannedFrom, totalScanned, webUrl } = {}) {
|
|
1153
|
+
const criticalCount = results.filter(r => hasCritical(r.riskFlags)).length;
|
|
1154
|
+
const summaryLine = criticalCount > 0
|
|
1155
|
+
? `⚠ **${criticalCount} CRITICAL package${criticalCount > 1 ? 's' : ''} found**`
|
|
1156
|
+
: `✅ No CRITICAL packages`;
|
|
1157
|
+
|
|
1158
|
+
function riskEmoji(pkg) {
|
|
1159
|
+
if (hasCritical(pkg.riskFlags)) return '🔴 CRITICAL';
|
|
1160
|
+
if ((pkg.score || 100) < 40) return '🟠 HIGH';
|
|
1161
|
+
if ((pkg.score || 100) < 60) return '🟡 MODERATE';
|
|
1162
|
+
if ((pkg.score || 100) < 75) return '🟡 GOOD';
|
|
1163
|
+
return '🟢 HEALTHY';
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const header = `| Package | Risk | Score | Publishers | Downloads |
|
|
1167
|
+
|---------|------|-------|------------|-----------|`;
|
|
1168
|
+
|
|
1169
|
+
function fmtDl(n) {
|
|
1170
|
+
if (!n) return '—';
|
|
1171
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M/wk';
|
|
1172
|
+
if (n >= 1e3) return (n / 1e3).toFixed(0) + 'K/wk';
|
|
1173
|
+
return n + '/wk';
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const rows = results.map(pkg => {
|
|
1177
|
+
const maintDisplay = pkg.maintainers === 35 ? '30+' : (pkg.maintainers ?? '?');
|
|
1178
|
+
const dlDisplay = pkg.ecosystem === 'golang' ? '—' : fmtDl(pkg.weeklyDownloads);
|
|
1179
|
+
return `| ${pkg.name}${pkg.hasProvenance ? ' 🔐' : ''} | ${riskEmoji(pkg)} | ${pkg.score ?? '?'} | ${maintDisplay} | ${dlDisplay} |`;
|
|
1180
|
+
}).join('\n');
|
|
1181
|
+
|
|
1182
|
+
const scannedNote = scannedFrom ? ` (from \`${scannedFrom}\`)` : '';
|
|
1183
|
+
const totalNote = totalScanned && totalScanned > results.length ? `, top ${results.length} of ${totalScanned}` : '';
|
|
1184
|
+
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})*`;
|
|
1185
|
+
|
|
1186
|
+
return `## Supply chain audit\n\n${summaryLine}\n\n${header}\n${rows}${footer}`;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* poc report — generate shareable HTML report + Markdown snippet
|
|
1191
|
+
*/
|
|
1192
|
+
async function cmdReport(packages, ecosystem, { filePath, isLockfile, totalScanned } = {}) {
|
|
1193
|
+
const fs = await import('fs');
|
|
1194
|
+
|
|
1195
|
+
process.stdout.write(clr(c.dim, `Scoring ${packages.length} ${ecosystem} package${packages.length > 1 ? 's' : ''}...`));
|
|
1196
|
+
const t0 = Date.now();
|
|
1197
|
+
|
|
1198
|
+
let allResults;
|
|
1199
|
+
try {
|
|
1200
|
+
if (packages.length <= 20) {
|
|
1201
|
+
const res = await fetch(API, {
|
|
1202
|
+
method: 'POST',
|
|
1203
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1204
|
+
body: JSON.stringify({ packages, ecosystem }),
|
|
1205
|
+
});
|
|
1206
|
+
if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`);
|
|
1207
|
+
const data = await res.json();
|
|
1208
|
+
allResults = data.results || [];
|
|
1209
|
+
} else {
|
|
1210
|
+
allResults = await auditBatched(packages, ecosystem);
|
|
1211
|
+
}
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
console.error(`\nError: ${err.message}`);
|
|
1214
|
+
process.exit(1);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
|
1218
|
+
process.stdout.write(clr(c.dim, ` done in ${elapsed}s\n\n`));
|
|
1219
|
+
|
|
1220
|
+
// Sort: CRITICAL first, then by score ascending
|
|
1221
|
+
allResults.sort((a, b) => {
|
|
1222
|
+
const aCrit = hasCritical(a.riskFlags) ? 1 : 0;
|
|
1223
|
+
const bCrit = hasCritical(b.riskFlags) ? 1 : 0;
|
|
1224
|
+
if (aCrit !== bCrit) return bCrit - aCrit;
|
|
1225
|
+
return (a.score || 100) - (b.score || 100);
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
const displayResults = allResults.slice(0, 50);
|
|
1229
|
+
const topPkgs = displayResults.slice(0, 20).map(r => r.name).join(',');
|
|
1230
|
+
const webUrl = `${WEB}?packages=${encodeURIComponent(topPkgs)}`;
|
|
1231
|
+
|
|
1232
|
+
// Save HTML report
|
|
1233
|
+
const outFile = 'audit-report.html';
|
|
1234
|
+
const html = buildHtmlReport(displayResults, {
|
|
1235
|
+
ecosystem,
|
|
1236
|
+
scannedFrom: filePath ? filePath.split('/').pop() : null,
|
|
1237
|
+
totalScanned: totalScanned || allResults.length,
|
|
1238
|
+
});
|
|
1239
|
+
fs.writeFileSync(outFile, html);
|
|
1240
|
+
|
|
1241
|
+
// Print Markdown snippet
|
|
1242
|
+
const md = buildMarkdown(displayResults, {
|
|
1243
|
+
ecosystem,
|
|
1244
|
+
scannedFrom: filePath ? filePath.split('/').pop() : null,
|
|
1245
|
+
totalScanned: totalScanned || allResults.length,
|
|
1246
|
+
webUrl,
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
console.log(clr(c.bold, 'Markdown snippet') + clr(c.dim, ' (paste into GitHub issues, PRs, Slack):'));
|
|
1250
|
+
console.log(clr(c.dim, '─'.repeat(60)));
|
|
1251
|
+
console.log(md);
|
|
1252
|
+
console.log(clr(c.dim, '─'.repeat(60)));
|
|
1253
|
+
console.log();
|
|
1254
|
+
console.log(clr(c.green, ` ✓ HTML report saved → ${outFile}`));
|
|
1255
|
+
console.log(clr(c.cyan, ` 🔗 Web report: ${webUrl}`));
|
|
1256
|
+
console.log();
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* poc init — scaffold a GitHub Action workflow + README badge for the current project.
|
|
1261
|
+
* Turns every CLI user into a permanent distribution node.
|
|
1262
|
+
*/
|
|
1263
|
+
async function cmdInit() {
|
|
1264
|
+
const fs = await import('fs');
|
|
1265
|
+
const path = await import('path');
|
|
1266
|
+
const cwd = process.cwd();
|
|
1267
|
+
|
|
1268
|
+
// Detect ecosystem from project files
|
|
1269
|
+
let ecosystem = 'npm';
|
|
1270
|
+
let manifestName = 'package.json';
|
|
1271
|
+
const checks = [
|
|
1272
|
+
['package.json', 'npm'],
|
|
1273
|
+
['package-lock.json', 'npm'],
|
|
1274
|
+
['yarn.lock', 'npm'],
|
|
1275
|
+
['pnpm-lock.yaml', 'npm'],
|
|
1276
|
+
['requirements.txt', 'pypi'],
|
|
1277
|
+
['Cargo.toml', 'cargo'],
|
|
1278
|
+
['go.mod', 'golang'],
|
|
1279
|
+
];
|
|
1280
|
+
for (const [file, eco] of checks) {
|
|
1281
|
+
if (fs.existsSync(path.join(cwd, file))) {
|
|
1282
|
+
ecosystem = eco;
|
|
1283
|
+
manifestName = file;
|
|
1284
|
+
break;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
console.log(clr(c.bold, '\n Commit — supply chain audit for CI\n'));
|
|
1289
|
+
console.log(clr(c.dim, ` Detected: ${manifestName} (${ecosystem})`));
|
|
1290
|
+
|
|
1291
|
+
// ── 1. GitHub Action workflow ──
|
|
1292
|
+
const workflowDir = path.join(cwd, '.github', 'workflows');
|
|
1293
|
+
const workflowPath = path.join(workflowDir, 'commit-audit.yml');
|
|
1294
|
+
|
|
1295
|
+
const ecoFlag = ecosystem === 'npm' ? '' : ` --${ecosystem === 'golang' ? 'go' : ecosystem}`;
|
|
1296
|
+
const workflowContent = `# Supply chain audit — powered by Commit (getcommit.dev)
|
|
1297
|
+
# Scores dependencies on behavioral signals: publisher concentration,
|
|
1298
|
+
# download anomalies, release patterns, trusted publishing adoption.
|
|
1299
|
+
# Blocks PRs when CRITICAL packages are found (configurable).
|
|
1300
|
+
|
|
1301
|
+
name: Supply Chain Audit
|
|
1302
|
+
|
|
1303
|
+
on:
|
|
1304
|
+
pull_request:
|
|
1305
|
+
push:
|
|
1306
|
+
branches: [main, master]
|
|
1307
|
+
schedule:
|
|
1308
|
+
- cron: '0 9 * * 1' # Weekly Monday 09:00 UTC
|
|
1309
|
+
|
|
1310
|
+
jobs:
|
|
1311
|
+
audit:
|
|
1312
|
+
runs-on: ubuntu-latest
|
|
1313
|
+
steps:
|
|
1314
|
+
- uses: actions/checkout@v4
|
|
1315
|
+
- uses: actions/setup-node@v4
|
|
1316
|
+
with:
|
|
1317
|
+
node-version: '20'
|
|
1318
|
+
- run: npx -y proof-of-commitment${ecoFlag} --fail-on=critical
|
|
1319
|
+
`;
|
|
1320
|
+
|
|
1321
|
+
let workflowCreated = false;
|
|
1322
|
+
if (fs.existsSync(workflowPath)) {
|
|
1323
|
+
console.log(clr(c.yellow, ` ⚠ .github/workflows/commit-audit.yml already exists — skipped`));
|
|
1324
|
+
} else {
|
|
1325
|
+
fs.mkdirSync(workflowDir, { recursive: true });
|
|
1326
|
+
fs.writeFileSync(workflowPath, workflowContent);
|
|
1327
|
+
workflowCreated = true;
|
|
1328
|
+
console.log(clr(c.green, ` ✓ Created .github/workflows/commit-audit.yml`));
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// ── 2. README badge ──
|
|
1332
|
+
// Try to read project name from package.json or directory name
|
|
1333
|
+
let projectName = path.basename(cwd);
|
|
1334
|
+
try {
|
|
1335
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8'));
|
|
1336
|
+
if (pkg.name) projectName = pkg.name;
|
|
1337
|
+
} catch {}
|
|
1338
|
+
|
|
1339
|
+
const badgeUrl = `https://poc-backend.amdal-dev.workers.dev/badge/npm/${encodeURIComponent(projectName)}`;
|
|
1340
|
+
const auditUrl = `https://getcommit.dev/audit?packages=${encodeURIComponent(projectName)}`;
|
|
1341
|
+
const badgeMd = `[](${auditUrl})`;
|
|
1342
|
+
|
|
1343
|
+
console.log(clr(c.green, ` ✓ README badge (paste into your README.md):\n`));
|
|
1344
|
+
console.log(` ${badgeMd}\n`);
|
|
1345
|
+
|
|
1346
|
+
// ── 3. Try to auto-insert badge into README ──
|
|
1347
|
+
let badgeInserted = false;
|
|
1348
|
+
const readmeCandidates = ['README.md', 'readme.md', 'Readme.md'];
|
|
1349
|
+
for (const name of readmeCandidates) {
|
|
1350
|
+
const readmePath = path.join(cwd, name);
|
|
1351
|
+
if (fs.existsSync(readmePath)) {
|
|
1352
|
+
const content = fs.readFileSync(readmePath, 'utf-8');
|
|
1353
|
+
if (content.includes('Commit Score') || content.includes('poc-backend.amdal-dev.workers.dev/badge')) {
|
|
1354
|
+
console.log(clr(c.dim, ` Badge already in ${name} — skipped`));
|
|
1355
|
+
badgeInserted = true;
|
|
1356
|
+
break;
|
|
1357
|
+
}
|
|
1358
|
+
// Insert after the first H1 heading, or at the top
|
|
1359
|
+
const h1Match = content.match(/^#\s+.+$/m);
|
|
1360
|
+
let newContent;
|
|
1361
|
+
if (h1Match) {
|
|
1362
|
+
const insertPos = h1Match.index + h1Match[0].length;
|
|
1363
|
+
newContent = content.slice(0, insertPos) + '\n\n' + badgeMd + content.slice(insertPos);
|
|
1364
|
+
} else {
|
|
1365
|
+
newContent = badgeMd + '\n\n' + content;
|
|
1366
|
+
}
|
|
1367
|
+
fs.writeFileSync(readmePath, newContent);
|
|
1368
|
+
badgeInserted = true;
|
|
1369
|
+
console.log(clr(c.green, ` ✓ Badge added to ${name}`));
|
|
1370
|
+
break;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
if (!badgeInserted) {
|
|
1374
|
+
console.log(clr(c.dim, ` No README found — paste the badge manually`));
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// ── 4. Next steps ──
|
|
1378
|
+
console.log(clr(c.bold, '\n What happens next:\n'));
|
|
1379
|
+
if (workflowCreated) {
|
|
1380
|
+
console.log(clr(c.white, ' 1. Commit and push — the Action runs on your next PR'));
|
|
1381
|
+
console.log(clr(c.white, ' 2. PRs with CRITICAL dependencies are blocked automatically'));
|
|
1382
|
+
console.log(clr(c.white, ' 3. The badge updates daily with your project\'s score'));
|
|
1383
|
+
} else {
|
|
1384
|
+
console.log(clr(c.white, ' 1. The badge updates daily with your project\'s score'));
|
|
1385
|
+
console.log(clr(c.white, ' 2. Push to trigger the existing workflow'));
|
|
1386
|
+
}
|
|
1387
|
+
console.log(clr(c.dim, `\n Want daily monitoring + alerts? Get a free key:`));
|
|
1388
|
+
console.log(clr(c.cyan, ' https://getcommit.dev/get-started'));
|
|
1389
|
+
console.log(clr(c.dim, ' Then run: ') + clr(c.cyan, 'poc login') + clr(c.dim, ' + ') + clr(c.cyan, 'poc watch <package>\n'));
|
|
1390
|
+
}
|
|
1391
|
+
|
|
998
1392
|
async function main() {
|
|
999
1393
|
const args = process.argv.slice(2);
|
|
1000
1394
|
|
|
@@ -1006,6 +1400,11 @@ async function main() {
|
|
|
1006
1400
|
// Subcommands
|
|
1007
1401
|
const subcmd = args[0];
|
|
1008
1402
|
|
|
1403
|
+
if (subcmd === 'init') {
|
|
1404
|
+
await cmdInit();
|
|
1405
|
+
process.exit(0);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1009
1408
|
if (subcmd === 'login') {
|
|
1010
1409
|
const keyArg = args[1] || null;
|
|
1011
1410
|
await cmdLogin(keyArg);
|
|
@@ -1022,6 +1421,55 @@ async function main() {
|
|
|
1022
1421
|
process.exit(0);
|
|
1023
1422
|
}
|
|
1024
1423
|
|
|
1424
|
+
if (subcmd === 'report') {
|
|
1425
|
+
// Parse report args (same flags as main scan)
|
|
1426
|
+
const reportArgs = args.slice(1);
|
|
1427
|
+
let ecosystem = 'npm';
|
|
1428
|
+
let packages = [];
|
|
1429
|
+
let filePath = null;
|
|
1430
|
+
let totalInFile = 0;
|
|
1431
|
+
|
|
1432
|
+
let ri = 0;
|
|
1433
|
+
while (ri < reportArgs.length) {
|
|
1434
|
+
const a = reportArgs[ri];
|
|
1435
|
+
if (a === '--pypi') { ecosystem = 'pypi'; ri++; }
|
|
1436
|
+
else if (a === '--npm') { ecosystem = 'npm'; ri++; }
|
|
1437
|
+
else if (a === '--cargo') { ecosystem = 'cargo'; ri++; }
|
|
1438
|
+
else if (a === '--golang' || a === '--go') { ecosystem = 'golang'; ri++; }
|
|
1439
|
+
else if (a === '--file' || a === '-f') { filePath = reportArgs[++ri]; ri++; }
|
|
1440
|
+
else if (a.startsWith('--')) { console.error(`Unknown flag: ${a}`); process.exit(1); }
|
|
1441
|
+
else { packages.push(a); ri++; }
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
if (!filePath && packages.length === 0) {
|
|
1445
|
+
const detected = await autodetectManifest(process.cwd());
|
|
1446
|
+
if (detected) {
|
|
1447
|
+
filePath = detected;
|
|
1448
|
+
console.log(clr(c.dim, `Auto-detected manifest: ${detected}`));
|
|
1449
|
+
} else {
|
|
1450
|
+
console.error('No packages specified and no manifest found. Run: poc report [packages...] or --file <manifest>');
|
|
1451
|
+
process.exit(1);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
if (filePath) {
|
|
1456
|
+
try {
|
|
1457
|
+
const result = await readPackagesFromFile(filePath);
|
|
1458
|
+
packages = result.packages;
|
|
1459
|
+
ecosystem = result.ecosystem;
|
|
1460
|
+
totalInFile = result.totalInFile || packages.length;
|
|
1461
|
+
console.log(clr(c.dim, `Detected ${totalInFile} packages from ${filePath} (${ecosystem})`));
|
|
1462
|
+
} catch (err) {
|
|
1463
|
+
console.error(`Error reading ${filePath}: ${err.message}`);
|
|
1464
|
+
process.exit(1);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
if (packages.length === 0) { console.error('No packages found.'); process.exit(1); }
|
|
1469
|
+
await cmdReport(packages, ecosystem, { filePath, totalScanned: totalInFile || packages.length });
|
|
1470
|
+
process.exit(0);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1025
1473
|
if (subcmd === 'watch') {
|
|
1026
1474
|
const pkg = args[1];
|
|
1027
1475
|
if (!pkg) { console.error('Usage: poc watch <package> [--ecosystem npm|pypi|cargo|golang]'); process.exit(1); }
|