@lateos/npm-scan 0.18.3 → 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 (149) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +864 -826
  3. package/VALIDATION.md +92 -0
  4. package/backend/cra.js +113 -21
  5. package/backend/db/pg-schema.sql +155 -0
  6. package/backend/db.js +18 -10
  7. package/backend/detectors/atk-001-lifecycle.js +5 -5
  8. package/backend/detectors/atk-002-obfusc.js +126 -47
  9. package/backend/detectors/atk-003-creds.js +8 -4
  10. package/backend/detectors/atk-004-persist.js +3 -3
  11. package/backend/detectors/atk-005-exfil.js +8 -4
  12. package/backend/detectors/atk-006-depconf.js +3 -3
  13. package/backend/detectors/atk-007-typosquat.js +64 -10
  14. package/backend/detectors/atk-008-tarball-tamper.js +6 -6
  15. package/backend/detectors/atk-009-dormant-trigger.js +9 -5
  16. package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
  17. package/backend/detectors/atk-011-transitive-prop.js +14 -13
  18. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
  19. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
  20. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
  21. package/backend/detectors/axios-poisoning/index.js +77 -60
  22. package/backend/detectors/config/thresholds.js +111 -0
  23. package/backend/detectors/config/whitelist.json +74 -0
  24. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
  25. package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
  26. package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
  27. package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
  28. package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
  29. package/backend/detectors/hf-impersonation/index.js +94 -31
  30. package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
  31. package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
  32. package/backend/detectors/hf-impersonation/simhash.js +2 -2
  33. package/backend/detectors/index.js +184 -31
  34. package/backend/detectors/lib/ast-patterns.js +24 -0
  35. package/backend/detectors/lib/entropy-analyzer.js +32 -0
  36. package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
  37. package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
  38. package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
  39. package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
  40. package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
  41. package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
  42. package/backend/detectors/megalodon/index.js +35 -25
  43. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
  44. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
  45. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
  46. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
  47. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
  48. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
  49. package/backend/detectors/mini-shai-hulud/index.js +63 -26
  50. package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
  51. package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
  52. package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
  53. package/backend/detectors/msh-supplement/index.js +78 -63
  54. package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
  55. package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
  56. package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
  57. package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
  58. package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
  59. package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
  60. package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
  61. package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
  62. package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
  63. package/backend/detectors/node-ipc-compromise/index.js +21 -15
  64. package/backend/detectors/tier1-binary-embed.js +138 -41
  65. package/backend/detectors/tier1-cloud-imds.js +57 -37
  66. package/backend/detectors/tier1-encrypted-c2.js +198 -0
  67. package/backend/detectors/tier1-infostealer.js +121 -68
  68. package/backend/detectors/tier1-lifecycle-hook.js +63 -23
  69. package/backend/detectors/tier1-maintainer-compromise.js +157 -0
  70. package/backend/detectors/tier1-metadata-spoof.js +92 -42
  71. package/backend/detectors/tier1-multistage-postinstall.js +46 -19
  72. package/backend/detectors/tier1-obfuscation-heuristics.js +184 -0
  73. package/backend/detectors/tier1-self-propagation.js +115 -0
  74. package/backend/detectors/tier1-slsa-attestation.js +12 -0
  75. package/backend/detectors/tier1-transitive-deps.js +182 -0
  76. package/backend/detectors/tier1-typosquat.js +129 -50
  77. package/backend/detectors/tier1-version-anomaly.js +223 -0
  78. package/backend/detectors/tier1-version-confusion.js +79 -59
  79. package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
  80. package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
  81. package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
  82. package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
  83. package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
  84. package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
  85. package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
  86. package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
  87. package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
  88. package/backend/detectors/trapdoor/index.js +19 -14
  89. package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
  90. package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
  91. package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
  92. package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
  93. package/backend/detectors.test.js +147 -0
  94. package/backend/fetch.js +37 -29
  95. package/backend/index.js +1 -1
  96. package/backend/license.js +20 -4
  97. package/backend/lockfile.js +60 -36
  98. package/backend/pdf.js +107 -28
  99. package/backend/policy.js +183 -56
  100. package/backend/provenance.js +28 -3
  101. package/backend/report.js +136 -70
  102. package/backend/sbom.js +33 -27
  103. package/backend/scripts/analyze-false-positives.js +152 -0
  104. package/backend/scripts/analyze-validation.js +157 -0
  105. package/backend/scripts/detect-false-positives.js +103 -0
  106. package/backend/scripts/fetch-top-packages.js +277 -0
  107. package/backend/scripts/validate-d10-d13.js +103 -0
  108. package/backend/scripts/validate-detectors.js +151 -0
  109. package/backend/siem/cef.js +23 -21
  110. package/backend/siem/ecs.js +3 -3
  111. package/backend/siem/index.js +1 -1
  112. package/backend/siem/qradar.js +3 -3
  113. package/backend/siem/sentinel.js +2 -2
  114. package/backend/tests-d5-enhanced.test.js +47 -0
  115. package/backend/tests-d6-version-anomaly.test.js +67 -0
  116. package/backend/tests-d6.test.js +126 -0
  117. package/backend/tests-d6c.test.js +119 -0
  118. package/backend/tests-d7-obfuscation.test.js +88 -0
  119. package/backend/tests.test.js +997 -0
  120. package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
  121. package/backend/vsix-scan/detectors/burst-publish.js +14 -7
  122. package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
  123. package/backend/vsix-scan/detectors/known-ioc.js +23 -8
  124. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
  125. package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
  126. package/backend/vsix-scan/index.js +97 -41
  127. package/backend/vsix-scan/marketplace-client.js +29 -13
  128. package/cli/cli.js +154 -64
  129. package/package.json +36 -10
  130. package/.dockerignore +0 -20
  131. package/.husky/pre-commit +0 -1
  132. package/SECURITY.md +0 -73
  133. package/deploy/helm/npm-scan/Chart.yaml +0 -22
  134. package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
  135. package/deploy/helm/npm-scan/templates/api.yaml +0 -94
  136. package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
  137. package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
  138. package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
  139. package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
  140. package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
  141. package/deploy/helm/npm-scan/values.yaml +0 -103
  142. package/scripts/download-corpus.js +0 -30
  143. package/scripts/gen-mal-corpus.js +0 -35
  144. package/scripts/generate-campaign-fixtures.js +0 -170
  145. package/src/config/top-5000.json +0 -87
  146. package/test/fixtures/lockfiles/npm-lock.json +0 -69
  147. package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
  148. package/test/fixtures/lockfiles/yarn.lock +0 -104
  149. package/test/fixtures/mock-data.js +0 -69
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
 
package/backend/policy.js CHANGED
@@ -5,26 +5,89 @@ const SEVERITY_ORDER = ['none', 'low', 'medium', 'high', 'critical'];
5
5
  const VALID_SEVERITIES = new Set(SEVERITY_ORDER);
6
6
 
7
7
  const KNOWN_REPUTABLE_PACKAGES = new Set([
8
- 'react', 'react-dom', 'vue', 'angular', 'next', 'nuxt',
9
- 'express', 'fastify', 'hono', 'koa', 'connect',
10
- 'webpack', 'vite', 'rollup', 'esbuild', 'typescript', 'babel-core',
11
- 'lodash', 'ramda', 'underscore',
12
- 'axios', 'node-fetch', 'got', 'superagent',
13
- 'sequelize', 'prisma', 'typeorm', 'mongoose',
14
- 'jest', 'mocha', 'vitest', 'ava',
15
- 'prettier', 'eslint', 'stylelint',
16
- 'socket.io', 'ws',
17
- 'rimraf', 'glob', 'minimatch', 'fs-extra',
18
- 'electron', 'puppeteer', 'playwright', 'sharp', 'node-canvas',
19
- 'ffmpeg-static', 'turbo',
20
- 'react-scripts', '@angular/cli',
21
- 'gatsby', 'parcel',
22
- 'tslib', 'core-js', 'regenerator-runtime', 'buffer',
23
- 'node-gyp', 'node-pre-gyp',
24
- 'winston', 'uuid', 'moment', 'dotenv', 'pg', 'semver', 'redux', 'redis',
25
- 'dayjs', 'luxon', 'chalk', 'debug', 'cors', 'helmet', 'multer',
26
- 'body-parser', 'cheerio', 'bluebird', 'bcrypt', 'commander', 'yargs',
27
- 'passport', 'jsonwebtoken', 'nodemailer', 'class-validator',
8
+ 'react',
9
+ 'react-dom',
10
+ 'vue',
11
+ 'angular',
12
+ 'next',
13
+ 'nuxt',
14
+ 'express',
15
+ 'fastify',
16
+ 'hono',
17
+ 'koa',
18
+ 'connect',
19
+ 'webpack',
20
+ 'vite',
21
+ 'rollup',
22
+ 'esbuild',
23
+ 'typescript',
24
+ 'babel-core',
25
+ 'lodash',
26
+ 'ramda',
27
+ 'underscore',
28
+ 'axios',
29
+ 'node-fetch',
30
+ 'got',
31
+ 'superagent',
32
+ 'sequelize',
33
+ 'prisma',
34
+ 'typeorm',
35
+ 'mongoose',
36
+ 'jest',
37
+ 'mocha',
38
+ 'vitest',
39
+ 'ava',
40
+ 'prettier',
41
+ 'eslint',
42
+ 'stylelint',
43
+ 'socket.io',
44
+ 'ws',
45
+ 'rimraf',
46
+ 'glob',
47
+ 'minimatch',
48
+ 'fs-extra',
49
+ 'electron',
50
+ 'puppeteer',
51
+ 'playwright',
52
+ 'sharp',
53
+ 'node-canvas',
54
+ 'ffmpeg-static',
55
+ 'turbo',
56
+ 'react-scripts',
57
+ '@angular/cli',
58
+ 'gatsby',
59
+ 'parcel',
60
+ 'tslib',
61
+ 'core-js',
62
+ 'regenerator-runtime',
63
+ 'buffer',
64
+ 'node-gyp',
65
+ 'node-pre-gyp',
66
+ 'winston',
67
+ 'uuid',
68
+ 'moment',
69
+ 'dotenv',
70
+ 'pg',
71
+ 'semver',
72
+ 'redux',
73
+ 'redis',
74
+ 'dayjs',
75
+ 'luxon',
76
+ 'chalk',
77
+ 'debug',
78
+ 'cors',
79
+ 'helmet',
80
+ 'multer',
81
+ 'body-parser',
82
+ 'cheerio',
83
+ 'bluebird',
84
+ 'bcrypt',
85
+ 'commander',
86
+ 'yargs',
87
+ 'passport',
88
+ 'jsonwebtoken',
89
+ 'nodemailer',
90
+ 'class-validator',
28
91
  ]);
29
92
 
30
93
  function severityIndex(s) {
@@ -32,8 +95,12 @@ function severityIndex(s) {
32
95
  }
33
96
 
34
97
  function matchesFilePath(filePath, pattern) {
35
- if (!pattern) return false;
36
- if (pattern === '*') return true;
98
+ if (!pattern) {
99
+ return false;
100
+ }
101
+ if (pattern === '*') {
102
+ return true;
103
+ }
37
104
  const regexPattern = pattern
38
105
  .replace(/\./g, '\\.')
39
106
  .replace(/\*\*/g, '___DOUBLE_STAR___')
@@ -44,49 +111,90 @@ function matchesFilePath(filePath, pattern) {
44
111
 
45
112
  function matchesContext(finding, rule) {
46
113
  const ctx = finding.context;
47
- if (!ctx) return false;
48
-
49
- if (rule.context?.is_dist_build === true && !ctx.is_dist_build) return false;
50
- if (rule.context?.is_dist_build === false && ctx.is_dist_build) return false;
51
- if (rule.context?.is_test_fixture === true && !ctx.is_test_fixture) return false;
52
- if (rule.context?.is_test_fixture === false && ctx.is_test_fixture) return false;
53
- if (rule.context?.is_lifecycle_hook === true && !ctx.is_lifecycle_hook) return false;
54
- if (rule.context?.is_lifecycle_hook === false && ctx.is_lifecycle_hook) return false;
55
- if (rule.context?.is_known_safe_domain === true && !ctx.is_known_safe_domain) return false;
56
- if (rule.context?.is_known_safe_domain === false && ctx.is_known_safe_domain) return false;
57
-
58
- if (rule.context?.file_path && !matchesFilePath(ctx.file_path, rule.context.file_path)) return false;
114
+ if (!ctx) {
115
+ return false;
116
+ }
117
+
118
+ if (rule.context?.is_dist_build === true && !ctx.is_dist_build) {
119
+ return false;
120
+ }
121
+ if (rule.context?.is_dist_build === false && ctx.is_dist_build) {
122
+ return false;
123
+ }
124
+ if (rule.context?.is_test_fixture === true && !ctx.is_test_fixture) {
125
+ return false;
126
+ }
127
+ if (rule.context?.is_test_fixture === false && ctx.is_test_fixture) {
128
+ return false;
129
+ }
130
+ if (rule.context?.is_lifecycle_hook === true && !ctx.is_lifecycle_hook) {
131
+ return false;
132
+ }
133
+ if (rule.context?.is_lifecycle_hook === false && ctx.is_lifecycle_hook) {
134
+ return false;
135
+ }
136
+ if (rule.context?.is_known_safe_domain === true && !ctx.is_known_safe_domain) {
137
+ return false;
138
+ }
139
+ if (rule.context?.is_known_safe_domain === false && ctx.is_known_safe_domain) {
140
+ return false;
141
+ }
142
+
143
+ if (rule.context?.file_path && !matchesFilePath(ctx.file_path, rule.context.file_path)) {
144
+ return false;
145
+ }
59
146
  if (rule.context?.url_domain) {
60
- if (!ctx.url_domain) return false;
147
+ if (!ctx.url_domain) {
148
+ return false;
149
+ }
61
150
  const domainPattern = rule.context.url_domain.replace(/\*/g, '.*');
62
- if (!new RegExp(`^${domainPattern}$`).test(ctx.url_domain)) return false;
151
+ if (!new RegExp(`^${domainPattern}$`).test(ctx.url_domain)) {
152
+ return false;
153
+ }
63
154
  }
64
155
 
65
156
  return true;
66
157
  }
67
158
 
68
159
  function matchesKnownReputable(packageName) {
69
- if (KNOWN_REPUTABLE_PACKAGES.has(packageName)) return true;
160
+ if (KNOWN_REPUTABLE_PACKAGES.has(packageName)) {
161
+ return true;
162
+ }
70
163
  const [scope, name] = packageName.split('/');
71
- if (scope && name && KNOWN_REPUTABLE_PACKAGES.has(`${scope}/*`)) return true;
164
+ if (scope && name && KNOWN_REPUTABLE_PACKAGES.has(`${scope}/*`)) {
165
+ return true;
166
+ }
72
167
  return false;
73
168
  }
74
169
 
75
170
  function getPackageReputationTier(pkgName) {
76
171
  const name = pkgName?.replace(/^@/, '').replace(/\/.*/, '') || '';
77
- if (matchesKnownReputable(name)) return 'trusted';
172
+ if (matchesKnownReputable(name)) {
173
+ return 'trusted';
174
+ }
78
175
  return 'unknown';
79
176
  }
80
177
 
81
178
  function matchesSuppressRule(finding, pkgName, rule) {
82
- if (rule.atk_id !== (finding.atk_id || finding.id)) return false;
83
- if (rule.package && rule.package !== '*' && rule.package !== pkgName) return false;
179
+ if (rule.atk_id !== (finding.atk_id || finding.id)) {
180
+ return false;
181
+ }
182
+ if (rule.package && rule.package !== '*' && rule.package !== pkgName) {
183
+ return false;
184
+ }
84
185
 
85
- if (rule.context && !matchesContext(finding, rule)) return false;
186
+ if (rule.context && !matchesContext(finding, rule)) {
187
+ return false;
188
+ }
86
189
 
87
190
  if (rule.reputation_tier) {
88
191
  const tier = getPackageReputationTier(pkgName);
89
- if (rule.reputation_tier !== tier && !(rule.reputation_tier === '*' || rule.reputation_tier === 'any')) return false;
192
+ if (
193
+ rule.reputation_tier !== tier &&
194
+ !(rule.reputation_tier === '*' || rule.reputation_tier === 'any')
195
+ ) {
196
+ return false;
197
+ }
90
198
  }
91
199
 
92
200
  return true;
@@ -109,13 +217,17 @@ function loadPolicy(path) {
109
217
  if (policy.severity_overrides) {
110
218
  for (const [atkId, severity] of Object.entries(policy.severity_overrides)) {
111
219
  if (!VALID_SEVERITIES.has(severity)) {
112
- throw new Error(`Invalid severity "${severity}" for ${atkId} — must be one of: low, medium, high, critical`);
220
+ throw new Error(
221
+ `Invalid severity "${severity}" for ${atkId} — must be one of: low, medium, high, critical`
222
+ );
113
223
  }
114
224
  }
115
225
  }
116
226
 
117
227
  if (policy.fail_on && !VALID_SEVERITIES.has(policy.fail_on)) {
118
- throw new Error(`Invalid fail_on "${policy.fail_on}" — must be one of: none, low, medium, high, critical`);
228
+ throw new Error(
229
+ `Invalid fail_on "${policy.fail_on}" — must be one of: none, low, medium, high, critical`
230
+ );
119
231
  }
120
232
 
121
233
  if (policy.suppress) {
@@ -143,7 +255,7 @@ function sanitizePolicy(policy) {
143
255
  allow: { packages: policy.allow?.packages ?? [] },
144
256
  severity_overrides: policy.severity_overrides ?? {},
145
257
  fail_on: policy.fail_on ?? 'none',
146
- suppress: (policy.suppress ?? []).map(r => ({
258
+ suppress: (policy.suppress ?? []).map((r) => ({
147
259
  atk_id: r.atk_id,
148
260
  package: r.package || '*',
149
261
  reason: r.reason || '',
@@ -154,23 +266,29 @@ function sanitizePolicy(policy) {
154
266
  }
155
267
 
156
268
  function isAllowed(packageName, policy) {
157
- if (!policy.allow.packages.length) return false;
269
+ if (!policy.allow.packages.length) {
270
+ return false;
271
+ }
158
272
  const nameOnly = packageName.split('@')[0];
159
- return policy.allow.packages.some(p => p === packageName || p === nameOnly);
273
+ return policy.allow.packages.some((p) => p === packageName || p === nameOnly);
160
274
  }
161
275
 
162
276
  function applyPolicy(findings, packageName, policy) {
163
277
  let filtered = [...findings];
164
278
 
165
279
  if (policy.suppress.length) {
166
- filtered = filtered.filter(f => {
167
- if (f.context?.is_lifecycle_hook) return true;
168
- if (f.context?.is_multi_layer) return true;
169
- return !policy.suppress.some(r => matchesSuppressRule(f, packageName, r));
280
+ filtered = filtered.filter((f) => {
281
+ if (f.context?.is_lifecycle_hook) {
282
+ return true;
283
+ }
284
+ if (f.context?.is_multi_layer) {
285
+ return true;
286
+ }
287
+ return !policy.suppress.some((r) => matchesSuppressRule(f, packageName, r));
170
288
  });
171
289
  }
172
290
 
173
- filtered = filtered.map(f => {
291
+ filtered = filtered.map((f) => {
174
292
  const override = policy.severity_overrides[f.atk_id || f.id];
175
293
  if (override) {
176
294
  return { ...f, severity: override, _severityOverridden: true };
@@ -184,10 +302,19 @@ function applyPolicy(findings, packageName, policy) {
184
302
  }
185
303
 
186
304
  function checkFailOn(findings, policy) {
187
- if (policy.fail_on === 'none') return false;
305
+ if (policy.fail_on === 'none') {
306
+ return false;
307
+ }
188
308
 
189
309
  const threshold = severityIndex(policy.fail_on);
190
- return findings.some(f => severityIndex(f.severity) >= threshold);
310
+ return findings.some((f) => severityIndex(f.severity) >= threshold);
191
311
  }
192
312
 
193
- export { loadPolicy, applyPolicy, isAllowed, getPackageReputationTier, matchesContext, KNOWN_REPUTABLE_PACKAGES };
313
+ export {
314
+ loadPolicy,
315
+ applyPolicy,
316
+ isAllowed,
317
+ getPackageReputationTier,
318
+ matchesContext,
319
+ KNOWN_REPUTABLE_PACKAGES,
320
+ };
@@ -12,7 +12,13 @@ export function signManifest(manifest, key = HMAC_KEY) {
12
12
  return createHmac('sha256', key).update(JSON.stringify(manifest)).digest('hex');
13
13
  }
14
14
 
15
- export function buildDetectionRule({ ruleId, ruleName, severity, cveReferences = [], campaignName }) {
15
+ export function buildDetectionRule({
16
+ ruleId,
17
+ ruleName,
18
+ severity,
19
+ cveReferences = [],
20
+ campaignName,
21
+ }) {
16
22
  return {
17
23
  rule_id: ruleId,
18
24
  rule_name: ruleName,
@@ -41,7 +47,12 @@ export function buildDetectionResult({ triggered, severity, indicators = [] }) {
41
47
 
42
48
  export function buildAuditTrail({ detectionLogic, ruleProvenanceUrl, campaignSourceUrl }) {
43
49
  const contentHash = hashContent(detectionLogic);
44
- const manifest = { contentHash, ruleProvenanceUrl, campaignSourceUrl, generatedAt: new Date().toISOString() };
50
+ const manifest = {
51
+ contentHash,
52
+ ruleProvenanceUrl,
53
+ campaignSourceUrl,
54
+ generatedAt: new Date().toISOString(),
55
+ };
45
56
  return {
46
57
  content_hash: contentHash,
47
58
  rule_provenance_url: ruleProvenanceUrl,
@@ -60,7 +71,21 @@ export function buildDetectionRecord({ rule, scanMetadata, detectionResult, audi
60
71
  };
61
72
  }
62
73
 
63
- export function attachProvenance(evidence, { ruleId, ruleName, severity, campaignName, pkgName, pkgVersion, triggered, indicators, ruleProvenanceUrl, campaignSourceUrl }) {
74
+ export function attachProvenance(
75
+ evidence,
76
+ {
77
+ ruleId,
78
+ ruleName,
79
+ severity,
80
+ campaignName,
81
+ pkgName,
82
+ pkgVersion,
83
+ triggered,
84
+ indicators,
85
+ ruleProvenanceUrl,
86
+ campaignSourceUrl,
87
+ }
88
+ ) {
64
89
  const rule = buildDetectionRule({ ruleId, ruleName, severity, campaignName });
65
90
  const scanMetadata = buildScanMetadata({
66
91
  scannerVersion: '@lateos/npm-scan',