@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.
Files changed (125) hide show
  1. package/README.md +864 -861
  2. package/backend/cra.js +113 -21
  3. package/backend/db.js +18 -10
  4. package/backend/detectors/atk-001-lifecycle.js +5 -5
  5. package/backend/detectors/atk-002-obfusc.js +126 -47
  6. package/backend/detectors/atk-003-creds.js +8 -4
  7. package/backend/detectors/atk-004-persist.js +3 -3
  8. package/backend/detectors/atk-005-exfil.js +8 -4
  9. package/backend/detectors/atk-006-depconf.js +3 -3
  10. package/backend/detectors/atk-007-typosquat.js +64 -10
  11. package/backend/detectors/atk-008-tarball-tamper.js +6 -6
  12. package/backend/detectors/atk-009-dormant-trigger.js +9 -5
  13. package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
  14. package/backend/detectors/atk-011-transitive-prop.js +14 -13
  15. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
  16. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
  17. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
  18. package/backend/detectors/axios-poisoning/index.js +77 -60
  19. package/backend/detectors/config/thresholds.js +48 -3
  20. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
  21. package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
  22. package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
  23. package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
  24. package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
  25. package/backend/detectors/hf-impersonation/index.js +94 -31
  26. package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
  27. package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
  28. package/backend/detectors/hf-impersonation/simhash.js +2 -2
  29. package/backend/detectors/index.js +181 -34
  30. package/backend/detectors/lib/ast-patterns.js +4 -1
  31. package/backend/detectors/lib/entropy-analyzer.js +12 -4
  32. package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
  33. package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
  34. package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
  35. package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
  36. package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
  37. package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
  38. package/backend/detectors/megalodon/index.js +35 -25
  39. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
  40. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
  41. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
  42. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
  43. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
  44. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
  45. package/backend/detectors/mini-shai-hulud/index.js +63 -26
  46. package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
  47. package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
  48. package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
  49. package/backend/detectors/msh-supplement/index.js +78 -63
  50. package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
  51. package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
  52. package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
  53. package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
  54. package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
  55. package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
  56. package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
  57. package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
  58. package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
  59. package/backend/detectors/node-ipc-compromise/index.js +21 -15
  60. package/backend/detectors/tier1-binary-embed.js +109 -41
  61. package/backend/detectors/tier1-cloud-imds.js +57 -37
  62. package/backend/detectors/tier1-encrypted-c2.js +198 -0
  63. package/backend/detectors/tier1-infostealer.js +121 -68
  64. package/backend/detectors/tier1-lifecycle-hook.js +63 -23
  65. package/backend/detectors/tier1-maintainer-compromise.js +157 -0
  66. package/backend/detectors/tier1-metadata-spoof.js +92 -42
  67. package/backend/detectors/tier1-multistage-postinstall.js +46 -19
  68. package/backend/detectors/tier1-obfuscation-heuristics.js +45 -17
  69. package/backend/detectors/tier1-self-propagation.js +115 -0
  70. package/backend/detectors/tier1-slsa-attestation.js +1 -1
  71. package/backend/detectors/tier1-transitive-deps.js +182 -0
  72. package/backend/detectors/tier1-typosquat.js +129 -50
  73. package/backend/detectors/tier1-version-anomaly.js +77 -41
  74. package/backend/detectors/tier1-version-confusion.js +79 -59
  75. package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
  76. package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
  77. package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
  78. package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
  79. package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
  80. package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
  81. package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
  82. package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
  83. package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
  84. package/backend/detectors/trapdoor/index.js +19 -14
  85. package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
  86. package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
  87. package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
  88. package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
  89. package/backend/detectors.test.js +78 -19
  90. package/backend/fetch.js +37 -29
  91. package/backend/index.js +1 -1
  92. package/backend/license.js +20 -4
  93. package/backend/lockfile.js +60 -36
  94. package/backend/pdf.js +107 -28
  95. package/backend/policy.js +183 -56
  96. package/backend/provenance.js +28 -3
  97. package/backend/report.js +136 -70
  98. package/backend/sbom.js +33 -27
  99. package/backend/scripts/analyze-false-positives.js +14 -8
  100. package/backend/scripts/analyze-validation.js +27 -21
  101. package/backend/scripts/detect-false-positives.js +20 -10
  102. package/backend/scripts/fetch-top-packages.js +197 -49
  103. package/backend/scripts/validate-d10-d13.js +103 -0
  104. package/backend/scripts/validate-detectors.js +26 -17
  105. package/backend/siem/cef.js +23 -21
  106. package/backend/siem/ecs.js +3 -3
  107. package/backend/siem/index.js +1 -1
  108. package/backend/siem/qradar.js +3 -3
  109. package/backend/siem/sentinel.js +2 -2
  110. package/backend/tests-d5-enhanced.test.js +13 -12
  111. package/backend/tests-d6-version-anomaly.test.js +17 -8
  112. package/backend/tests-d6.test.js +24 -14
  113. package/backend/tests-d6c.test.js +27 -14
  114. package/backend/tests-d7-obfuscation.test.js +9 -12
  115. package/backend/tests.test.js +182 -83
  116. package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
  117. package/backend/vsix-scan/detectors/burst-publish.js +14 -7
  118. package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
  119. package/backend/vsix-scan/detectors/known-ioc.js +23 -8
  120. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
  121. package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
  122. package/backend/vsix-scan/index.js +97 -41
  123. package/backend/vsix-scan/marketplace-client.js +29 -13
  124. package/cli/cli.js +154 -64
  125. package/package.json +12 -3
@@ -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, filePath) {
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 === '') continue;
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, filePath) {
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 = /^"?([\w@./-]+)@(\^?[\w.+\-~]+)"?\s*,\s*"?([\w@./-]+)@(\^?[\w.+\-~]+)"?\s*:\s*$/;
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
- let line = lines[i].trimEnd();
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) version = vMatch[1];
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('https://registry.yarnpkg.com/', 'https://registry.npmjs.org/');
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) parseDepList(m[1], dependencies);
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) parseDepList(m[1], optionalDependencies);
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) parseDepList(m[1], peerDependencies);
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) return;
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, filePath) {
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) continue;
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 (pkg.dependencies && typeof pkg.dependencies === 'object' && Object.keys(pkg.dependencies).length > 5) {
317
- const transitiveCount = Object.keys(pkg.dependencies).filter(k => k.includes('/')).length;
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) return '0.0';
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 = { 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) };
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) lines.push(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) lines.push(current);
45
+ if (current) {
46
+ lines.push(current);
47
+ }
39
48
  return lines;
40
49
  }
41
50
 
42
- function drawTableRow(page, font, columns, y, colWidths, fontSize, isHeader) {
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], { x: x + 2, y: y - (j * fontSize) - 2, size: fontSize, font, color: rgb(0, 0, 0) });
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({ start: { x: MARGIN, y: y - 4 }, end: { x: PAGE_W - MARGIN, y: y - 4 }, thickness: 1, color: rgb(0.7, 0.7, 0.7) });
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 (s.findings || [])) {
72
- if (sevCounts[f.severity] !== undefined) sevCounts[f.severity]++;
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()}`, { x: MARGIN, y, size: 10, font, color: rgb(0.4, 0.4, 0.4) });
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(`Version: ${version} | Packages scanned: ${scans.length} | Total findings: ${totalFindings}`, { x: MARGIN, y, size: 10, font, color: rgb(0.4, 0.4, 0.4) });
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', { x: MARGIN, y, size: 14, font: boldFont, color: rgb(0, 0, 0) });
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}`, { x: MARGIN + 16, y: y - 8, size: 11, font, color: rgb(0, 0, 0) });
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) { page = doc.addPage([PAGE_W, PAGE_H]); y = PAGE_H - MARGIN; }
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'}`, { x: MARGIN, y, size: 12, font: boldFont, color: rgb(0, 0, 0) });
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`, { x: MARGIN, y, size: 10, font, color: rgb(0.4, 0.4, 0.4) });
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) { page = doc.addPage([PAGE_W, PAGE_H]); y = PAGE_H - MARGIN; }
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
- lineLoop: for (const s of scans) {
140
- for (const f of (s.findings || [])) {
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) maxLines = lines.length;
220
+ if (lines.length > maxLines) {
221
+ maxLines = lines.length;
222
+ }
160
223
  }
161
224
 
162
- if (y - (maxLines * 11) < MARGIN) {
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 - (j * 11) - 2, size: 9, font, color });
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({ 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;
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 (s.findings || [])) {
271
+ for (const f of s.findings || []) {
204
272
  const key = f.atk_id || f.id;
205
- if (!atkMap[key]) 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({ 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) });
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, y: 20, size: 8, font, color: rgb(0.6, 0.6, 0.6),
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