@lateos/npm-scan 0.18.1 → 0.18.2
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/.dockerignore +20 -20
- package/.husky/pre-commit +1 -1
- package/CHANGELOG.md +233 -199
- package/LICENSING.md +19 -19
- package/README.de.md +708 -708
- package/README.fr.md +707 -707
- package/README.ja.md +704 -704
- package/README.md +826 -826
- package/README.zh.md +708 -708
- package/SECURITY.md +72 -72
- package/backend/cra.js +68 -68
- package/backend/db/schema.sql +32 -32
- package/backend/db.js +88 -88
- package/backend/detectors/atk-001-lifecycle.js +17 -17
- package/backend/detectors/atk-002-obfusc.js +261 -261
- package/backend/detectors/atk-003-creds.js +13 -13
- package/backend/detectors/atk-004-persist.js +13 -13
- package/backend/detectors/atk-005-exfil.js +13 -13
- package/backend/detectors/atk-006-depconf.js +14 -14
- package/backend/detectors/atk-007-typosquat.js +34 -34
- package/backend/detectors/atk-008-tarball-tamper.js +91 -91
- package/backend/detectors/atk-009-dormant-trigger.js +62 -62
- package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
- package/backend/detectors/atk-011-transitive-prop.js +76 -76
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
- package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
- package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
- package/backend/detectors/hf-impersonation/index.js +396 -396
- package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
- package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
- package/backend/detectors/hf-impersonation/simhash.js +46 -46
- package/backend/detectors/index.js +81 -75
- package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
- package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
- package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
- package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
- package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
- package/backend/detectors/megalodon/index.js +80 -80
- package/backend/detectors/megalodon/types.js +9 -9
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
- package/backend/detectors/mini-shai-hulud/index.js +118 -118
- package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
- package/backend/detectors/tier1-cloud-imds.js +124 -0
- package/backend/detectors/tier1-infostealer.js +36 -0
- package/backend/detectors/tier1-multistage-postinstall.js +81 -0
- package/backend/detectors/tier1-version-confusion.js +107 -0
- package/backend/fetch.js +175 -175
- package/backend/index.js +4 -4
- package/backend/license.js +89 -89
- package/backend/lockfile.js +379 -379
- package/backend/pdf.js +245 -245
- package/backend/policy.js +193 -193
- package/backend/report.js +254 -254
- package/backend/sbom.js +66 -66
- package/backend/siem/cef.js +32 -32
- package/backend/siem/ecs.js +40 -40
- package/backend/siem/index.js +18 -18
- package/backend/siem/qradar.js +56 -56
- package/backend/siem/sentinel.js +27 -27
- package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
- package/backend/vsix-scan/detectors/burst-publish.js +52 -52
- package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
- package/backend/vsix-scan/detectors/known-ioc.js +105 -105
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
- package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
- package/backend/vsix-scan/index.js +183 -183
- package/backend/vsix-scan/marketplace-client.js +145 -145
- package/backend/vsix-scan/vsix-iocs.json +31 -31
- package/cli/cli.js +458 -458
- package/deploy/helm/npm-scan/Chart.yaml +21 -21
- package/deploy/helm/npm-scan/templates/_helpers.tpl +8 -8
- package/deploy/helm/npm-scan/templates/api.yaml +93 -93
- package/deploy/helm/npm-scan/templates/ingress.yaml +27 -27
- package/deploy/helm/npm-scan/templates/postgresql.yaml +66 -66
- package/deploy/helm/npm-scan/templates/secrets.yaml +18 -18
- package/deploy/helm/npm-scan/templates/worker.yaml +31 -31
- package/deploy/helm/npm-scan/values.byoc.yaml +74 -74
- package/deploy/helm/npm-scan/values.yaml +102 -102
- package/package.json +57 -57
- package/scripts/download-corpus.js +30 -30
- package/scripts/gen-mal-corpus.js +34 -34
- package/test/fixtures/lockfiles/npm-lock.json +68 -68
- package/test/fixtures/lockfiles/pnpm-lock.yaml +117 -117
- package/test/fixtures/lockfiles/yarn.lock +103 -103
- package/test/fixtures/mock-data.js +69 -69
package/backend/pdf.js
CHANGED
|
@@ -1,245 +1,245 @@
|
|
|
1
|
-
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
|
|
2
|
-
|
|
3
|
-
const SEV_ORDER = ['critical', 'high', 'medium', 'low'];
|
|
4
|
-
const SEV_COLORS = { critical: rgb(0.8, 0.2, 0.2), high: rgb(0.75, 0.15, 0.15), medium: rgb(0.9, 0.5, 0.1), low: rgb(0.8, 0.7, 0.1) };
|
|
5
|
-
|
|
6
|
-
const NIST_SR_MAP = {
|
|
7
|
-
'ATK-001': { control: 'SR-3.1', title: 'Malicious code detection' },
|
|
8
|
-
'ATK-002': { control: 'SR-4.2', title: 'Code obfuscation analysis' },
|
|
9
|
-
'ATK-003': { control: 'SR-5.3', title: 'Credential protection' },
|
|
10
|
-
'ATK-004': { control: 'SR-6.4', title: 'Persistence monitoring' },
|
|
11
|
-
'ATK-005': { control: 'SR-7.5', title: 'Data exfiltration prevention' },
|
|
12
|
-
'ATK-006': { control: 'SR-2.2', title: 'Dependency validation' },
|
|
13
|
-
'ATK-007': { control: 'SR-2.1', title: 'Typosquatting prevention' },
|
|
14
|
-
'ATK-008': { control: 'SR-8.1', title: 'Integrity verification' },
|
|
15
|
-
'ATK-009': { control: 'SR-9.2', title: 'Conditional behavior analysis' },
|
|
16
|
-
'ATK-010': { control: 'SR-10.3', title: 'Anti-evasion detection' },
|
|
17
|
-
'ATK-011': { control: 'SR-11.4', title: 'Supply chain propagation monitoring' },
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const MARGIN = 50;
|
|
21
|
-
const PAGE_W = 612;
|
|
22
|
-
const PAGE_H = 792;
|
|
23
|
-
const CONTENT_W = PAGE_W - MARGIN * 2;
|
|
24
|
-
|
|
25
|
-
function wrapText(text, font, size, maxWidth) {
|
|
26
|
-
const words = text.split(' ');
|
|
27
|
-
const lines = [];
|
|
28
|
-
let current = '';
|
|
29
|
-
for (const word of words) {
|
|
30
|
-
const test = current ? current + ' ' + word : word;
|
|
31
|
-
if (font.widthOfTextAtSize(test, size) > maxWidth) {
|
|
32
|
-
if (current) lines.push(current);
|
|
33
|
-
current = word;
|
|
34
|
-
} else {
|
|
35
|
-
current = test;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
if (current) lines.push(current);
|
|
39
|
-
return lines;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function drawTableRow(page, font, columns, y, colWidths, fontSize, isHeader) {
|
|
43
|
-
let x = MARGIN;
|
|
44
|
-
const rowH = fontSize + 6;
|
|
45
|
-
for (let i = 0; i < columns.length; i++) {
|
|
46
|
-
const text = columns[i];
|
|
47
|
-
const lines = wrapText(text, font, fontSize, colWidths[i] - 4);
|
|
48
|
-
for (let j = 0; j < lines.length; j++) {
|
|
49
|
-
page.drawText(lines[j], { x: x + 2, y: y - (j * fontSize) - 2, size: fontSize, font, color: rgb(0, 0, 0) });
|
|
50
|
-
}
|
|
51
|
-
x += colWidths[i];
|
|
52
|
-
}
|
|
53
|
-
return y - rowH;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function drawPageHeader(page, font, text, y) {
|
|
57
|
-
page.drawText(text, { x: MARGIN, y, size: 14, font, color: rgb(0.2, 0.2, 0.2) });
|
|
58
|
-
page.drawLine({ start: { x: MARGIN, y: y - 4 }, end: { x: PAGE_W - MARGIN, y: y - 4 }, thickness: 1, color: rgb(0.7, 0.7, 0.7) });
|
|
59
|
-
return y - 20;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function generatePDF(scans) {
|
|
63
|
-
const doc = await PDFDocument.create();
|
|
64
|
-
const font = await doc.embedFont(StandardFonts.Helvetica);
|
|
65
|
-
const boldFont = await doc.embedFont(StandardFonts.HelveticaBold);
|
|
66
|
-
const version = process.env.npm_package_version || '0.0.0';
|
|
67
|
-
|
|
68
|
-
const sevCounts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
69
|
-
let totalFindings = 0;
|
|
70
|
-
for (const s of scans) {
|
|
71
|
-
for (const f of (s.findings || [])) {
|
|
72
|
-
if (sevCounts[f.severity] !== undefined) sevCounts[f.severity]++;
|
|
73
|
-
totalFindings++;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ─── Page 1: Title ──────────────────────────────────────────────────
|
|
78
|
-
let page = doc.addPage([PAGE_W, PAGE_H]);
|
|
79
|
-
let y = PAGE_H - MARGIN;
|
|
80
|
-
|
|
81
|
-
page.drawText('npm-scan Report', { x: MARGIN, y, size: 24, font: boldFont, color: rgb(0, 0, 0) });
|
|
82
|
-
y -= 30;
|
|
83
|
-
page.drawText(`Generated: ${new Date().toISOString()}`, { x: MARGIN, y, size: 10, font, color: rgb(0.4, 0.4, 0.4) });
|
|
84
|
-
y -= 14;
|
|
85
|
-
page.drawText(`Version: ${version} | Packages scanned: ${scans.length} | Total findings: ${totalFindings}`, { x: MARGIN, y, size: 10, font, color: rgb(0.4, 0.4, 0.4) });
|
|
86
|
-
y -= 30;
|
|
87
|
-
|
|
88
|
-
// Severity summary
|
|
89
|
-
page.drawText('Severity Summary', { x: MARGIN, y, size: 14, font: boldFont, color: rgb(0, 0, 0) });
|
|
90
|
-
y -= 20;
|
|
91
|
-
|
|
92
|
-
for (const sev of SEV_ORDER) {
|
|
93
|
-
const count = sevCounts[sev] || 0;
|
|
94
|
-
const color = SEV_COLORS[sev] || rgb(0, 0, 0);
|
|
95
|
-
page.drawCircle({ x: MARGIN + 6, y: y - 4, size: 4, color });
|
|
96
|
-
page.drawText(`${sev}: ${count}`, { x: MARGIN + 16, y: y - 8, size: 11, font, color: rgb(0, 0, 0) });
|
|
97
|
-
y -= 18;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
y -= 20;
|
|
101
|
-
|
|
102
|
-
// Per-package summary
|
|
103
|
-
for (const s of scans) {
|
|
104
|
-
const findings = s.findings || [];
|
|
105
|
-
if (y < MARGIN + 60) { page = doc.addPage([PAGE_W, PAGE_H]); y = PAGE_H - MARGIN; }
|
|
106
|
-
|
|
107
|
-
page.drawText(`${s.package_name}@${s.version || 'unknown'}`, { x: MARGIN, y, size: 12, font: boldFont, color: rgb(0, 0, 0) });
|
|
108
|
-
y -= 16;
|
|
109
|
-
page.drawText(` ${findings.length} findings`, { x: MARGIN, y, size: 10, font, color: rgb(0.4, 0.4, 0.4) });
|
|
110
|
-
y -= 14;
|
|
111
|
-
|
|
112
|
-
for (const f of findings) {
|
|
113
|
-
if (y < MARGIN + 20) { page = doc.addPage([PAGE_W, PAGE_H]); y = PAGE_H - MARGIN; }
|
|
114
|
-
const sevColor = SEV_COLORS[f.severity] || rgb(0, 0, 0);
|
|
115
|
-
page.drawCircle({ x: MARGIN + 3, y: y + 2, size: 3, color: sevColor });
|
|
116
|
-
const line = `${f.atk_id || f.id} ${f.severity} ${(f.description || f.title || '').slice(0, 70)}`;
|
|
117
|
-
page.drawText(line, { x: MARGIN + 12, y, size: 9, font, color: rgb(0.1, 0.1, 0.1) });
|
|
118
|
-
y -= 13;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ─── Page: Findings Table ───────────────────────────────────────────
|
|
123
|
-
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
124
|
-
y = PAGE_H - MARGIN;
|
|
125
|
-
y = drawPageHeader(page, boldFont, 'All Findings', y);
|
|
126
|
-
y -= 6;
|
|
127
|
-
|
|
128
|
-
const colWidths = [60, 55, 140, CONTENT_W - 255];
|
|
129
|
-
const headers = ['ATK ID', 'Severity', 'Title', 'Evidence'];
|
|
130
|
-
|
|
131
|
-
// Draw header
|
|
132
|
-
let x = MARGIN;
|
|
133
|
-
for (let i = 0; i < headers.length; i++) {
|
|
134
|
-
page.drawText(headers[i], { x: x + 2, y, size: 10, font: boldFont, color: rgb(0, 0, 0) });
|
|
135
|
-
x += colWidths[i];
|
|
136
|
-
}
|
|
137
|
-
y -= 16;
|
|
138
|
-
|
|
139
|
-
lineLoop: for (const s of scans) {
|
|
140
|
-
for (const f of (s.findings || [])) {
|
|
141
|
-
if (y < MARGIN + 20) {
|
|
142
|
-
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
143
|
-
y = PAGE_H - MARGIN;
|
|
144
|
-
y = drawPageHeader(page, boldFont, 'All Findings (continued)', y);
|
|
145
|
-
y -= 6;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const rowData = [
|
|
149
|
-
f.atk_id || f.id || '',
|
|
150
|
-
f.severity || '',
|
|
151
|
-
(f.title || '').slice(0, 30),
|
|
152
|
-
(f.evidence || '').slice(0, 60),
|
|
153
|
-
];
|
|
154
|
-
|
|
155
|
-
let rowY = y;
|
|
156
|
-
let maxLines = 1;
|
|
157
|
-
for (let i = 0; i < rowData.length; i++) {
|
|
158
|
-
const lines = wrapText(rowData[i], font, 9, colWidths[i] - 4);
|
|
159
|
-
if (lines.length > maxLines) maxLines = lines.length;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (y - (maxLines * 11) < MARGIN) {
|
|
163
|
-
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
164
|
-
y = PAGE_H - MARGIN;
|
|
165
|
-
y = drawPageHeader(page, boldFont, 'All Findings (continued)', y);
|
|
166
|
-
y -= 6;
|
|
167
|
-
rowY = y;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
x = MARGIN;
|
|
171
|
-
for (let i = 0; i < rowData.length; i++) {
|
|
172
|
-
const lines = wrapText(rowData[i], font, 9, colWidths[i] - 4);
|
|
173
|
-
for (let j = 0; j < lines.length; j++) {
|
|
174
|
-
const color = i === 1 && SEV_COLORS[f.severity] ? SEV_COLORS[f.severity] : rgb(0, 0, 0);
|
|
175
|
-
page.drawText(lines[j], { x: x + 2, y: rowY - (j * 11) - 2, size: 9, font, color });
|
|
176
|
-
}
|
|
177
|
-
x += colWidths[i];
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const lineY = rowY + 2;
|
|
181
|
-
page.drawLine({ start: { x: MARGIN, y: lineY }, end: { x: PAGE_W - MARGIN, y: lineY }, thickness: 0.5, color: rgb(0.85, 0.85, 0.85) });
|
|
182
|
-
y = rowY - (maxLines * 11) - 4;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// ─── Page: NIST 800-161 Compliance Matrix ───────────────────────────
|
|
187
|
-
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
188
|
-
y = PAGE_H - MARGIN;
|
|
189
|
-
y = drawPageHeader(page, boldFont, 'NIST SP 800-161 Compliance Summary', y);
|
|
190
|
-
y -= 6;
|
|
191
|
-
|
|
192
|
-
const nistColWidths = [70, CONTENT_W - 180, 110];
|
|
193
|
-
const nistHeaders = ['Control', 'Control Title', 'Status'];
|
|
194
|
-
x = MARGIN;
|
|
195
|
-
for (let i = 0; i < nistHeaders.length; i++) {
|
|
196
|
-
page.drawText(nistHeaders[i], { x: x + 2, y, size: 10, font: boldFont, color: rgb(0, 0, 0) });
|
|
197
|
-
x += nistColWidths[i];
|
|
198
|
-
}
|
|
199
|
-
y -= 16;
|
|
200
|
-
|
|
201
|
-
const atkMap = {};
|
|
202
|
-
for (const s of scans) {
|
|
203
|
-
for (const f of (s.findings || [])) {
|
|
204
|
-
const key = f.atk_id || f.id;
|
|
205
|
-
if (!atkMap[key]) atkMap[key] = [];
|
|
206
|
-
atkMap[key].push(f);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
for (const [atkId, { control, title }] of Object.entries(NIST_SR_MAP)) {
|
|
211
|
-
if (y < MARGIN + 20) {
|
|
212
|
-
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
213
|
-
y = PAGE_H - MARGIN;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const count = (atkMap[atkId] || []).length;
|
|
217
|
-
const status = count > 0 ? `${count} finding(s)` : 'Pass';
|
|
218
|
-
const statusColor = count > 0 ? rgb(0.8, 0.2, 0.2) : rgb(0.2, 0.6, 0.2);
|
|
219
|
-
|
|
220
|
-
const rowData = [control, title, status];
|
|
221
|
-
const rowWidths = nistColWidths;
|
|
222
|
-
|
|
223
|
-
x = MARGIN;
|
|
224
|
-
for (let i = 0; i < rowData.length; i++) {
|
|
225
|
-
const color = i === 2 ? statusColor : rgb(0, 0, 0);
|
|
226
|
-
const fnt = i === 0 ? boldFont : font;
|
|
227
|
-
page.drawText(rowData[i], { x: x + 2, y: y - 2, size: 9, font: fnt, color });
|
|
228
|
-
x += rowWidths[i];
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
page.drawLine({ start: { x: MARGIN, y: y + 4 }, end: { x: PAGE_W - MARGIN, y: y + 4 }, thickness: 0.5, color: rgb(0.85, 0.85, 0.85) });
|
|
232
|
-
y -= 18;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Footer
|
|
236
|
-
const pages = doc.getPages();
|
|
237
|
-
for (const p of pages) {
|
|
238
|
-
const { width } = p.getSize();
|
|
239
|
-
p.drawText(`npm-scan v${version} | Apache-2.0 + Commons Clause`, {
|
|
240
|
-
x: MARGIN, y: 20, size: 8, font, color: rgb(0.6, 0.6, 0.6),
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return doc.save();
|
|
245
|
-
}
|
|
1
|
+
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
|
|
2
|
+
|
|
3
|
+
const SEV_ORDER = ['critical', 'high', 'medium', 'low'];
|
|
4
|
+
const SEV_COLORS = { critical: rgb(0.8, 0.2, 0.2), high: rgb(0.75, 0.15, 0.15), medium: rgb(0.9, 0.5, 0.1), low: rgb(0.8, 0.7, 0.1) };
|
|
5
|
+
|
|
6
|
+
const NIST_SR_MAP = {
|
|
7
|
+
'ATK-001': { control: 'SR-3.1', title: 'Malicious code detection' },
|
|
8
|
+
'ATK-002': { control: 'SR-4.2', title: 'Code obfuscation analysis' },
|
|
9
|
+
'ATK-003': { control: 'SR-5.3', title: 'Credential protection' },
|
|
10
|
+
'ATK-004': { control: 'SR-6.4', title: 'Persistence monitoring' },
|
|
11
|
+
'ATK-005': { control: 'SR-7.5', title: 'Data exfiltration prevention' },
|
|
12
|
+
'ATK-006': { control: 'SR-2.2', title: 'Dependency validation' },
|
|
13
|
+
'ATK-007': { control: 'SR-2.1', title: 'Typosquatting prevention' },
|
|
14
|
+
'ATK-008': { control: 'SR-8.1', title: 'Integrity verification' },
|
|
15
|
+
'ATK-009': { control: 'SR-9.2', title: 'Conditional behavior analysis' },
|
|
16
|
+
'ATK-010': { control: 'SR-10.3', title: 'Anti-evasion detection' },
|
|
17
|
+
'ATK-011': { control: 'SR-11.4', title: 'Supply chain propagation monitoring' },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const MARGIN = 50;
|
|
21
|
+
const PAGE_W = 612;
|
|
22
|
+
const PAGE_H = 792;
|
|
23
|
+
const CONTENT_W = PAGE_W - MARGIN * 2;
|
|
24
|
+
|
|
25
|
+
function wrapText(text, font, size, maxWidth) {
|
|
26
|
+
const words = text.split(' ');
|
|
27
|
+
const lines = [];
|
|
28
|
+
let current = '';
|
|
29
|
+
for (const word of words) {
|
|
30
|
+
const test = current ? current + ' ' + word : word;
|
|
31
|
+
if (font.widthOfTextAtSize(test, size) > maxWidth) {
|
|
32
|
+
if (current) lines.push(current);
|
|
33
|
+
current = word;
|
|
34
|
+
} else {
|
|
35
|
+
current = test;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (current) lines.push(current);
|
|
39
|
+
return lines;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function drawTableRow(page, font, columns, y, colWidths, fontSize, isHeader) {
|
|
43
|
+
let x = MARGIN;
|
|
44
|
+
const rowH = fontSize + 6;
|
|
45
|
+
for (let i = 0; i < columns.length; i++) {
|
|
46
|
+
const text = columns[i];
|
|
47
|
+
const lines = wrapText(text, font, fontSize, colWidths[i] - 4);
|
|
48
|
+
for (let j = 0; j < lines.length; j++) {
|
|
49
|
+
page.drawText(lines[j], { x: x + 2, y: y - (j * fontSize) - 2, size: fontSize, font, color: rgb(0, 0, 0) });
|
|
50
|
+
}
|
|
51
|
+
x += colWidths[i];
|
|
52
|
+
}
|
|
53
|
+
return y - rowH;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function drawPageHeader(page, font, text, y) {
|
|
57
|
+
page.drawText(text, { x: MARGIN, y, size: 14, font, color: rgb(0.2, 0.2, 0.2) });
|
|
58
|
+
page.drawLine({ start: { x: MARGIN, y: y - 4 }, end: { x: PAGE_W - MARGIN, y: y - 4 }, thickness: 1, color: rgb(0.7, 0.7, 0.7) });
|
|
59
|
+
return y - 20;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function generatePDF(scans) {
|
|
63
|
+
const doc = await PDFDocument.create();
|
|
64
|
+
const font = await doc.embedFont(StandardFonts.Helvetica);
|
|
65
|
+
const boldFont = await doc.embedFont(StandardFonts.HelveticaBold);
|
|
66
|
+
const version = process.env.npm_package_version || '0.0.0';
|
|
67
|
+
|
|
68
|
+
const sevCounts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
69
|
+
let totalFindings = 0;
|
|
70
|
+
for (const s of scans) {
|
|
71
|
+
for (const f of (s.findings || [])) {
|
|
72
|
+
if (sevCounts[f.severity] !== undefined) sevCounts[f.severity]++;
|
|
73
|
+
totalFindings++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Page 1: Title ──────────────────────────────────────────────────
|
|
78
|
+
let page = doc.addPage([PAGE_W, PAGE_H]);
|
|
79
|
+
let y = PAGE_H - MARGIN;
|
|
80
|
+
|
|
81
|
+
page.drawText('npm-scan Report', { x: MARGIN, y, size: 24, font: boldFont, color: rgb(0, 0, 0) });
|
|
82
|
+
y -= 30;
|
|
83
|
+
page.drawText(`Generated: ${new Date().toISOString()}`, { x: MARGIN, y, size: 10, font, color: rgb(0.4, 0.4, 0.4) });
|
|
84
|
+
y -= 14;
|
|
85
|
+
page.drawText(`Version: ${version} | Packages scanned: ${scans.length} | Total findings: ${totalFindings}`, { x: MARGIN, y, size: 10, font, color: rgb(0.4, 0.4, 0.4) });
|
|
86
|
+
y -= 30;
|
|
87
|
+
|
|
88
|
+
// Severity summary
|
|
89
|
+
page.drawText('Severity Summary', { x: MARGIN, y, size: 14, font: boldFont, color: rgb(0, 0, 0) });
|
|
90
|
+
y -= 20;
|
|
91
|
+
|
|
92
|
+
for (const sev of SEV_ORDER) {
|
|
93
|
+
const count = sevCounts[sev] || 0;
|
|
94
|
+
const color = SEV_COLORS[sev] || rgb(0, 0, 0);
|
|
95
|
+
page.drawCircle({ x: MARGIN + 6, y: y - 4, size: 4, color });
|
|
96
|
+
page.drawText(`${sev}: ${count}`, { x: MARGIN + 16, y: y - 8, size: 11, font, color: rgb(0, 0, 0) });
|
|
97
|
+
y -= 18;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
y -= 20;
|
|
101
|
+
|
|
102
|
+
// Per-package summary
|
|
103
|
+
for (const s of scans) {
|
|
104
|
+
const findings = s.findings || [];
|
|
105
|
+
if (y < MARGIN + 60) { page = doc.addPage([PAGE_W, PAGE_H]); y = PAGE_H - MARGIN; }
|
|
106
|
+
|
|
107
|
+
page.drawText(`${s.package_name}@${s.version || 'unknown'}`, { x: MARGIN, y, size: 12, font: boldFont, color: rgb(0, 0, 0) });
|
|
108
|
+
y -= 16;
|
|
109
|
+
page.drawText(` ${findings.length} findings`, { x: MARGIN, y, size: 10, font, color: rgb(0.4, 0.4, 0.4) });
|
|
110
|
+
y -= 14;
|
|
111
|
+
|
|
112
|
+
for (const f of findings) {
|
|
113
|
+
if (y < MARGIN + 20) { page = doc.addPage([PAGE_W, PAGE_H]); y = PAGE_H - MARGIN; }
|
|
114
|
+
const sevColor = SEV_COLORS[f.severity] || rgb(0, 0, 0);
|
|
115
|
+
page.drawCircle({ x: MARGIN + 3, y: y + 2, size: 3, color: sevColor });
|
|
116
|
+
const line = `${f.atk_id || f.id} ${f.severity} ${(f.description || f.title || '').slice(0, 70)}`;
|
|
117
|
+
page.drawText(line, { x: MARGIN + 12, y, size: 9, font, color: rgb(0.1, 0.1, 0.1) });
|
|
118
|
+
y -= 13;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Page: Findings Table ───────────────────────────────────────────
|
|
123
|
+
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
124
|
+
y = PAGE_H - MARGIN;
|
|
125
|
+
y = drawPageHeader(page, boldFont, 'All Findings', y);
|
|
126
|
+
y -= 6;
|
|
127
|
+
|
|
128
|
+
const colWidths = [60, 55, 140, CONTENT_W - 255];
|
|
129
|
+
const headers = ['ATK ID', 'Severity', 'Title', 'Evidence'];
|
|
130
|
+
|
|
131
|
+
// Draw header
|
|
132
|
+
let x = MARGIN;
|
|
133
|
+
for (let i = 0; i < headers.length; i++) {
|
|
134
|
+
page.drawText(headers[i], { x: x + 2, y, size: 10, font: boldFont, color: rgb(0, 0, 0) });
|
|
135
|
+
x += colWidths[i];
|
|
136
|
+
}
|
|
137
|
+
y -= 16;
|
|
138
|
+
|
|
139
|
+
lineLoop: for (const s of scans) {
|
|
140
|
+
for (const f of (s.findings || [])) {
|
|
141
|
+
if (y < MARGIN + 20) {
|
|
142
|
+
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
143
|
+
y = PAGE_H - MARGIN;
|
|
144
|
+
y = drawPageHeader(page, boldFont, 'All Findings (continued)', y);
|
|
145
|
+
y -= 6;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const rowData = [
|
|
149
|
+
f.atk_id || f.id || '',
|
|
150
|
+
f.severity || '',
|
|
151
|
+
(f.title || '').slice(0, 30),
|
|
152
|
+
(f.evidence || '').slice(0, 60),
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
let rowY = y;
|
|
156
|
+
let maxLines = 1;
|
|
157
|
+
for (let i = 0; i < rowData.length; i++) {
|
|
158
|
+
const lines = wrapText(rowData[i], font, 9, colWidths[i] - 4);
|
|
159
|
+
if (lines.length > maxLines) maxLines = lines.length;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (y - (maxLines * 11) < MARGIN) {
|
|
163
|
+
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
164
|
+
y = PAGE_H - MARGIN;
|
|
165
|
+
y = drawPageHeader(page, boldFont, 'All Findings (continued)', y);
|
|
166
|
+
y -= 6;
|
|
167
|
+
rowY = y;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
x = MARGIN;
|
|
171
|
+
for (let i = 0; i < rowData.length; i++) {
|
|
172
|
+
const lines = wrapText(rowData[i], font, 9, colWidths[i] - 4);
|
|
173
|
+
for (let j = 0; j < lines.length; j++) {
|
|
174
|
+
const color = i === 1 && SEV_COLORS[f.severity] ? SEV_COLORS[f.severity] : rgb(0, 0, 0);
|
|
175
|
+
page.drawText(lines[j], { x: x + 2, y: rowY - (j * 11) - 2, size: 9, font, color });
|
|
176
|
+
}
|
|
177
|
+
x += colWidths[i];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const lineY = rowY + 2;
|
|
181
|
+
page.drawLine({ start: { x: MARGIN, y: lineY }, end: { x: PAGE_W - MARGIN, y: lineY }, thickness: 0.5, color: rgb(0.85, 0.85, 0.85) });
|
|
182
|
+
y = rowY - (maxLines * 11) - 4;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── Page: NIST 800-161 Compliance Matrix ───────────────────────────
|
|
187
|
+
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
188
|
+
y = PAGE_H - MARGIN;
|
|
189
|
+
y = drawPageHeader(page, boldFont, 'NIST SP 800-161 Compliance Summary', y);
|
|
190
|
+
y -= 6;
|
|
191
|
+
|
|
192
|
+
const nistColWidths = [70, CONTENT_W - 180, 110];
|
|
193
|
+
const nistHeaders = ['Control', 'Control Title', 'Status'];
|
|
194
|
+
x = MARGIN;
|
|
195
|
+
for (let i = 0; i < nistHeaders.length; i++) {
|
|
196
|
+
page.drawText(nistHeaders[i], { x: x + 2, y, size: 10, font: boldFont, color: rgb(0, 0, 0) });
|
|
197
|
+
x += nistColWidths[i];
|
|
198
|
+
}
|
|
199
|
+
y -= 16;
|
|
200
|
+
|
|
201
|
+
const atkMap = {};
|
|
202
|
+
for (const s of scans) {
|
|
203
|
+
for (const f of (s.findings || [])) {
|
|
204
|
+
const key = f.atk_id || f.id;
|
|
205
|
+
if (!atkMap[key]) atkMap[key] = [];
|
|
206
|
+
atkMap[key].push(f);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const [atkId, { control, title }] of Object.entries(NIST_SR_MAP)) {
|
|
211
|
+
if (y < MARGIN + 20) {
|
|
212
|
+
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
213
|
+
y = PAGE_H - MARGIN;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const count = (atkMap[atkId] || []).length;
|
|
217
|
+
const status = count > 0 ? `${count} finding(s)` : 'Pass';
|
|
218
|
+
const statusColor = count > 0 ? rgb(0.8, 0.2, 0.2) : rgb(0.2, 0.6, 0.2);
|
|
219
|
+
|
|
220
|
+
const rowData = [control, title, status];
|
|
221
|
+
const rowWidths = nistColWidths;
|
|
222
|
+
|
|
223
|
+
x = MARGIN;
|
|
224
|
+
for (let i = 0; i < rowData.length; i++) {
|
|
225
|
+
const color = i === 2 ? statusColor : rgb(0, 0, 0);
|
|
226
|
+
const fnt = i === 0 ? boldFont : font;
|
|
227
|
+
page.drawText(rowData[i], { x: x + 2, y: y - 2, size: 9, font: fnt, color });
|
|
228
|
+
x += rowWidths[i];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
page.drawLine({ start: { x: MARGIN, y: y + 4 }, end: { x: PAGE_W - MARGIN, y: y + 4 }, thickness: 0.5, color: rgb(0.85, 0.85, 0.85) });
|
|
232
|
+
y -= 18;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Footer
|
|
236
|
+
const pages = doc.getPages();
|
|
237
|
+
for (const p of pages) {
|
|
238
|
+
const { width } = p.getSize();
|
|
239
|
+
p.drawText(`npm-scan v${version} | Apache-2.0 + Commons Clause`, {
|
|
240
|
+
x: MARGIN, y: 20, size: 8, font, color: rgb(0.6, 0.6, 0.6),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return doc.save();
|
|
245
|
+
}
|