@lateos/npm-scan 1.0.0 → 1.1.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 +864 -861
- package/backend/cra.js +113 -21
- package/backend/db.js +18 -10
- package/backend/detectors/atk-001-lifecycle.js +5 -5
- package/backend/detectors/atk-002-obfusc.js +126 -47
- package/backend/detectors/atk-003-creds.js +8 -4
- package/backend/detectors/atk-004-persist.js +3 -3
- package/backend/detectors/atk-005-exfil.js +8 -4
- package/backend/detectors/atk-006-depconf.js +3 -3
- package/backend/detectors/atk-007-typosquat.js +64 -10
- package/backend/detectors/atk-008-tarball-tamper.js +6 -6
- package/backend/detectors/atk-009-dormant-trigger.js +9 -5
- package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
- package/backend/detectors/atk-011-transitive-prop.js +14 -13
- package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
- package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
- package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
- package/backend/detectors/axios-poisoning/index.js +77 -60
- package/backend/detectors/config/thresholds.js +48 -3
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
- package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
- package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
- package/backend/detectors/hf-impersonation/index.js +94 -31
- package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
- package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
- package/backend/detectors/hf-impersonation/simhash.js +2 -2
- package/backend/detectors/index.js +181 -34
- package/backend/detectors/lib/ast-patterns.js +4 -1
- package/backend/detectors/lib/entropy-analyzer.js +12 -4
- package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
- package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
- package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
- package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
- package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
- package/backend/detectors/megalodon/index.js +35 -25
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
- package/backend/detectors/mini-shai-hulud/index.js +63 -26
- package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
- package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
- package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
- package/backend/detectors/msh-supplement/index.js +78 -63
- package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
- package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
- package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
- package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
- package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
- package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
- package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
- package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
- package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
- package/backend/detectors/node-ipc-compromise/index.js +21 -15
- package/backend/detectors/tier1-binary-embed.js +109 -41
- package/backend/detectors/tier1-cloud-imds.js +57 -37
- package/backend/detectors/tier1-encrypted-c2.js +198 -0
- package/backend/detectors/tier1-infostealer.js +121 -68
- package/backend/detectors/tier1-lifecycle-hook.js +63 -23
- package/backend/detectors/tier1-maintainer-compromise.js +157 -0
- package/backend/detectors/tier1-metadata-spoof.js +92 -42
- package/backend/detectors/tier1-multistage-postinstall.js +46 -19
- package/backend/detectors/tier1-obfuscation-heuristics.js +45 -17
- package/backend/detectors/tier1-self-propagation.js +115 -0
- package/backend/detectors/tier1-slsa-attestation.js +1 -1
- package/backend/detectors/tier1-transitive-deps.js +182 -0
- package/backend/detectors/tier1-typosquat.js +129 -50
- package/backend/detectors/tier1-version-anomaly.js +77 -41
- package/backend/detectors/tier1-version-confusion.js +79 -59
- package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
- package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
- package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
- package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
- package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
- package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
- package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
- package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
- package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
- package/backend/detectors/trapdoor/index.js +19 -14
- package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
- package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
- package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
- package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
- package/backend/detectors.test.js +78 -19
- package/backend/fetch.js +37 -29
- package/backend/index.js +1 -1
- package/backend/license.js +20 -4
- package/backend/lockfile.js +60 -36
- package/backend/pdf.js +107 -28
- package/backend/policy.js +183 -56
- package/backend/provenance.js +28 -3
- package/backend/report.js +136 -70
- package/backend/sbom.js +33 -27
- package/backend/scripts/analyze-false-positives.js +14 -8
- package/backend/scripts/analyze-validation.js +27 -21
- package/backend/scripts/detect-false-positives.js +20 -10
- package/backend/scripts/fetch-top-packages.js +197 -49
- package/backend/scripts/validate-d10-d13.js +103 -0
- package/backend/scripts/validate-detectors.js +26 -17
- package/backend/siem/cef.js +23 -21
- package/backend/siem/ecs.js +3 -3
- package/backend/siem/index.js +1 -1
- package/backend/siem/qradar.js +3 -3
- package/backend/siem/sentinel.js +2 -2
- package/backend/tests-d5-enhanced.test.js +13 -12
- package/backend/tests-d6-version-anomaly.test.js +17 -8
- package/backend/tests-d6.test.js +24 -14
- package/backend/tests-d6c.test.js +27 -14
- package/backend/tests-d7-obfuscation.test.js +9 -12
- package/backend/tests.test.js +182 -83
- package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
- package/backend/vsix-scan/detectors/burst-publish.js +14 -7
- package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
- package/backend/vsix-scan/detectors/known-ioc.js +23 -8
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
- package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
- package/backend/vsix-scan/index.js +97 -41
- package/backend/vsix-scan/marketplace-client.js +29 -13
- package/cli/cli.js +154 -64
- package/package.json +12 -3
package/backend/lockfile.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'fs';
|
|
2
|
-
import { resolve, dirname } from 'path';
|
|
2
|
+
import { resolve as _resolve, dirname as _dirname } from 'path';
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
4
|
|
|
5
5
|
export function parseLockfile(filePath, options = {}) {
|
|
@@ -32,17 +32,19 @@ export function parseLockfile(filePath, options = {}) {
|
|
|
32
32
|
|
|
33
33
|
return parseNpmLockfile(content, filePath);
|
|
34
34
|
} catch (e) {
|
|
35
|
-
throw new Error(`Failed to parse lockfile: ${e.message}
|
|
35
|
+
throw new Error(`Failed to parse lockfile: ${e.message}`, { cause: e });
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function parseNpmLockfile(content,
|
|
39
|
+
function parseNpmLockfile(content, _filePath) {
|
|
40
40
|
const lockfile = JSON.parse(content);
|
|
41
41
|
const packages = [];
|
|
42
42
|
|
|
43
43
|
if (lockfile.packages) {
|
|
44
44
|
for (const [key, pkg] of Object.entries(lockfile.packages)) {
|
|
45
|
-
if (key === '')
|
|
45
|
+
if (key === '') {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
46
48
|
const name = pkg.name || key.replace(/^node_modules\//, '').replace(/^[^/]+\//, '');
|
|
47
49
|
packages.push({
|
|
48
50
|
name,
|
|
@@ -54,7 +56,7 @@ function parseNpmLockfile(content, filePath) {
|
|
|
54
56
|
dev: pkg.dev || false,
|
|
55
57
|
optional: pkg.optional || false,
|
|
56
58
|
scripts: pkg.scripts || {},
|
|
57
|
-
dependencies: pkg.dependencies || {}
|
|
59
|
+
dependencies: pkg.dependencies || {},
|
|
58
60
|
});
|
|
59
61
|
}
|
|
60
62
|
}
|
|
@@ -68,22 +70,23 @@ function parseNpmLockfile(content, filePath) {
|
|
|
68
70
|
version: rootDeps.version || 'unknown',
|
|
69
71
|
dependencies: rootDeps.dependencies || {},
|
|
70
72
|
devDependencies: rootDeps.devDependencies || {},
|
|
71
|
-
peerDependencies: rootDeps.peerDependencies || {}
|
|
72
|
-
}
|
|
73
|
+
peerDependencies: rootDeps.peerDependencies || {},
|
|
74
|
+
},
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
function parseYarnLockfile(content,
|
|
78
|
+
function parseYarnLockfile(content, _filePath) {
|
|
77
79
|
const packages = [];
|
|
78
80
|
const lines = content.split('\n');
|
|
79
81
|
let i = 0;
|
|
80
82
|
const n = lines.length;
|
|
81
83
|
|
|
82
|
-
const MULTI_ENTRY_RE =
|
|
84
|
+
const MULTI_ENTRY_RE =
|
|
85
|
+
/^"?([\w@./-]+)@(\^?[\w.+\-~]+)"?\s*,\s*"?([\w@./-]+)@(\^?[\w.+\-~]+)"?\s*:\s*$/;
|
|
83
86
|
const SINGLE_ENTRY_RE = /^"?([\w@./-]+)@(\^?[\w.+\-~]+)"?\s*:\s*$/;
|
|
84
87
|
|
|
85
88
|
while (i < n) {
|
|
86
|
-
|
|
89
|
+
const line = lines[i].trimEnd();
|
|
87
90
|
|
|
88
91
|
let specs = [];
|
|
89
92
|
|
|
@@ -93,7 +96,7 @@ function parseYarnLockfile(content, filePath) {
|
|
|
93
96
|
if (multiMatch) {
|
|
94
97
|
specs = [
|
|
95
98
|
{ name: multiMatch[1], specVersion: multiMatch[2] },
|
|
96
|
-
{ name: multiMatch[3], specVersion: multiMatch[4] }
|
|
99
|
+
{ name: multiMatch[3], specVersion: multiMatch[4] },
|
|
97
100
|
];
|
|
98
101
|
} else if (singleMatch) {
|
|
99
102
|
specs = [{ name: singleMatch[1], specVersion: singleMatch[2] }];
|
|
@@ -125,26 +128,37 @@ function parseYarnLockfile(content, filePath) {
|
|
|
125
128
|
|
|
126
129
|
if (bodyTrim.startsWith('version ')) {
|
|
127
130
|
const vMatch = bodyTrim.match(/^version ['"]([^'"]+)['"]/);
|
|
128
|
-
if (vMatch)
|
|
131
|
+
if (vMatch) {
|
|
132
|
+
version = vMatch[1];
|
|
133
|
+
}
|
|
129
134
|
} else if (bodyTrim.match(/^\s*resolved\s+(.+)/)) {
|
|
130
135
|
const rMatch = bodyTrim.match(/^\s*resolved\s+(.+)/);
|
|
131
136
|
if (rMatch) {
|
|
132
137
|
resolved = rMatch[1].trim().replace(/^['"]|['"]$/g, '');
|
|
133
138
|
if (resolved.startsWith('https://registry.yarnpkg.com/')) {
|
|
134
|
-
resolved = resolved.replace(
|
|
139
|
+
resolved = resolved.replace(
|
|
140
|
+
'https://registry.yarnpkg.com/',
|
|
141
|
+
'https://registry.npmjs.org/'
|
|
142
|
+
);
|
|
135
143
|
}
|
|
136
144
|
}
|
|
137
145
|
} else if (bodyTrim.startsWith('integrity ')) {
|
|
138
146
|
integrity = bodyTrim.replace('integrity ', '').trim();
|
|
139
147
|
} else if (bodyTrim.startsWith('dependencies')) {
|
|
140
148
|
const m = bodyTrim.match(/^dependencies\s+(.*)/);
|
|
141
|
-
if (m)
|
|
149
|
+
if (m) {
|
|
150
|
+
parseDepList(m[1], dependencies);
|
|
151
|
+
}
|
|
142
152
|
} else if (bodyTrim.startsWith('optionalDependencies')) {
|
|
143
153
|
const m = bodyTrim.match(/^optionalDependencies\s+(.*)/);
|
|
144
|
-
if (m)
|
|
154
|
+
if (m) {
|
|
155
|
+
parseDepList(m[1], optionalDependencies);
|
|
156
|
+
}
|
|
145
157
|
} else if (bodyTrim.startsWith('peerDependencies')) {
|
|
146
158
|
const m = bodyTrim.match(/^peerDependencies\s+(.*)/);
|
|
147
|
-
if (m)
|
|
159
|
+
if (m) {
|
|
160
|
+
parseDepList(m[1], peerDependencies);
|
|
161
|
+
}
|
|
148
162
|
} else if (bodyTrim.match(/^\s*dev\s+(true|false)$/)) {
|
|
149
163
|
dev = bodyTrim.includes('true');
|
|
150
164
|
} else if (bodyTrim.match(/^\s*optional\s+(true|false)$/)) {
|
|
@@ -166,7 +180,7 @@ function parseYarnLockfile(content, filePath) {
|
|
|
166
180
|
optional,
|
|
167
181
|
scripts: {},
|
|
168
182
|
dependencies,
|
|
169
|
-
optionalDependencies
|
|
183
|
+
optionalDependencies,
|
|
170
184
|
});
|
|
171
185
|
}
|
|
172
186
|
} else {
|
|
@@ -192,14 +206,16 @@ function parseYarnLockfile(content, filePath) {
|
|
|
192
206
|
version: 'unknown',
|
|
193
207
|
dependencies: rootDeps,
|
|
194
208
|
devDependencies: rootDevDeps,
|
|
195
|
-
peerDependencies: {}
|
|
196
|
-
}
|
|
209
|
+
peerDependencies: {},
|
|
210
|
+
},
|
|
197
211
|
};
|
|
198
212
|
}
|
|
199
213
|
|
|
200
214
|
function parseDepList(str, dest) {
|
|
201
215
|
const cleaned = str.replace(/^[[\]]/g, '').trim();
|
|
202
|
-
if (!cleaned)
|
|
216
|
+
if (!cleaned) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
203
219
|
const re = /([\w@./-]+)\s+\^?([\w@./-]+)/g;
|
|
204
220
|
let m;
|
|
205
221
|
while ((m = re.exec(cleaned)) !== null) {
|
|
@@ -207,14 +223,16 @@ function parseDepList(str, dest) {
|
|
|
207
223
|
}
|
|
208
224
|
}
|
|
209
225
|
|
|
210
|
-
function parsePnpmLockfile(content,
|
|
226
|
+
function parsePnpmLockfile(content, _filePath) {
|
|
211
227
|
const lockfile = yaml.load(content);
|
|
212
228
|
const packages = [];
|
|
213
229
|
|
|
214
230
|
if (lockfile.packages) {
|
|
215
231
|
for (const [key, pkg] of Object.entries(lockfile.packages)) {
|
|
216
232
|
const nameMatch = key.match(/^\/(.+?)@([^@/]+)$/);
|
|
217
|
-
if (!nameMatch)
|
|
233
|
+
if (!nameMatch) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
218
236
|
const name = nameMatch[1];
|
|
219
237
|
const version = nameMatch[2];
|
|
220
238
|
|
|
@@ -237,7 +255,7 @@ function parsePnpmLockfile(content, filePath) {
|
|
|
237
255
|
optional: pkg.optional || false,
|
|
238
256
|
scripts: pkg.hasBundledMedia ? { bundled: true } : {},
|
|
239
257
|
dependencies: pkg.dependencies || {},
|
|
240
|
-
optionalDependencies: pkg.optionalDependencies || {}
|
|
258
|
+
optionalDependencies: pkg.optionalDependencies || {},
|
|
241
259
|
});
|
|
242
260
|
}
|
|
243
261
|
}
|
|
@@ -257,8 +275,8 @@ function parsePnpmLockfile(content, filePath) {
|
|
|
257
275
|
version: lockfile.lockfileVersion ? 'unknown' : 'unknown',
|
|
258
276
|
dependencies: rootDepsMap,
|
|
259
277
|
devDependencies: rootDevDepsMap,
|
|
260
|
-
peerDependencies: rootPeerDepsMap
|
|
261
|
-
}
|
|
278
|
+
peerDependencies: rootPeerDepsMap,
|
|
279
|
+
},
|
|
262
280
|
};
|
|
263
281
|
}
|
|
264
282
|
|
|
@@ -282,7 +300,7 @@ export function checkMaliciousPatterns(pkg) {
|
|
|
282
300
|
severity: 'high',
|
|
283
301
|
title: 'Typosquat detected',
|
|
284
302
|
description: `Package name "${pkg.name}" is similar to popular packages`,
|
|
285
|
-
evidence: `similar to ${pattern.source}
|
|
303
|
+
evidence: `similar to ${pattern.source}`,
|
|
286
304
|
});
|
|
287
305
|
}
|
|
288
306
|
}
|
|
@@ -307,21 +325,25 @@ export function analyzeDependencyGraph(lockfileData) {
|
|
|
307
325
|
severity: 'high',
|
|
308
326
|
title: 'Transitive propagation (worm)',
|
|
309
327
|
description: `Package "${pkg.name}" depends on peer "${peerName}@${peerVersion}" - potential worm propagation chain`,
|
|
310
|
-
evidence: `peer dep chain: ${pkg.name} -> ${peerName}
|
|
328
|
+
evidence: `peer dep chain: ${pkg.name} -> ${peerName}`,
|
|
311
329
|
});
|
|
312
330
|
}
|
|
313
331
|
}
|
|
314
332
|
}
|
|
315
333
|
|
|
316
|
-
if (
|
|
317
|
-
|
|
334
|
+
if (
|
|
335
|
+
pkg.dependencies &&
|
|
336
|
+
typeof pkg.dependencies === 'object' &&
|
|
337
|
+
Object.keys(pkg.dependencies).length > 5
|
|
338
|
+
) {
|
|
339
|
+
const transitiveCount = Object.keys(pkg.dependencies).filter((k) => k.includes('/')).length;
|
|
318
340
|
if (transitiveCount > 3) {
|
|
319
341
|
findings.push({
|
|
320
342
|
id: 'ATK-011',
|
|
321
343
|
severity: 'medium',
|
|
322
344
|
title: 'Transitive propagation (worm)',
|
|
323
345
|
description: `Package "${pkg.name}" has excessive transitive dependencies (${transitiveCount} scoped)`,
|
|
324
|
-
evidence: `heavy transitive dep chain: ${pkg.name}
|
|
346
|
+
evidence: `heavy transitive dep chain: ${pkg.name}`,
|
|
325
347
|
});
|
|
326
348
|
}
|
|
327
349
|
}
|
|
@@ -332,7 +354,7 @@ export function analyzeDependencyGraph(lockfileData) {
|
|
|
332
354
|
severity: 'low',
|
|
333
355
|
title: 'Transitive propagation (worm)',
|
|
334
356
|
description: `Package "${pkg.name}" has excessive optional dependencies (${Object.keys(pkg.optionalDependencies).length})`,
|
|
335
|
-
evidence: `optional dep chain: ${pkg.name} -> [${Object.keys(pkg.optionalDependencies).slice(0, 3).join(', ')}, ...]
|
|
357
|
+
evidence: `optional dep chain: ${pkg.name} -> [${Object.keys(pkg.optionalDependencies).slice(0, 3).join(', ')}, ...]`,
|
|
336
358
|
});
|
|
337
359
|
}
|
|
338
360
|
}
|
|
@@ -342,8 +364,8 @@ export function analyzeDependencyGraph(lockfileData) {
|
|
|
342
364
|
|
|
343
365
|
export function generateLockfileReport(lockfileData) {
|
|
344
366
|
const total = lockfileData.packages.length;
|
|
345
|
-
const dev = lockfileData.packages.filter(p => p.dev).length;
|
|
346
|
-
const optional = lockfileData.packages.filter(p => p.optional).length;
|
|
367
|
+
const dev = lockfileData.packages.filter((p) => p.dev).length;
|
|
368
|
+
const optional = lockfileData.packages.filter((p) => p.optional).length;
|
|
347
369
|
|
|
348
370
|
const findings = [];
|
|
349
371
|
|
|
@@ -363,12 +385,14 @@ export function generateLockfileReport(lockfileData) {
|
|
|
363
385
|
optionalDependencies: optional,
|
|
364
386
|
lockfileVersion: lockfileData.version,
|
|
365
387
|
findings,
|
|
366
|
-
riskScore: calculateRiskScore(findings)
|
|
388
|
+
riskScore: calculateRiskScore(findings),
|
|
367
389
|
};
|
|
368
390
|
}
|
|
369
391
|
|
|
370
392
|
function calculateRiskScore(findings) {
|
|
371
|
-
if (!findings.length)
|
|
393
|
+
if (!findings.length) {
|
|
394
|
+
return '0.0';
|
|
395
|
+
}
|
|
372
396
|
const weights = { critical: 10, high: 7, medium: 4, low: 2, info: 0.5 };
|
|
373
397
|
const maxSeverity = findings.reduce((max, f) => {
|
|
374
398
|
const w = weights[f.severity] || 0;
|
|
@@ -377,4 +401,4 @@ function calculateRiskScore(findings) {
|
|
|
377
401
|
const countBonus = Math.min(findings.length * 0.3, 3);
|
|
378
402
|
const score = Math.min(maxSeverity + countBonus, 10);
|
|
379
403
|
return score.toFixed(1);
|
|
380
|
-
}
|
|
404
|
+
}
|
package/backend/pdf.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
|
|
2
2
|
|
|
3
3
|
const SEV_ORDER = ['critical', 'high', 'medium', 'low'];
|
|
4
|
-
const SEV_COLORS = {
|
|
4
|
+
const SEV_COLORS = {
|
|
5
|
+
critical: rgb(0.8, 0.2, 0.2),
|
|
6
|
+
high: rgb(0.75, 0.15, 0.15),
|
|
7
|
+
medium: rgb(0.9, 0.5, 0.1),
|
|
8
|
+
low: rgb(0.8, 0.7, 0.1),
|
|
9
|
+
};
|
|
5
10
|
|
|
6
11
|
const NIST_SR_MAP = {
|
|
7
12
|
'ATK-001': { control: 'SR-3.1', title: 'Malicious code detection' },
|
|
@@ -29,24 +34,34 @@ function wrapText(text, font, size, maxWidth) {
|
|
|
29
34
|
for (const word of words) {
|
|
30
35
|
const test = current ? current + ' ' + word : word;
|
|
31
36
|
if (font.widthOfTextAtSize(test, size) > maxWidth) {
|
|
32
|
-
if (current)
|
|
37
|
+
if (current) {
|
|
38
|
+
lines.push(current);
|
|
39
|
+
}
|
|
33
40
|
current = word;
|
|
34
41
|
} else {
|
|
35
42
|
current = test;
|
|
36
43
|
}
|
|
37
44
|
}
|
|
38
|
-
if (current)
|
|
45
|
+
if (current) {
|
|
46
|
+
lines.push(current);
|
|
47
|
+
}
|
|
39
48
|
return lines;
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
function
|
|
51
|
+
function _drawTableRow(page, font, columns, y, colWidths, fontSize, _isHeader) {
|
|
43
52
|
let x = MARGIN;
|
|
44
53
|
const rowH = fontSize + 6;
|
|
45
54
|
for (let i = 0; i < columns.length; i++) {
|
|
46
55
|
const text = columns[i];
|
|
47
56
|
const lines = wrapText(text, font, fontSize, colWidths[i] - 4);
|
|
48
57
|
for (let j = 0; j < lines.length; j++) {
|
|
49
|
-
page.drawText(lines[j], {
|
|
58
|
+
page.drawText(lines[j], {
|
|
59
|
+
x: x + 2,
|
|
60
|
+
y: y - j * fontSize - 2,
|
|
61
|
+
size: fontSize,
|
|
62
|
+
font,
|
|
63
|
+
color: rgb(0, 0, 0),
|
|
64
|
+
});
|
|
50
65
|
}
|
|
51
66
|
x += colWidths[i];
|
|
52
67
|
}
|
|
@@ -55,7 +70,12 @@ function drawTableRow(page, font, columns, y, colWidths, fontSize, isHeader) {
|
|
|
55
70
|
|
|
56
71
|
function drawPageHeader(page, font, text, y) {
|
|
57
72
|
page.drawText(text, { x: MARGIN, y, size: 14, font, color: rgb(0.2, 0.2, 0.2) });
|
|
58
|
-
page.drawLine({
|
|
73
|
+
page.drawLine({
|
|
74
|
+
start: { x: MARGIN, y: y - 4 },
|
|
75
|
+
end: { x: PAGE_W - MARGIN, y: y - 4 },
|
|
76
|
+
thickness: 1,
|
|
77
|
+
color: rgb(0.7, 0.7, 0.7),
|
|
78
|
+
});
|
|
59
79
|
return y - 20;
|
|
60
80
|
}
|
|
61
81
|
|
|
@@ -68,8 +88,10 @@ export async function generatePDF(scans) {
|
|
|
68
88
|
const sevCounts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
69
89
|
let totalFindings = 0;
|
|
70
90
|
for (const s of scans) {
|
|
71
|
-
for (const f of
|
|
72
|
-
if (sevCounts[f.severity] !== undefined)
|
|
91
|
+
for (const f of s.findings || []) {
|
|
92
|
+
if (sevCounts[f.severity] !== undefined) {
|
|
93
|
+
sevCounts[f.severity]++;
|
|
94
|
+
}
|
|
73
95
|
totalFindings++;
|
|
74
96
|
}
|
|
75
97
|
}
|
|
@@ -80,20 +102,41 @@ export async function generatePDF(scans) {
|
|
|
80
102
|
|
|
81
103
|
page.drawText('npm-scan Report', { x: MARGIN, y, size: 24, font: boldFont, color: rgb(0, 0, 0) });
|
|
82
104
|
y -= 30;
|
|
83
|
-
page.drawText(`Generated: ${new Date().toISOString()}`, {
|
|
105
|
+
page.drawText(`Generated: ${new Date().toISOString()}`, {
|
|
106
|
+
x: MARGIN,
|
|
107
|
+
y,
|
|
108
|
+
size: 10,
|
|
109
|
+
font,
|
|
110
|
+
color: rgb(0.4, 0.4, 0.4),
|
|
111
|
+
});
|
|
84
112
|
y -= 14;
|
|
85
|
-
page.drawText(
|
|
113
|
+
page.drawText(
|
|
114
|
+
`Version: ${version} | Packages scanned: ${scans.length} | Total findings: ${totalFindings}`,
|
|
115
|
+
{ x: MARGIN, y, size: 10, font, color: rgb(0.4, 0.4, 0.4) }
|
|
116
|
+
);
|
|
86
117
|
y -= 30;
|
|
87
118
|
|
|
88
119
|
// Severity summary
|
|
89
|
-
page.drawText('Severity Summary', {
|
|
120
|
+
page.drawText('Severity Summary', {
|
|
121
|
+
x: MARGIN,
|
|
122
|
+
y,
|
|
123
|
+
size: 14,
|
|
124
|
+
font: boldFont,
|
|
125
|
+
color: rgb(0, 0, 0),
|
|
126
|
+
});
|
|
90
127
|
y -= 20;
|
|
91
128
|
|
|
92
129
|
for (const sev of SEV_ORDER) {
|
|
93
130
|
const count = sevCounts[sev] || 0;
|
|
94
131
|
const color = SEV_COLORS[sev] || rgb(0, 0, 0);
|
|
95
132
|
page.drawCircle({ x: MARGIN + 6, y: y - 4, size: 4, color });
|
|
96
|
-
page.drawText(`${sev}: ${count}`, {
|
|
133
|
+
page.drawText(`${sev}: ${count}`, {
|
|
134
|
+
x: MARGIN + 16,
|
|
135
|
+
y: y - 8,
|
|
136
|
+
size: 11,
|
|
137
|
+
font,
|
|
138
|
+
color: rgb(0, 0, 0),
|
|
139
|
+
});
|
|
97
140
|
y -= 18;
|
|
98
141
|
}
|
|
99
142
|
|
|
@@ -102,15 +145,33 @@ export async function generatePDF(scans) {
|
|
|
102
145
|
// Per-package summary
|
|
103
146
|
for (const s of scans) {
|
|
104
147
|
const findings = s.findings || [];
|
|
105
|
-
if (y < MARGIN + 60) {
|
|
148
|
+
if (y < MARGIN + 60) {
|
|
149
|
+
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
150
|
+
y = PAGE_H - MARGIN;
|
|
151
|
+
}
|
|
106
152
|
|
|
107
|
-
page.drawText(`${s.package_name}@${s.version || 'unknown'}`, {
|
|
153
|
+
page.drawText(`${s.package_name}@${s.version || 'unknown'}`, {
|
|
154
|
+
x: MARGIN,
|
|
155
|
+
y,
|
|
156
|
+
size: 12,
|
|
157
|
+
font: boldFont,
|
|
158
|
+
color: rgb(0, 0, 0),
|
|
159
|
+
});
|
|
108
160
|
y -= 16;
|
|
109
|
-
page.drawText(` ${findings.length} findings`, {
|
|
161
|
+
page.drawText(` ${findings.length} findings`, {
|
|
162
|
+
x: MARGIN,
|
|
163
|
+
y,
|
|
164
|
+
size: 10,
|
|
165
|
+
font,
|
|
166
|
+
color: rgb(0.4, 0.4, 0.4),
|
|
167
|
+
});
|
|
110
168
|
y -= 14;
|
|
111
169
|
|
|
112
170
|
for (const f of findings) {
|
|
113
|
-
if (y < MARGIN + 20) {
|
|
171
|
+
if (y < MARGIN + 20) {
|
|
172
|
+
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
173
|
+
y = PAGE_H - MARGIN;
|
|
174
|
+
}
|
|
114
175
|
const sevColor = SEV_COLORS[f.severity] || rgb(0, 0, 0);
|
|
115
176
|
page.drawCircle({ x: MARGIN + 3, y: y + 2, size: 3, color: sevColor });
|
|
116
177
|
const line = `${f.atk_id || f.id} ${f.severity} ${(f.description || f.title || '').slice(0, 70)}`;
|
|
@@ -136,8 +197,8 @@ export async function generatePDF(scans) {
|
|
|
136
197
|
}
|
|
137
198
|
y -= 16;
|
|
138
199
|
|
|
139
|
-
|
|
140
|
-
for (const f of
|
|
200
|
+
for (const s of scans) {
|
|
201
|
+
for (const f of s.findings || []) {
|
|
141
202
|
if (y < MARGIN + 20) {
|
|
142
203
|
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
143
204
|
y = PAGE_H - MARGIN;
|
|
@@ -156,10 +217,12 @@ export async function generatePDF(scans) {
|
|
|
156
217
|
let maxLines = 1;
|
|
157
218
|
for (let i = 0; i < rowData.length; i++) {
|
|
158
219
|
const lines = wrapText(rowData[i], font, 9, colWidths[i] - 4);
|
|
159
|
-
if (lines.length > maxLines)
|
|
220
|
+
if (lines.length > maxLines) {
|
|
221
|
+
maxLines = lines.length;
|
|
222
|
+
}
|
|
160
223
|
}
|
|
161
224
|
|
|
162
|
-
if (y -
|
|
225
|
+
if (y - maxLines * 11 < MARGIN) {
|
|
163
226
|
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
164
227
|
y = PAGE_H - MARGIN;
|
|
165
228
|
y = drawPageHeader(page, boldFont, 'All Findings (continued)', y);
|
|
@@ -172,14 +235,19 @@ export async function generatePDF(scans) {
|
|
|
172
235
|
const lines = wrapText(rowData[i], font, 9, colWidths[i] - 4);
|
|
173
236
|
for (let j = 0; j < lines.length; j++) {
|
|
174
237
|
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 -
|
|
238
|
+
page.drawText(lines[j], { x: x + 2, y: rowY - j * 11 - 2, size: 9, font, color });
|
|
176
239
|
}
|
|
177
240
|
x += colWidths[i];
|
|
178
241
|
}
|
|
179
242
|
|
|
180
243
|
const lineY = rowY + 2;
|
|
181
|
-
page.drawLine({
|
|
182
|
-
|
|
244
|
+
page.drawLine({
|
|
245
|
+
start: { x: MARGIN, y: lineY },
|
|
246
|
+
end: { x: PAGE_W - MARGIN, y: lineY },
|
|
247
|
+
thickness: 0.5,
|
|
248
|
+
color: rgb(0.85, 0.85, 0.85),
|
|
249
|
+
});
|
|
250
|
+
y = rowY - maxLines * 11 - 4;
|
|
183
251
|
}
|
|
184
252
|
}
|
|
185
253
|
|
|
@@ -200,9 +268,11 @@ export async function generatePDF(scans) {
|
|
|
200
268
|
|
|
201
269
|
const atkMap = {};
|
|
202
270
|
for (const s of scans) {
|
|
203
|
-
for (const f of
|
|
271
|
+
for (const f of s.findings || []) {
|
|
204
272
|
const key = f.atk_id || f.id;
|
|
205
|
-
if (!atkMap[key])
|
|
273
|
+
if (!atkMap[key]) {
|
|
274
|
+
atkMap[key] = [];
|
|
275
|
+
}
|
|
206
276
|
atkMap[key].push(f);
|
|
207
277
|
}
|
|
208
278
|
}
|
|
@@ -228,16 +298,25 @@ export async function generatePDF(scans) {
|
|
|
228
298
|
x += rowWidths[i];
|
|
229
299
|
}
|
|
230
300
|
|
|
231
|
-
page.drawLine({
|
|
301
|
+
page.drawLine({
|
|
302
|
+
start: { x: MARGIN, y: y + 4 },
|
|
303
|
+
end: { x: PAGE_W - MARGIN, y: y + 4 },
|
|
304
|
+
thickness: 0.5,
|
|
305
|
+
color: rgb(0.85, 0.85, 0.85),
|
|
306
|
+
});
|
|
232
307
|
y -= 18;
|
|
233
308
|
}
|
|
234
309
|
|
|
235
310
|
// Footer
|
|
236
311
|
const pages = doc.getPages();
|
|
237
312
|
for (const p of pages) {
|
|
238
|
-
const { width } = p.getSize();
|
|
313
|
+
const { width: _width } = p.getSize();
|
|
239
314
|
p.drawText(`npm-scan v${version} | Apache-2.0 + Commons Clause`, {
|
|
240
|
-
x: MARGIN,
|
|
315
|
+
x: MARGIN,
|
|
316
|
+
y: 20,
|
|
317
|
+
size: 8,
|
|
318
|
+
font,
|
|
319
|
+
color: rgb(0.6, 0.6, 0.6),
|
|
241
320
|
});
|
|
242
321
|
}
|
|
243
322
|
|