@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/report.js CHANGED
@@ -1,21 +1,29 @@
1
1
  export function generateHTML(scans) {
2
- const rows = scans.map(s => {
2
+ const rows = scans.map((s) => {
3
3
  const findings = s.findings || [];
4
4
  const sevMap = { critical: 5, high: 4, medium: 3, low: 2, info: 1 };
5
5
  const worst = findings.reduce((m, f) => Math.max(m, sevMap[f.severity] || 0), 0);
6
6
  const worstLabel = ['', 'info', 'low', 'medium', 'high', 'critical'][worst] || 'clean';
7
- const color = { critical: '#d73a49', high: '#cb2431', medium: '#f66a0a', low: '#dbab09', clean: '#28a745' }[worstLabel] || '#28a745';
8
- const findingRows = findings.map(f =>
9
- `<tr><td>${f.atk_id || f.id}</td><td style="color:${color}">${f.severity}</td><td>${f.description || f.title || ''}</td><td>${(f.evidence || '').slice(0, 80)}</td></tr>`
10
- ).join('');
7
+ const color =
8
+ { critical: '#d73a49', high: '#cb2431', medium: '#f66a0a', low: '#dbab09', clean: '#28a745' }[
9
+ worstLabel
10
+ ] || '#28a745';
11
+ const findingRows = findings
12
+ .map(
13
+ (f) =>
14
+ `<tr><td>${f.atk_id || f.id}</td><td style="color:${color}">${f.severity}</td><td>${f.description || f.title || ''}</td><td>${(f.evidence || '').slice(0, 80)}</td></tr>`
15
+ )
16
+ .join('');
11
17
  return { name: s.package_name, worstLabel, color, count: findings.length, findingRows };
12
18
  });
13
19
 
14
- const criticalCount = scans.filter(s => s.findings?.some(f => f.severity === 'critical')).length;
15
- const highCount = scans.filter(s => s.findings?.some(f => f.severity === 'high')).length;
16
- const mediumCount = scans.filter(s => s.findings?.some(f => f.severity === 'medium')).length;
17
- const lowCount = scans.filter(s => s.findings?.some(f => f.severity === 'low')).length;
18
- const cleanCount = scans.filter(s => !s.findings?.length).length;
20
+ const criticalCount = scans.filter((s) =>
21
+ s.findings?.some((f) => f.severity === 'critical')
22
+ ).length;
23
+ const highCount = scans.filter((s) => s.findings?.some((f) => f.severity === 'high')).length;
24
+ const mediumCount = scans.filter((s) => s.findings?.some((f) => f.severity === 'medium')).length;
25
+ const lowCount = scans.filter((s) => s.findings?.some((f) => f.severity === 'low')).length;
26
+ const cleanCount = scans.filter((s) => !s.findings?.length).length;
19
27
 
20
28
  const nistMap = generateNistTable(scans);
21
29
 
@@ -59,7 +67,7 @@ th { background: #161b22; font-weight: 600; }
59
67
  <h2>Findings</h2>
60
68
  <table>
61
69
  <thead><tr><th>ATK</th><th>Severity</th><th>Title</th><th>Evidence</th></tr></thead>
62
- <tbody>${rows.map(r => `<tr><td colspan="4" style="background:#161b22;font-weight:600">${r.name} <span class="badge ${r.worstLabel}">${r.count ? r.worstLabel : 'clean'}</span></td></tr>${r.findingRows}`).join('')}</tbody>
70
+ <tbody>${rows.map((r) => `<tr><td colspan="4" style="background:#161b22;font-weight:600">${r.name} <span class="badge ${r.worstLabel}">${r.count ? r.worstLabel : 'clean'}</span></td></tr>${r.findingRows}`).join('')}</tbody>
63
71
  </table>
64
72
 
65
73
  <h2>NIST SP 800-161 Compliance Summary</h2>
@@ -73,9 +81,11 @@ ${nistMap}
73
81
  function getAtkFindings(scans) {
74
82
  const map = {};
75
83
  for (const s of scans) {
76
- for (const f of (s.findings || [])) {
84
+ for (const f of s.findings || []) {
77
85
  const key = f.atk_id || f.id;
78
- if (!map[key]) map[key] = [];
86
+ if (!map[key]) {
87
+ map[key] = [];
88
+ }
79
89
  map[key].push(f);
80
90
  }
81
91
  }
@@ -117,7 +127,9 @@ export function generateText(scans) {
117
127
  const worst = findings.reduce((m, f) => Math.max(m, sevMap[f.severity] || 0), 0);
118
128
  const worstLabel = sevLabel[worst] || 'clean';
119
129
 
120
- lines.push(`${s.package_name}@${s.version || 'unknown'} \u2500\u2500 ${findings.length} findings (worst: ${worstLabel})`);
130
+ lines.push(
131
+ `${s.package_name}@${s.version || 'unknown'} \u2500\u2500 ${findings.length} findings (worst: ${worstLabel})`
132
+ );
121
133
 
122
134
  for (const f of findings) {
123
135
  const desc = (f.description || f.title || '').slice(0, 80);
@@ -159,41 +171,46 @@ function generateNistTable(scans) {
159
171
 
160
172
  export function generateSARIF(scan, format = 'json') {
161
173
  const findings = scan.findings || [];
162
- const runs = [{
163
- tool: {
164
- driver: {
165
- name: 'npm-scan',
166
- version: '0.9.7',
167
- informationUri: 'https://github.com/lateos-ai/npm-scan',
168
- rules: Array.from(new Set(findings.map(f => f.id))).map(id => ({
169
- id,
170
- name: `ATK-${id.replace('ATK-', '')}`,
171
- shortDescription: { text: findings.find(f => f.id === id)?.title || id },
172
- fullDescription: { text: findings.find(f => f.id === id)?.description || '' },
173
- defaultConfiguration: { enabled: true }
174
- }))
175
- }
174
+ const runs = [
175
+ {
176
+ tool: {
177
+ driver: {
178
+ name: 'npm-scan',
179
+ version: '0.9.7',
180
+ informationUri: 'https://github.com/lateos-ai/npm-scan',
181
+ rules: Array.from(new Set(findings.map((f) => f.id))).map((id) => ({
182
+ id,
183
+ name: `ATK-${id.replace('ATK-', '')}`,
184
+ shortDescription: { text: findings.find((f) => f.id === id)?.title || id },
185
+ fullDescription: { text: findings.find((f) => f.id === id)?.description || '' },
186
+ defaultConfiguration: { enabled: true },
187
+ })),
188
+ },
189
+ },
190
+ results: findings.map((f) => {
191
+ const severityMap = { critical: 'error', high: 'error', medium: 'warning', low: 'note' };
192
+ return {
193
+ ruleId: f.id,
194
+ level: severityMap[f.severity] || 'note',
195
+ message: { text: f.description || f.title },
196
+ locations: [
197
+ {
198
+ physicalLocation: {
199
+ artifactLocation: { uri: f.evidence || 'unknown' },
200
+ region: { startLine: 1, startColumn: 1 },
201
+ },
202
+ },
203
+ ],
204
+ };
205
+ }),
176
206
  },
177
- results: findings.map(f => {
178
- const severityMap = { critical: 'error', high: 'error', medium: 'warning', low: 'note' };
179
- return {
180
- ruleId: f.id,
181
- level: severityMap[f.severity] || 'note',
182
- message: { text: f.description || f.title },
183
- locations: [{
184
- physicalLocation: {
185
- artifactLocation: { uri: f.evidence || 'unknown' },
186
- region: { startLine: 1, startColumn: 1 }
187
- }
188
- }]
189
- };
190
- })
191
- }];
207
+ ];
192
208
 
193
209
  const sarif = {
194
210
  version: '2.1.0',
195
- schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
196
- runs
211
+ schema:
212
+ 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
213
+ runs,
197
214
  };
198
215
 
199
216
  return format === 'pretty' ? JSON.stringify(sarif, null, 2) : JSON.stringify(sarif);
@@ -201,17 +218,21 @@ export function generateSARIF(scan, format = 'json') {
201
218
 
202
219
  export function generateCSV(scans) {
203
220
  const headers = 'id,severity,title,description,evidence,package_name,version\n';
204
- const rows = (scans || []).flatMap(s =>
205
- (s.findings || []).map(f => [
206
- f.id,
207
- f.severity || '',
208
- (f.title || '').replace(/,/g, ';'),
209
- (f.description || '').replace(/,/g, ';'),
210
- (f.evidence || '').replace(/,/g, ';'),
211
- s.package_name || '',
212
- s.version || ''
213
- ].join(','))
214
- ).join('\n');
221
+ const rows = (scans || [])
222
+ .flatMap((s) =>
223
+ (s.findings || []).map((f) =>
224
+ [
225
+ f.id,
226
+ f.severity || '',
227
+ (f.title || '').replace(/,/g, ';'),
228
+ (f.description || '').replace(/,/g, ';'),
229
+ (f.evidence || '').replace(/,/g, ';'),
230
+ s.package_name || '',
231
+ s.version || '',
232
+ ].join(',')
233
+ )
234
+ )
235
+ .join('\n');
215
236
  return headers + rows;
216
237
  }
217
238
 
@@ -222,25 +243,70 @@ export function calculateRiskScore(findings, totalPackages = 1) {
222
243
  }
223
244
 
224
245
  const STIG_MAP = {
225
- 'SRG-APP-000141': { title: 'Application Malware Detection', atk: 'ATK-001', desc: 'Lifecycle script detection' },
226
- 'SRG-APP-000142': { title: 'Application Code Obfuscation', atk: 'ATK-002', desc: 'Obfuscated payload detection' },
227
- 'SRG-APP-000143': { title: 'Credential Harvesting', atk: 'ATK-003', desc: 'Credential exfiltration detection' },
228
- 'SRG-APP-000144': { title: 'Persistence Mechanisms', atk: 'ATK-004', desc: 'Malicious persistence detection' },
229
- 'SRG-APP-000145': { title: 'Data Exfiltration', atk: 'ATK-005', desc: 'Network exfiltration detection' },
230
- 'SRG-APP-000146': { title: 'Dependency Confusion', atk: 'ATK-006', desc: 'Internal package detection' },
231
- 'SRG-APP-000147': { title: 'Typosquatting', atk: 'ATK-007', desc: 'Malicious package name detection' },
232
- 'SRG-APP-000148': { title: 'Tarball Tampering', atk: 'ATK-008', desc: 'Modified package detection' },
233
- 'SRG-APP-000149': { title: 'Dormant Triggers', atk: 'ATK-009', desc: 'Conditional execution detection' },
234
- 'SRG-APP-000150': { title: 'Sandbox Evasion', atk: 'ATK-010', desc: 'Environment detection evasion' },
235
- 'SRG-APP-000151': { title: 'Transitive Propagation', atk: 'ATK-011', desc: 'Dependency chain attacks' }
246
+ 'SRG-APP-000141': {
247
+ title: 'Application Malware Detection',
248
+ atk: 'ATK-001',
249
+ desc: 'Lifecycle script detection',
250
+ },
251
+ 'SRG-APP-000142': {
252
+ title: 'Application Code Obfuscation',
253
+ atk: 'ATK-002',
254
+ desc: 'Obfuscated payload detection',
255
+ },
256
+ 'SRG-APP-000143': {
257
+ title: 'Credential Harvesting',
258
+ atk: 'ATK-003',
259
+ desc: 'Credential exfiltration detection',
260
+ },
261
+ 'SRG-APP-000144': {
262
+ title: 'Persistence Mechanisms',
263
+ atk: 'ATK-004',
264
+ desc: 'Malicious persistence detection',
265
+ },
266
+ 'SRG-APP-000145': {
267
+ title: 'Data Exfiltration',
268
+ atk: 'ATK-005',
269
+ desc: 'Network exfiltration detection',
270
+ },
271
+ 'SRG-APP-000146': {
272
+ title: 'Dependency Confusion',
273
+ atk: 'ATK-006',
274
+ desc: 'Internal package detection',
275
+ },
276
+ 'SRG-APP-000147': {
277
+ title: 'Typosquatting',
278
+ atk: 'ATK-007',
279
+ desc: 'Malicious package name detection',
280
+ },
281
+ 'SRG-APP-000148': {
282
+ title: 'Tarball Tampering',
283
+ atk: 'ATK-008',
284
+ desc: 'Modified package detection',
285
+ },
286
+ 'SRG-APP-000149': {
287
+ title: 'Dormant Triggers',
288
+ atk: 'ATK-009',
289
+ desc: 'Conditional execution detection',
290
+ },
291
+ 'SRG-APP-000150': {
292
+ title: 'Sandbox Evasion',
293
+ atk: 'ATK-010',
294
+ desc: 'Environment detection evasion',
295
+ },
296
+ 'SRG-APP-000151': {
297
+ title: 'Transitive Propagation',
298
+ atk: 'ATK-011',
299
+ desc: 'Dependency chain attacks',
300
+ },
236
301
  };
237
302
 
238
303
  export function generateSTIG(scans) {
239
304
  const rows = [];
240
305
  for (const [stigId, info] of Object.entries(STIG_MAP)) {
241
- const findings = scans.flatMap(s => (s.findings || []).filter(f => f.id === info.atk));
306
+ const findings = scans.flatMap((s) => (s.findings || []).filter((f) => f.id === info.atk));
242
307
  const status = findings.length > 0 ? 'NOT APPLICABLE' : 'COMPLETE';
243
- const findingsList = findings.map(f => `${f.severity.toUpperCase()}: ${f.title}`).join('; ') || 'None';
308
+ const findingsList =
309
+ findings.map((f) => `${f.severity.toUpperCase()}: ${f.title}`).join('; ') || 'None';
244
310
  rows.push(`| ${stigId} | ${info.title} | ${status} | ${findingsList} |`);
245
311
  }
246
312
  return `# STIG Compliance Report
@@ -252,4 +318,4 @@ ${rows.join('\n')}
252
318
 
253
319
  ---
254
320
  *This report maps application security controls to DISA STIG requirements.*`;
255
- }
321
+ }
package/backend/sbom.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export function generateSBOM(pkgJson, findings, format = 'json') {
2
- if (format === 'spdx') return generateSPDX(pkgJson, findings);
2
+ if (format === 'spdx') {
3
+ return generateSPDX(pkgJson, findings);
4
+ }
3
5
  return generateCycloneDX(pkgJson, findings);
4
6
  }
5
7
 
@@ -13,20 +15,20 @@ function generateCycloneDX(pkgJson, findings) {
13
15
  type: 'library',
14
16
  name: pkgJson.name || 'unknown',
15
17
  version: pkgJson.version || 'unknown',
16
- purl: `pkg:npm/${pkgJson.name || 'unknown'}@${pkgJson.version || 'unknown'}`
18
+ purl: `pkg:npm/${pkgJson.name || 'unknown'}@${pkgJson.version || 'unknown'}`,
17
19
  },
18
- tools: [{ name: 'npm-scan', version: process.env.npm_package_version || '0.3.2' }]
20
+ tools: [{ name: 'npm-scan', version: process.env.npm_package_version || '0.3.2' }],
19
21
  },
20
- vulnerabilities: findings.map(f => {
22
+ vulnerabilities: findings.map((f) => {
21
23
  const atkId = f.atk_id || f.id;
22
24
  return {
23
- id: atkId,
24
- source: { name: 'npm-scan' },
25
- ratings: [{ severity: f.severity }],
26
- description: f.description || f.title || '',
27
- recommendation: f.mitigation || 'Review evidence'
25
+ id: atkId,
26
+ source: { name: 'npm-scan' },
27
+ ratings: [{ severity: f.severity }],
28
+ description: f.description || f.title || '',
29
+ recommendation: f.mitigation || 'Review evidence',
28
30
  };
29
- })
31
+ }),
30
32
  };
31
33
  return JSON.stringify(bom, null, 2);
32
34
  }
@@ -42,26 +44,30 @@ function generateSPDX(pkgJson, findings) {
42
44
  documentNamespace: `https://npm-scan.io/spdx/${pkgName}-${pkgVer}`,
43
45
  creationInfo: {
44
46
  creators: ['Tool: npm-scan'],
45
- created: new Date().toISOString()
47
+ created: new Date().toISOString(),
46
48
  },
47
- packages: [{
48
- SPDXID: 'SPDXRef-Package',
49
- name: pkgName,
50
- versionInfo: pkgVer,
51
- packageFileName: `pkg:npm/${pkgName}@${pkgVer}`,
52
- primaryPackagePurpose: 'LIBRARY',
53
- externalRefs: [{
54
- referenceCategory: 'PACKAGE-MANAGER',
55
- referenceType: 'purl',
56
- referenceLocator: `pkg:npm/${pkgName}@${pkgVer}`
57
- }]
58
- }],
59
- annotations: findings.map(f => ({
49
+ packages: [
50
+ {
51
+ SPDXID: 'SPDXRef-Package',
52
+ name: pkgName,
53
+ versionInfo: pkgVer,
54
+ packageFileName: `pkg:npm/${pkgName}@${pkgVer}`,
55
+ primaryPackagePurpose: 'LIBRARY',
56
+ externalRefs: [
57
+ {
58
+ referenceCategory: 'PACKAGE-MANAGER',
59
+ referenceType: 'purl',
60
+ referenceLocator: `pkg:npm/${pkgName}@${pkgVer}`,
61
+ },
62
+ ],
63
+ },
64
+ ],
65
+ annotations: findings.map((f) => ({
60
66
  annotationDate: new Date().toISOString(),
61
67
  annotationType: 'OTHER',
62
68
  annotator: 'Tool: npm-scan',
63
- comment: `[${f.atk_id || f.id}] ${f.severity.toUpperCase()}: ${f.description || f.title || ''}`
64
- }))
69
+ comment: `[${f.atk_id || f.id}] ${f.severity.toUpperCase()}: ${f.description || f.title || ''}`,
70
+ })),
65
71
  };
66
72
  return JSON.stringify(spdx, null, 2);
67
- }
73
+ }
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, existsSync, writeFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+
5
+ function analyzeFalsePositives(fpFile) {
6
+ const analysis = {
7
+ total_fps: 0,
8
+ scanned_packages: 0,
9
+ fp_rate: '0%',
10
+ detectors: {},
11
+ high_fp_detectors: [],
12
+ recommendations: [],
13
+ per_package: {},
14
+ };
15
+
16
+ const absPath = resolve(fpFile);
17
+ if (!existsSync(absPath)) {
18
+ console.error(`[ERROR] False positives file not found: ${absPath}`);
19
+ process.exit(1);
20
+ }
21
+
22
+ const text = readFileSync(absPath, 'utf-8');
23
+ const lines = text.split('\n').filter((l) => l.trim());
24
+
25
+ for (const line of lines) {
26
+ const fp = JSON.parse(line);
27
+ analysis.total_fps += 1;
28
+
29
+ const detector = fp.detector;
30
+ if (!analysis.detectors[detector]) {
31
+ analysis.detectors[detector] = {
32
+ fp_count: 0,
33
+ avg_confidence: 0,
34
+ confidences: [],
35
+ severities: [],
36
+ examples: [],
37
+ unique_packages: new Set(),
38
+ };
39
+ }
40
+
41
+ analysis.detectors[detector].fp_count += 1;
42
+ analysis.detectors[detector].confidences.push(fp.confidence);
43
+ analysis.detectors[detector].severities.push(fp.severity);
44
+ analysis.detectors[detector].unique_packages.add(fp.package);
45
+
46
+ if (analysis.detectors[detector].examples.length < 5) {
47
+ analysis.detectors[detector].examples.push({
48
+ package: fp.package,
49
+ version: fp.version,
50
+ confidence: fp.confidence,
51
+ subtype: fp.subtype,
52
+ });
53
+ }
54
+
55
+ if (!analysis.per_package[fp.package]) {
56
+ analysis.per_package[fp.package] = [];
57
+ }
58
+ analysis.per_package[fp.package].push({
59
+ detector: fp.detector,
60
+ confidence: fp.confidence,
61
+ version: fp.version,
62
+ });
63
+ }
64
+
65
+ for (const [detectorName, stats] of Object.entries(analysis.detectors)) {
66
+ stats.avg_confidence =
67
+ stats.confidences.length > 0
68
+ ? (stats.confidences.reduce((a, b) => a + b, 0) / stats.confidences.length).toFixed(1)
69
+ : '0.0';
70
+ stats.unique_package_count = stats.unique_packages.size;
71
+ delete stats.unique_packages;
72
+
73
+ const fpShare = ((stats.fp_count / analysis.total_fps) * 100).toFixed(1);
74
+
75
+ if (stats.fp_count >= 5) {
76
+ analysis.high_fp_detectors.push(detectorName);
77
+ analysis.recommendations.push({
78
+ detector: detectorName,
79
+ fp_count: stats.fp_count,
80
+ unique_packages: stats.unique_package_count,
81
+ share_of_total_fps: fpShare + '%',
82
+ avg_confidence: stats.avg_confidence,
83
+ severity_distribution: stats.severities.reduce((acc, s) => {
84
+ acc[s] = (acc[s] || 0) + 1;
85
+ return acc;
86
+ }, {}),
87
+ suggested_action: `Increase confidence threshold from current to ${Math.min(100, Math.ceil(parseFloat(stats.avg_confidence)) + 5)}`,
88
+ examples: stats.examples,
89
+ });
90
+ }
91
+ }
92
+
93
+ return analysis;
94
+ }
95
+
96
+ const fpFile = process.argv[2] || 'false-positives.jsonl';
97
+
98
+ console.log(`[INFO] Analyzing ${fpFile}...`);
99
+ const analysis = analyzeFalsePositives(fpFile);
100
+
101
+ console.log('\n=== FALSE POSITIVE ANALYSIS ===');
102
+ console.log(`Total FPs: ${analysis.total_fps}`);
103
+ console.log(`Detectors with FPs: ${Object.keys(analysis.detectors).length}`);
104
+
105
+ if (analysis.high_fp_detectors.length > 0) {
106
+ console.log(`\nHigh-FP detectors (>= 5 FPs): ${analysis.high_fp_detectors.join(', ')}`);
107
+ } else {
108
+ console.log('\nNo high-FP detectors found (all < 5 FPs) — thresholds are well-calibrated');
109
+ }
110
+
111
+ console.log('\n=== PER-DETECTOR BREAKDOWN ===');
112
+ console.log('Detector FPs UniquePkgs AvgConf Top Examples');
113
+ console.log('─'.repeat(90));
114
+ for (const [name, stats] of Object.entries(analysis.detectors).sort(
115
+ (a, b) => b[1].fp_count - a[1].fp_count
116
+ )) {
117
+ const dName = name.padEnd(32).slice(0, 32);
118
+ const examples = stats.examples
119
+ .slice(0, 2)
120
+ .map((e) => e.package)
121
+ .join(', ');
122
+ console.log(
123
+ `${dName} ${String(stats.fp_count).padStart(4)} ${String(stats.unique_package_count).padStart(11)} ` +
124
+ `${stats.avg_confidence.padStart(7)} ${examples}`
125
+ );
126
+ }
127
+
128
+ if (analysis.recommendations.length > 0) {
129
+ console.log('\n=== RECOMMENDATIONS ===');
130
+ for (const rec of analysis.recommendations) {
131
+ console.log(`\n${rec.detector}:`);
132
+ console.log(
133
+ ` FPs: ${rec.fp_count} (${rec.share_of_total_fps} of total) across ${rec.unique_packages} unique packages`
134
+ );
135
+ console.log(` Avg confidence: ${rec.avg_confidence}`);
136
+ console.log(` Severity breakdown: ${JSON.stringify(rec.severity_distribution)}`);
137
+ console.log(` Suggestion: ${rec.suggested_action}`);
138
+ console.log(` Examples:`);
139
+ for (const ex of rec.examples.slice(0, 3)) {
140
+ console.log(` ${ex.package}@${ex.version} (${ex.confidence}%) [${ex.subtype}]`);
141
+ }
142
+ }
143
+ } else {
144
+ console.log('\n=== RECOMMENDATIONS ===');
145
+ console.log('No threshold adjustments needed — FP rates are within acceptable bounds.');
146
+ }
147
+
148
+ const outPath = resolve('fp-analysis.json');
149
+ writeFileSync(outPath, JSON.stringify(analysis, null, 2), 'utf-8');
150
+ console.log(`\n[INFO] Full analysis written to ${outPath}`);
151
+
152
+ process.exit(0);
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, existsSync, writeFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+
5
+ async function analyzeValidation(resultsFile) {
6
+ const stats = {
7
+ total_packages: 0,
8
+ total_detections: 0,
9
+ total_expected: 0,
10
+ total_matched: 0,
11
+ campaigns: {},
12
+ detectors: {},
13
+ detection_matrix: {},
14
+ };
15
+
16
+ const absPath = resolve(resultsFile);
17
+ if (!existsSync(absPath)) {
18
+ console.error(`[ERROR] Results file not found: ${absPath}`);
19
+ process.exit(1);
20
+ }
21
+
22
+ const text = readFileSync(absPath, 'utf-8');
23
+ const lines = text.split('\n').filter((l) => l.trim());
24
+
25
+ for (const line of lines) {
26
+ const result = JSON.parse(line);
27
+ if (result.error) {
28
+ continue;
29
+ }
30
+
31
+ stats.total_packages += 1;
32
+ stats.total_detections += result.detection_count;
33
+
34
+ const campaignId = result.campaign_id;
35
+ if (!stats.campaigns[campaignId]) {
36
+ stats.campaigns[campaignId] = {
37
+ name: result.campaign_name,
38
+ total: 0,
39
+ detected: 0,
40
+ detection_rate: 0,
41
+ total_expected: 0,
42
+ total_matched: 0,
43
+ avg_confidence: 0,
44
+ confidences: [],
45
+ };
46
+ }
47
+
48
+ const campaign = stats.campaigns[campaignId];
49
+ campaign.total += 1;
50
+ campaign.total_expected += result.expected_detectors.length;
51
+
52
+ const matched = result.expected_detectors.filter((id) =>
53
+ result.detected_detectors.includes(id)
54
+ );
55
+ campaign.total_matched += matched.length;
56
+ stats.total_expected += result.expected_detectors.length;
57
+ stats.total_matched += matched.length;
58
+
59
+ if (matched.length > 0) {
60
+ campaign.detected += 1;
61
+ }
62
+
63
+ for (const detection of result.detections) {
64
+ const detectorName = detection.id || detection.detector;
65
+ if (!stats.detectors[detectorName]) {
66
+ stats.detectors[detectorName] = {
67
+ total_hits: 0,
68
+ expected_count: 0,
69
+ avg_confidence: 0,
70
+ confidences: [],
71
+ severities: [],
72
+ };
73
+ }
74
+
75
+ stats.detectors[detectorName].total_hits += 1;
76
+ stats.detectors[detectorName].confidences.push(detection.confidenceScore);
77
+ stats.detectors[detectorName].severities.push(detection.severity);
78
+
79
+ if (result.expected_detectors.includes(detectorName)) {
80
+ stats.detectors[detectorName].expected_count += 1;
81
+ }
82
+
83
+ if (!stats.detection_matrix[campaignId]) {
84
+ stats.detection_matrix[campaignId] = {};
85
+ }
86
+ if (!stats.detection_matrix[campaignId][detectorName]) {
87
+ stats.detection_matrix[campaignId][detectorName] = 0;
88
+ }
89
+ stats.detection_matrix[campaignId][detectorName] += 1;
90
+ }
91
+ }
92
+
93
+ for (const campaignId of Object.keys(stats.campaigns)) {
94
+ const campaign = stats.campaigns[campaignId];
95
+ campaign.detection_rate =
96
+ campaign.total > 0 ? ((campaign.detected / campaign.total) * 100).toFixed(1) + '%' : '0%';
97
+ campaign.expected_match_rate =
98
+ campaign.total_expected > 0
99
+ ? ((campaign.total_matched / campaign.total_expected) * 100).toFixed(1) + '%'
100
+ : '0%';
101
+ }
102
+
103
+ for (const detectorName of Object.keys(stats.detectors)) {
104
+ const detector = stats.detectors[detectorName];
105
+ detector.avg_confidence =
106
+ detector.confidences.length > 0
107
+ ? (detector.confidences.reduce((a, b) => a + b, 0) / detector.confidences.length).toFixed(1)
108
+ : '0.0';
109
+ detector.precision =
110
+ detector.total_hits > 0
111
+ ? ((detector.expected_count / detector.total_hits) * 100).toFixed(1) + '%'
112
+ : '0%';
113
+ }
114
+
115
+ return stats;
116
+ }
117
+
118
+ const resultsFile = process.argv[2] || 'validation-results.jsonl';
119
+
120
+ console.log(`[INFO] Analyzing ${resultsFile}...`);
121
+ const stats = await analyzeValidation(resultsFile);
122
+
123
+ console.log('\n=== CAMPAIGN DETECTION RATES ===');
124
+ console.log(
125
+ 'Campaign Packages Detected Rate Expected Matched Match%'
126
+ );
127
+ console.log('─'.repeat(95));
128
+ for (const [_id, campaign] of Object.entries(stats.campaigns)) {
129
+ const name = campaign.name.padEnd(33).slice(0, 33);
130
+ console.log(
131
+ `${name} ${String(campaign.total).padStart(8)} ${String(campaign.detected).padStart(9)} ` +
132
+ `${campaign.detection_rate.padStart(7)} ${String(campaign.total_expected).padStart(9)} ` +
133
+ `${String(campaign.total_matched).padStart(8)} ${campaign.expected_match_rate.padStart(7)}`
134
+ );
135
+ }
136
+ console.log(`\nTotal: ${stats.total_packages} packages, ${stats.total_detections} detections`);
137
+
138
+ console.log('\n=== DETECTOR PERFORMANCE ===');
139
+ console.log('Detector Hits Expected Precision Avg Confidence');
140
+ console.log('─'.repeat(80));
141
+ for (const [name, detector] of Object.entries(stats.detectors).sort(
142
+ (a, b) => b[1].total_hits - a[1].total_hits
143
+ )) {
144
+ const dName = name.padEnd(32).slice(0, 32);
145
+ console.log(
146
+ `${dName} ${String(detector.total_hits).padStart(5)} ${String(detector.expected_count).padStart(9)} ` +
147
+ `${detector.precision.padStart(10)} ${detector.avg_confidence.padStart(14)}`
148
+ );
149
+ }
150
+
151
+ console.log('\n=== DETECTION MATRIX (Hits per Campaign × Detector) ===');
152
+ console.log(JSON.stringify(stats.detection_matrix, null, 2));
153
+
154
+ writeFileSync('detection-rates.json', JSON.stringify(stats, null, 2), 'utf-8');
155
+ console.log('\n[INFO] Full results written to detection-rates.json');
156
+
157
+ process.exit(0);