npm-audit-report-cli 1.0.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/dist/cli.cjs ADDED
@@ -0,0 +1,760 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/cli.ts
5
+ var import_node_fs = require("fs");
6
+
7
+ // src/parse.ts
8
+ var ParseError = class extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "ParseError";
12
+ }
13
+ };
14
+ var VALID_SEVERITIES = ["critical", "high", "moderate", "low", "info"];
15
+ function normalizeSeverity(value) {
16
+ if (typeof value === "string" && VALID_SEVERITIES.includes(value)) {
17
+ return value;
18
+ }
19
+ return "info";
20
+ }
21
+ function buildSummary(vulns, metaCounts) {
22
+ const summary = {
23
+ critical: 0,
24
+ high: 0,
25
+ moderate: 0,
26
+ low: 0,
27
+ info: 0,
28
+ total: 0
29
+ };
30
+ for (const v of vulns) {
31
+ summary[v.severity] += 1;
32
+ summary.total += 1;
33
+ }
34
+ if (metaCounts && summary.total === 0) {
35
+ return {
36
+ critical: metaCounts.critical ?? 0,
37
+ high: metaCounts.high ?? 0,
38
+ moderate: metaCounts.moderate ?? 0,
39
+ low: metaCounts.low ?? 0,
40
+ info: metaCounts.info ?? 0,
41
+ total: metaCounts.total ?? (metaCounts.critical ?? 0) + (metaCounts.high ?? 0) + (metaCounts.moderate ?? 0) + (metaCounts.low ?? 0) + (metaCounts.info ?? 0)
42
+ };
43
+ }
44
+ return summary;
45
+ }
46
+ function buildMeta(raw) {
47
+ const totalDependencies = raw && typeof raw["totalDependencies"] === "number" ? raw["totalDependencies"] : 0;
48
+ return {
49
+ npmVersion: "",
50
+ nodeVersion: typeof process !== "undefined" && process.version ? process.version : "",
51
+ auditedAt: (/* @__PURE__ */ new Date()).toISOString(),
52
+ totalDependencies
53
+ };
54
+ }
55
+ function parseV1(data) {
56
+ const advisories = data["advisories"] ?? {};
57
+ const vulns = [];
58
+ for (const key of Object.keys(advisories)) {
59
+ const adv = advisories[key];
60
+ if (!adv) continue;
61
+ const cves = Array.isArray(adv["cves"]) ? adv["cves"] : [];
62
+ const firstCve = cves.length > 0 ? cves[0] : void 0;
63
+ const advisoryId = adv["id"];
64
+ const idStr = firstCve ?? (typeof advisoryId === "number" || typeof advisoryId === "string" ? `npm-advisory-${String(advisoryId)}` : `npm-advisory-${key}`);
65
+ const findings = Array.isArray(adv["findings"]) ? adv["findings"] : [];
66
+ const paths = [];
67
+ for (const f of findings) {
68
+ const p = f["paths"];
69
+ if (Array.isArray(p)) {
70
+ for (const path of p) {
71
+ if (typeof path === "string") paths.push(path);
72
+ }
73
+ }
74
+ }
75
+ const fixAvailable = adv["fixAvailable"] === true;
76
+ const name = typeof adv["module_name"] === "string" ? adv["module_name"] : typeof adv["name"] === "string" ? adv["name"] : paths[0] ?? "unknown";
77
+ const range = typeof adv["vulnerable_versions"] === "string" ? adv["vulnerable_versions"] : "*";
78
+ const patched = typeof adv["patched_versions"] === "string" ? adv["patched_versions"] : void 0;
79
+ const vuln = {
80
+ id: idStr,
81
+ name,
82
+ severity: normalizeSeverity(adv["severity"]),
83
+ title: typeof adv["title"] === "string" ? adv["title"] : "Unknown vulnerability",
84
+ url: typeof adv["url"] === "string" ? adv["url"] : "",
85
+ range,
86
+ fixAvailable,
87
+ paths
88
+ };
89
+ if (fixAvailable && patched && patched !== "<0.0.0") {
90
+ vuln.fixCommand = `npm install ${name}@${patched}`;
91
+ } else if (fixAvailable) {
92
+ vuln.fixCommand = `npm audit fix`;
93
+ }
94
+ vulns.push(vuln);
95
+ }
96
+ const metadata = data["metadata"] ?? {};
97
+ const metaCounts = metadata["vulnerabilities"] ?? void 0;
98
+ return {
99
+ meta: buildMeta(metadata),
100
+ summary: buildSummary(vulns, metaCounts),
101
+ vulnerabilities: vulns
102
+ };
103
+ }
104
+ function parseV2(data) {
105
+ const vulnsRaw = data["vulnerabilities"] ?? {};
106
+ const vulns = [];
107
+ for (const name of Object.keys(vulnsRaw)) {
108
+ const entry = vulnsRaw[name];
109
+ if (!entry) continue;
110
+ const via = Array.isArray(entry["via"]) ? entry["via"] : [];
111
+ let title = "Unknown vulnerability";
112
+ let url = "";
113
+ let range = typeof entry["range"] === "string" ? entry["range"] : "*";
114
+ let id = `npm:${name}`;
115
+ for (const v of via) {
116
+ if (typeof v === "object" && v !== null) {
117
+ if (typeof v["title"] === "string") title = v["title"];
118
+ if (typeof v["url"] === "string") url = v["url"];
119
+ if (typeof v["range"] === "string") range = v["range"];
120
+ if (typeof v["source"] === "number" || typeof v["source"] === "string") {
121
+ id = `GHSA-${String(v["source"])}`;
122
+ }
123
+ break;
124
+ }
125
+ }
126
+ const fix = entry["fixAvailable"];
127
+ let fixAvailable = false;
128
+ let fixCommand;
129
+ if (fix === true) {
130
+ fixAvailable = true;
131
+ fixCommand = `npm audit fix`;
132
+ } else if (typeof fix === "object" && fix !== null) {
133
+ fixAvailable = true;
134
+ const fixObj = fix;
135
+ const fixName = typeof fixObj["name"] === "string" ? fixObj["name"] : name;
136
+ const fixVersion = typeof fixObj["version"] === "string" ? fixObj["version"] : void 0;
137
+ if (fixVersion) {
138
+ fixCommand = `npm install ${fixName}@${fixVersion}`;
139
+ } else {
140
+ fixCommand = `npm audit fix`;
141
+ }
142
+ const isSemVerMajor = fixObj["isSemVerMajor"] === true;
143
+ if (isSemVerMajor) {
144
+ fixCommand = `npm audit fix --force`;
145
+ }
146
+ }
147
+ const nodes = Array.isArray(entry["nodes"]) ? entry["nodes"] : [];
148
+ const vuln = {
149
+ id,
150
+ name: typeof entry["name"] === "string" ? entry["name"] : name,
151
+ severity: normalizeSeverity(entry["severity"]),
152
+ title,
153
+ url,
154
+ range,
155
+ fixAvailable,
156
+ paths: nodes.filter((n) => typeof n === "string")
157
+ };
158
+ if (fixCommand) vuln.fixCommand = fixCommand;
159
+ vulns.push(vuln);
160
+ }
161
+ const metadata = data["metadata"] ?? {};
162
+ const metaCounts = metadata["vulnerabilities"] ?? void 0;
163
+ return {
164
+ meta: buildMeta(metadata),
165
+ summary: buildSummary(vulns, metaCounts),
166
+ vulnerabilities: vulns
167
+ };
168
+ }
169
+ function looksLikeV2(data) {
170
+ const v = data["vulnerabilities"];
171
+ if (typeof v !== "object" || v === null) return false;
172
+ return !Array.isArray(v);
173
+ }
174
+ function looksLikeV1(data) {
175
+ return typeof data["advisories"] === "object" && data["advisories"] !== null;
176
+ }
177
+ function parse(input) {
178
+ let data;
179
+ try {
180
+ data = JSON.parse(input);
181
+ } catch {
182
+ throw new ParseError("Invalid JSON input");
183
+ }
184
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
185
+ throw new ParseError("Unrecognized npm audit schema");
186
+ }
187
+ const obj = data;
188
+ const version = obj["auditReportVersion"];
189
+ if (version === 1) return parseV1(obj);
190
+ if (version === 2) return parseV2(obj);
191
+ if (looksLikeV2(obj)) return parseV2(obj);
192
+ if (looksLikeV1(obj)) return parseV1(obj);
193
+ throw new ParseError("Unrecognized npm audit schema");
194
+ }
195
+
196
+ // src/threshold.ts
197
+ var SEVERITY_ORDER = ["info", "low", "moderate", "high", "critical"];
198
+ function meetsThreshold(severity, threshold) {
199
+ return SEVERITY_ORDER.indexOf(severity) >= SEVERITY_ORDER.indexOf(threshold);
200
+ }
201
+ function shouldFail(report, failOn) {
202
+ if (failOn === "none") return false;
203
+ return report.vulnerabilities.some((v) => meetsThreshold(v.severity, failOn));
204
+ }
205
+ function filterBySeverity(vulns, minSeverity) {
206
+ return vulns.filter((v) => meetsThreshold(v.severity, minSeverity));
207
+ }
208
+
209
+ // src/formatters/markdown.ts
210
+ var SEVERITY_ORDER2 = ["critical", "high", "moderate", "low", "info"];
211
+ var SEVERITY_EMOJI = {
212
+ critical: "\u{1F534}",
213
+ high: "\u{1F7E0}",
214
+ moderate: "\u{1F7E1}",
215
+ low: "\u{1F535}",
216
+ info: "\u26AA"
217
+ };
218
+ var SEVERITY_LABEL = {
219
+ critical: "Critical",
220
+ high: "High",
221
+ moderate: "Moderate",
222
+ low: "Low",
223
+ info: "Info"
224
+ };
225
+ function countBySeverity(vulns) {
226
+ const result = {
227
+ critical: { count: 0, fixable: 0 },
228
+ high: { count: 0, fixable: 0 },
229
+ moderate: { count: 0, fixable: 0 },
230
+ low: { count: 0, fixable: 0 },
231
+ info: { count: 0, fixable: 0 }
232
+ };
233
+ for (const v of vulns) {
234
+ result[v.severity].count += 1;
235
+ if (v.fixAvailable) result[v.severity].fixable += 1;
236
+ }
237
+ return result;
238
+ }
239
+ function renderVulnerability(v) {
240
+ const lines = [];
241
+ lines.push(`### ${v.name}`);
242
+ lines.push(`- **ID:** ${v.id}`);
243
+ lines.push(`- **Title:** ${v.title}`);
244
+ lines.push(`- **Range:** \`${v.range}\``);
245
+ if (v.fixAvailable && v.fixCommand) {
246
+ lines.push(`- **Fix:** \`${v.fixCommand}\``);
247
+ } else {
248
+ lines.push(`- **Fix:** No fix available`);
249
+ }
250
+ if (v.url) lines.push(`- **Advisory:** ${v.url}`);
251
+ if (v.paths.length > 0) lines.push(`- **Paths:** \`${v.paths.join(", ")}\``);
252
+ return lines.join("\n");
253
+ }
254
+ function markdown(report, opts = {}) {
255
+ const title = opts.title ?? "npm audit report";
256
+ const minSeverity = opts.severity ?? "info";
257
+ const filtered = filterBySeverity(report.vulnerabilities, minSeverity);
258
+ const out = [];
259
+ out.push(`## ${title}`);
260
+ out.push("");
261
+ if (filtered.length === 0) {
262
+ out.push(`## \u2705 No vulnerabilities found`);
263
+ out.push("");
264
+ const date2 = report.meta.auditedAt ? report.meta.auditedAt.slice(0, 10) : "";
265
+ out.push(
266
+ `> 0 vulnerabilities found \xB7 audited ${date2} \xB7 ${report.meta.totalDependencies} dependencies`
267
+ );
268
+ out.push("");
269
+ out.push(`---`);
270
+ out.push(`*Generated by [audit-report](https://github.com/dimasdarfi/audit-report)*`);
271
+ return out.join("\n");
272
+ }
273
+ const date = report.meta.auditedAt ? report.meta.auditedAt.slice(0, 10) : "";
274
+ out.push(
275
+ `> ${filtered.length} vulnerabilities found \xB7 audited ${date} \xB7 ${report.meta.totalDependencies} dependencies`
276
+ );
277
+ out.push("");
278
+ const counts = countBySeverity(filtered);
279
+ out.push(`| Severity | Count | Fixable |`);
280
+ out.push(`|----------|-------|---------|`);
281
+ for (const sev of SEVERITY_ORDER2) {
282
+ const c = counts[sev];
283
+ if (c.count === 0) continue;
284
+ out.push(`| ${SEVERITY_EMOJI[sev]} ${SEVERITY_LABEL[sev]} | ${c.count} | ${c.fixable} |`);
285
+ }
286
+ out.push("");
287
+ out.push("---");
288
+ out.push("");
289
+ for (const sev of SEVERITY_ORDER2) {
290
+ const group = filtered.filter((v) => v.severity === sev);
291
+ if (group.length === 0) continue;
292
+ out.push(`<details>`);
293
+ out.push(`<summary>${SEVERITY_EMOJI[sev]} ${SEVERITY_LABEL[sev]} (${group.length})</summary>`);
294
+ out.push("");
295
+ for (const v of group) {
296
+ out.push(renderVulnerability(v));
297
+ out.push("");
298
+ }
299
+ out.push(`</details>`);
300
+ out.push("");
301
+ }
302
+ out.push("---");
303
+ out.push(`*Generated by [audit-report](https://github.com/dimasdarfi/audit-report)*`);
304
+ return out.join("\n");
305
+ }
306
+
307
+ // src/formatters/html.ts
308
+ var SEVERITY_ORDER3 = ["critical", "high", "moderate", "low", "info"];
309
+ var SEVERITY_COLOR = {
310
+ critical: "#fee2e2",
311
+ high: "#ffedd5",
312
+ moderate: "#fef9c3",
313
+ low: "#dbeafe",
314
+ info: "#f0fdf4"
315
+ };
316
+ var SEVERITY_LABEL2 = {
317
+ critical: "Critical",
318
+ high: "High",
319
+ moderate: "Moderate",
320
+ low: "Low",
321
+ info: "Info"
322
+ };
323
+ var SEVERITY_RANK = {
324
+ critical: 5,
325
+ high: 4,
326
+ moderate: 3,
327
+ low: 2,
328
+ info: 1
329
+ };
330
+ function escapeHtml(s) {
331
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
332
+ }
333
+ function countBySeverity2(vulns) {
334
+ const r = { critical: 0, high: 0, moderate: 0, low: 0, info: 0 };
335
+ for (const v of vulns) r[v.severity] += 1;
336
+ return r;
337
+ }
338
+ function html(report, opts = {}) {
339
+ const title = opts.title ?? "npm audit report";
340
+ const minSeverity = opts.severity ?? "info";
341
+ const filtered = filterBySeverity(report.vulnerabilities, minSeverity);
342
+ const date = report.meta.auditedAt ? report.meta.auditedAt.slice(0, 10) : "";
343
+ const styles = `
344
+ * { box-sizing: border-box; }
345
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 2rem; color: #1f2937; background: #f9fafb; }
346
+ .container { max-width: 1100px; margin: 0 auto; background: #ffffff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); padding: 2rem; }
347
+ h1 { margin: 0 0 0.5rem; font-size: 1.6rem; }
348
+ .meta { color: #6b7280; font-size: 0.9rem; margin-bottom: 1.5rem; }
349
+ table { width: 100%; border-collapse: collapse; margin-bottom: 1.5rem; font-size: 0.95rem; }
350
+ th, td { padding: 0.6rem 0.75rem; text-align: left; border-bottom: 1px solid #e5e7eb; }
351
+ th { background: #f3f4f6; cursor: pointer; user-select: none; font-weight: 600; }
352
+ th[data-sort] .arrow { color: #9ca3af; font-size: 0.75rem; margin-left: 0.25rem; }
353
+ th.sorted-asc .arrow::after { content: " \u25B2"; color: #1f2937; }
354
+ th.sorted-desc .arrow::after { content: " \u25BC"; color: #1f2937; }
355
+ tr.sev-critical { background: ${SEVERITY_COLOR.critical}; }
356
+ tr.sev-high { background: ${SEVERITY_COLOR.high}; }
357
+ tr.sev-moderate { background: ${SEVERITY_COLOR.moderate}; }
358
+ tr.sev-low { background: ${SEVERITY_COLOR.low}; }
359
+ tr.sev-info { background: ${SEVERITY_COLOR.info}; }
360
+ details { margin-bottom: 0.5rem; }
361
+ summary { cursor: pointer; padding: 0.4rem 0; font-weight: 600; }
362
+ .badge { display: inline-block; padding: 0.15rem 0.55rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
363
+ .badge-critical { background: #dc2626; color: white; }
364
+ .badge-high { background: #ea580c; color: white; }
365
+ .badge-moderate { background: #ca8a04; color: white; }
366
+ .badge-low { background: #2563eb; color: white; }
367
+ .badge-info { background: #16a34a; color: white; }
368
+ code { background: #f3f4f6; padding: 0.1rem 0.3rem; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; font-size: 0.85rem; }
369
+ .empty { padding: 2rem; text-align: center; color: #6b7280; }
370
+ .empty-icon { font-size: 3rem; }
371
+ .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 0.75rem; margin-bottom: 1.5rem; }
372
+ .summary-card { padding: 0.85rem; border-radius: 6px; border: 1px solid #e5e7eb; }
373
+ .summary-card .label { font-size: 0.75rem; text-transform: uppercase; color: #6b7280; font-weight: 600; letter-spacing: 0.03em; }
374
+ .summary-card .value { font-size: 1.4rem; font-weight: 700; margin-top: 0.15rem; }
375
+ footer { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 0.85rem; text-align: center; }
376
+ @media print {
377
+ body { background: white; padding: 0; }
378
+ .container { box-shadow: none; padding: 0; }
379
+ th { cursor: default; }
380
+ th .arrow { display: none; }
381
+ details { break-inside: avoid; }
382
+ details[open] summary { display: list-item; }
383
+ }
384
+ `;
385
+ const counts = countBySeverity2(filtered);
386
+ let body = "";
387
+ if (filtered.length === 0) {
388
+ body = `<div class="empty"><div class="empty-icon">\u2705</div><h2>No vulnerabilities found</h2><p>Audited ${report.meta.totalDependencies} dependencies</p></div>`;
389
+ } else {
390
+ const summaryCards = SEVERITY_ORDER3.map((sev) => {
391
+ const c = counts[sev];
392
+ return `<div class="summary-card" style="background:${SEVERITY_COLOR[sev]}"><div class="label">${SEVERITY_LABEL2[sev]}</div><div class="value">${c}</div></div>`;
393
+ }).join("");
394
+ const rows = filtered.map((v) => {
395
+ const fix = v.fixAvailable && v.fixCommand ? `<code>${escapeHtml(v.fixCommand)}</code>` : "No fix";
396
+ return `<tr class="sev-${v.severity}" data-severity="${SEVERITY_RANK[v.severity]}" data-name="${escapeHtml(v.name)}">
397
+ <td><span class="badge badge-${v.severity}">${SEVERITY_LABEL2[v.severity]}</span></td>
398
+ <td><strong>${escapeHtml(v.name)}</strong></td>
399
+ <td>${escapeHtml(v.title)}</td>
400
+ <td><code>${escapeHtml(v.range)}</code></td>
401
+ <td>${fix}</td>
402
+ </tr>`;
403
+ }).join("\n");
404
+ body = `
405
+ <div class="summary-grid">${summaryCards}</div>
406
+ <table id="vuln-table">
407
+ <thead>
408
+ <tr>
409
+ <th data-sort="severity">Severity<span class="arrow"></span></th>
410
+ <th data-sort="name">Package<span class="arrow"></span></th>
411
+ <th data-sort="title">Title<span class="arrow"></span></th>
412
+ <th data-sort="range">Range<span class="arrow"></span></th>
413
+ <th data-sort="fix">Fix<span class="arrow"></span></th>
414
+ </tr>
415
+ </thead>
416
+ <tbody>
417
+ ${rows}
418
+ </tbody>
419
+ </table>
420
+ `;
421
+ }
422
+ const sortScript = `
423
+ (function(){
424
+ var table = document.getElementById('vuln-table');
425
+ if (!table) return;
426
+ var headers = table.querySelectorAll('th[data-sort]');
427
+ headers.forEach(function(h, idx){
428
+ h.addEventListener('click', function(){
429
+ var asc = !h.classList.contains('sorted-asc');
430
+ headers.forEach(function(other){ other.classList.remove('sorted-asc','sorted-desc'); });
431
+ h.classList.add(asc ? 'sorted-asc' : 'sorted-desc');
432
+ var tbody = table.querySelector('tbody');
433
+ var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr'));
434
+ rows.sort(function(a, b){
435
+ var key = h.getAttribute('data-sort');
436
+ var av, bv;
437
+ if (key === 'severity') {
438
+ av = parseInt(a.getAttribute('data-severity'), 10);
439
+ bv = parseInt(b.getAttribute('data-severity'), 10);
440
+ } else {
441
+ av = a.children[idx].textContent.trim().toLowerCase();
442
+ bv = b.children[idx].textContent.trim().toLowerCase();
443
+ }
444
+ if (av < bv) return asc ? -1 : 1;
445
+ if (av > bv) return asc ? 1 : -1;
446
+ return 0;
447
+ });
448
+ rows.forEach(function(r){ tbody.appendChild(r); });
449
+ });
450
+ });
451
+ })();
452
+ `;
453
+ return `<!DOCTYPE html>
454
+ <html lang="en">
455
+ <head>
456
+ <meta charset="utf-8">
457
+ <meta name="viewport" content="width=device-width, initial-scale=1">
458
+ <title>${escapeHtml(title)} \u2014 ${escapeHtml(date)}</title>
459
+ <style>${styles}</style>
460
+ </head>
461
+ <body>
462
+ <div class="container">
463
+ <h1>${escapeHtml(title)}</h1>
464
+ <div class="meta">Audited ${escapeHtml(date)} \xB7 ${report.meta.totalDependencies} dependencies \xB7 ${filtered.length} vulnerabilities</div>
465
+ ${body}
466
+ <footer>Generated by audit-report</footer>
467
+ </div>
468
+ <script>${sortScript}</script>
469
+ </body>
470
+ </html>`;
471
+ }
472
+
473
+ // src/formatters/sarif.ts
474
+ function severityToLevel(s) {
475
+ if (s === "critical" || s === "high") return "error";
476
+ if (s === "moderate") return "warning";
477
+ return "note";
478
+ }
479
+ function sarif(report, opts = {}) {
480
+ const minSeverity = opts.severity ?? "info";
481
+ const filtered = filterBySeverity(report.vulnerabilities, minSeverity);
482
+ const rulesMap = /* @__PURE__ */ new Map();
483
+ for (const v of filtered) {
484
+ if (!rulesMap.has(v.id)) {
485
+ const rule = {
486
+ id: v.id,
487
+ name: v.name,
488
+ shortDescription: { text: v.title },
489
+ properties: { severity: v.severity }
490
+ };
491
+ if (v.url) rule.helpUri = v.url;
492
+ rulesMap.set(v.id, rule);
493
+ }
494
+ }
495
+ const results = filtered.map((v) => {
496
+ const fixText = v.fixAvailable && v.fixCommand ? `Fix: ${v.fixCommand}` : "No fix available.";
497
+ const messageText = `${v.title}. Affected range: ${v.range}. ${fixText}`;
498
+ return {
499
+ ruleId: v.id,
500
+ level: severityToLevel(v.severity),
501
+ message: { text: messageText },
502
+ locations: [
503
+ {
504
+ physicalLocation: {
505
+ artifactLocation: { uri: "package.json" }
506
+ }
507
+ }
508
+ ]
509
+ };
510
+ });
511
+ const doc = {
512
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
513
+ version: "2.1.0",
514
+ runs: [
515
+ {
516
+ tool: {
517
+ driver: {
518
+ name: "npm-audit",
519
+ version: report.meta.npmVersion || "0.0.0",
520
+ informationUri: "https://docs.npmjs.com/cli/commands/npm-audit",
521
+ rules: Array.from(rulesMap.values())
522
+ }
523
+ },
524
+ results
525
+ }
526
+ ]
527
+ };
528
+ return JSON.stringify(doc, null, 2);
529
+ }
530
+
531
+ // src/formatters/annotations.ts
532
+ function severityToAnnotation(s) {
533
+ if (s === "critical" || s === "high") return "error";
534
+ if (s === "moderate") return "warning";
535
+ return "notice";
536
+ }
537
+ function escapeAnnotation(s) {
538
+ return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
539
+ }
540
+ function escapeProperty(s) {
541
+ return escapeAnnotation(s).replace(/:/g, "%3A").replace(/,/g, "%2C");
542
+ }
543
+ function annotations(report, opts = {}) {
544
+ const minSeverity = opts.severity ?? "info";
545
+ const filtered = filterBySeverity(report.vulnerabilities, minSeverity);
546
+ const lines = filtered.map((v) => {
547
+ const level = severityToAnnotation(v.severity);
548
+ const fixPart = v.fixAvailable && v.fixCommand ? `fix: ${v.fixCommand}` : "no fix";
549
+ const message = `${v.name} \u2014 ${v.title}, affected: ${v.range} (${fixPart})`;
550
+ const titleProp = escapeProperty(v.id);
551
+ return `::${level} file=package.json,title=${titleProp}::${escapeAnnotation(message)}`;
552
+ });
553
+ return lines.join("\n");
554
+ }
555
+
556
+ // src/formatters/index.ts
557
+ function format(report, opts = {}) {
558
+ const fmt = opts.format ?? "markdown";
559
+ switch (fmt) {
560
+ case "markdown":
561
+ return markdown(report, opts);
562
+ case "html":
563
+ return html(report, opts);
564
+ case "sarif":
565
+ return sarif(report, opts);
566
+ case "annotations":
567
+ return annotations(report, opts);
568
+ default:
569
+ throw new Error(`Unknown format: ${String(fmt)}`);
570
+ }
571
+ }
572
+
573
+ // src/cli.ts
574
+ var VERSION = "1.0.0";
575
+ var VALID_FORMATS = ["markdown", "html", "sarif", "annotations"];
576
+ var VALID_SEVERITIES2 = ["critical", "high", "moderate", "low", "info"];
577
+ var VALID_FAIL_ON = ["critical", "high", "moderate", "low", "info", "none"];
578
+ function printHelp() {
579
+ const help = `audit-report v${VERSION}
580
+
581
+ Convert npm audit JSON to Markdown, HTML, SARIF, or GitHub annotations.
582
+
583
+ Usage:
584
+ npm audit --json | audit-report [options]
585
+ audit-report --file audit.json [options]
586
+
587
+ Options:
588
+ -f, --format <fmt> Output format: markdown|html|sarif|annotations (default: markdown)
589
+ -o, --output <path> Write to file instead of stdout
590
+ -s, --severity <sev> Minimum severity to include: critical|high|moderate|low|info (default: low)
591
+ --fail-on <sev> Exit 1 if findings at/above this severity: critical|high|moderate|low|info|none (default: high)
592
+ --file <path> Read audit JSON from file (default: stdin)
593
+ --title <text> Report title (default: "npm audit report")
594
+ --template <path> Custom markdown template (reserved; not yet implemented)
595
+ --no-color Disable colored output (no-op)
596
+ -q, --quiet Suppress non-essential stderr output
597
+ -v, --version Print version and exit
598
+ -h, --help Print this help and exit
599
+
600
+ Exit codes:
601
+ 0 success, no findings at/above --fail-on threshold
602
+ 1 findings at/above --fail-on threshold
603
+ 2 invalid input (bad JSON, unrecognized schema)
604
+ 3 --file not found
605
+ `;
606
+ process.stdout.write(help);
607
+ }
608
+ function isFormat(s) {
609
+ return VALID_FORMATS.includes(s);
610
+ }
611
+ function isSeverity(s) {
612
+ return VALID_SEVERITIES2.includes(s);
613
+ }
614
+ function isFailOn(s) {
615
+ return VALID_FAIL_ON.includes(s);
616
+ }
617
+ function fail(msg, code) {
618
+ process.stderr.write(`audit-report: ${msg}
619
+ `);
620
+ process.exit(code);
621
+ }
622
+ function parseArgs(argv) {
623
+ const args = {
624
+ format: "markdown",
625
+ severity: "low",
626
+ failOn: "high",
627
+ title: "npm audit report",
628
+ quiet: false
629
+ };
630
+ const take = (i, flag) => {
631
+ const v = argv[i + 1];
632
+ if (v === void 0 || v.startsWith("-")) fail(`missing value for ${flag}`, 2);
633
+ return v;
634
+ };
635
+ for (let i = 0; i < argv.length; i++) {
636
+ const a = argv[i];
637
+ if (a === void 0) continue;
638
+ switch (a) {
639
+ case "-h":
640
+ case "--help":
641
+ printHelp();
642
+ process.exit(0);
643
+ break;
644
+ case "-v":
645
+ case "--version":
646
+ process.stdout.write(`${VERSION}
647
+ `);
648
+ process.exit(0);
649
+ break;
650
+ case "-f":
651
+ case "--format": {
652
+ const v = take(i, a);
653
+ if (!isFormat(v)) fail(`invalid format: ${v}`, 2);
654
+ args.format = v;
655
+ i++;
656
+ break;
657
+ }
658
+ case "-o":
659
+ case "--output":
660
+ args.output = take(i, a);
661
+ i++;
662
+ break;
663
+ case "-s":
664
+ case "--severity": {
665
+ const v = take(i, a);
666
+ if (!isSeverity(v)) fail(`invalid severity: ${v}`, 2);
667
+ args.severity = v;
668
+ i++;
669
+ break;
670
+ }
671
+ case "--fail-on": {
672
+ const v = take(i, a);
673
+ if (!isFailOn(v)) fail(`invalid fail-on: ${v}`, 2);
674
+ args.failOn = v;
675
+ i++;
676
+ break;
677
+ }
678
+ case "--file":
679
+ args.file = take(i, a);
680
+ i++;
681
+ break;
682
+ case "--title":
683
+ args.title = take(i, a);
684
+ i++;
685
+ break;
686
+ case "--template":
687
+ args.template = take(i, a);
688
+ i++;
689
+ break;
690
+ case "--no-color":
691
+ break;
692
+ case "-q":
693
+ case "--quiet":
694
+ args.quiet = true;
695
+ break;
696
+ default:
697
+ fail(`unknown argument: ${a}`, 2);
698
+ }
699
+ }
700
+ return args;
701
+ }
702
+ async function readStdin() {
703
+ const chunks = [];
704
+ for await (const chunk of process.stdin) {
705
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
706
+ }
707
+ return Buffer.concat(chunks).toString("utf8");
708
+ }
709
+ async function main() {
710
+ const args = parseArgs(process.argv.slice(2));
711
+ let input;
712
+ if (args.file) {
713
+ if (!(0, import_node_fs.existsSync)(args.file)) fail(`file not found: ${args.file}`, 3);
714
+ try {
715
+ input = (0, import_node_fs.readFileSync)(args.file, "utf8");
716
+ } catch (e) {
717
+ fail(`could not read file: ${args.file}`, 3);
718
+ }
719
+ } else {
720
+ if (process.stdin.isTTY) {
721
+ fail(
722
+ "no input. Pipe `npm audit --json` to this command or use --file <path>. Run with --help for usage.",
723
+ 1
724
+ );
725
+ }
726
+ input = await readStdin();
727
+ }
728
+ let report;
729
+ try {
730
+ report = parse(input);
731
+ } catch (e) {
732
+ const msg = e instanceof ParseError ? e.message : "failed to parse input";
733
+ fail(msg, 2);
734
+ }
735
+ if (args.template && !args.quiet) {
736
+ process.stderr.write("audit-report: --template is reserved for future use and is being ignored\n");
737
+ }
738
+ const opts = {
739
+ format: args.format,
740
+ severity: args.severity,
741
+ failOn: args.failOn,
742
+ title: args.title
743
+ };
744
+ const output = format(report, opts);
745
+ if (args.output) {
746
+ (0, import_node_fs.writeFileSync)(args.output, output, "utf8");
747
+ } else {
748
+ process.stdout.write(output);
749
+ if (!output.endsWith("\n")) process.stdout.write("\n");
750
+ }
751
+ if (shouldFail(report, args.failOn)) {
752
+ process.exit(1);
753
+ }
754
+ process.exit(0);
755
+ }
756
+ main().catch((e) => {
757
+ process.stderr.write(`audit-report: ${e instanceof Error ? e.message : String(e)}
758
+ `);
759
+ process.exit(2);
760
+ });