@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.
- package/CHANGELOG.md +32 -0
- package/README.md +864 -826
- package/VALIDATION.md +92 -0
- package/backend/cra.js +113 -21
- package/backend/db/pg-schema.sql +155 -0
- package/backend/db.js +18 -10
- package/backend/detectors/atk-001-lifecycle.js +5 -5
- package/backend/detectors/atk-002-obfusc.js +126 -47
- package/backend/detectors/atk-003-creds.js +8 -4
- package/backend/detectors/atk-004-persist.js +3 -3
- package/backend/detectors/atk-005-exfil.js +8 -4
- package/backend/detectors/atk-006-depconf.js +3 -3
- package/backend/detectors/atk-007-typosquat.js +64 -10
- package/backend/detectors/atk-008-tarball-tamper.js +6 -6
- package/backend/detectors/atk-009-dormant-trigger.js +9 -5
- package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
- package/backend/detectors/atk-011-transitive-prop.js +14 -13
- package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
- package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
- package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
- package/backend/detectors/axios-poisoning/index.js +77 -60
- package/backend/detectors/config/thresholds.js +111 -0
- package/backend/detectors/config/whitelist.json +74 -0
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
- package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
- package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
- package/backend/detectors/hf-impersonation/index.js +94 -31
- package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
- package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
- package/backend/detectors/hf-impersonation/simhash.js +2 -2
- package/backend/detectors/index.js +184 -31
- package/backend/detectors/lib/ast-patterns.js +24 -0
- package/backend/detectors/lib/entropy-analyzer.js +32 -0
- package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
- package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
- package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
- package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
- package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
- package/backend/detectors/megalodon/index.js +35 -25
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
- package/backend/detectors/mini-shai-hulud/index.js +63 -26
- package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
- package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
- package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
- package/backend/detectors/msh-supplement/index.js +78 -63
- package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
- package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
- package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
- package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
- package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
- package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
- package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
- package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
- package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
- package/backend/detectors/node-ipc-compromise/index.js +21 -15
- package/backend/detectors/tier1-binary-embed.js +138 -41
- package/backend/detectors/tier1-cloud-imds.js +57 -37
- package/backend/detectors/tier1-encrypted-c2.js +198 -0
- package/backend/detectors/tier1-infostealer.js +121 -68
- package/backend/detectors/tier1-lifecycle-hook.js +63 -23
- package/backend/detectors/tier1-maintainer-compromise.js +157 -0
- package/backend/detectors/tier1-metadata-spoof.js +92 -42
- package/backend/detectors/tier1-multistage-postinstall.js +46 -19
- package/backend/detectors/tier1-obfuscation-heuristics.js +184 -0
- package/backend/detectors/tier1-self-propagation.js +115 -0
- package/backend/detectors/tier1-slsa-attestation.js +12 -0
- package/backend/detectors/tier1-transitive-deps.js +182 -0
- package/backend/detectors/tier1-typosquat.js +129 -50
- package/backend/detectors/tier1-version-anomaly.js +223 -0
- package/backend/detectors/tier1-version-confusion.js +79 -59
- package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
- package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
- package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
- package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
- package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
- package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
- package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
- package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
- package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
- package/backend/detectors/trapdoor/index.js +19 -14
- package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
- package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
- package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
- package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
- package/backend/detectors.test.js +147 -0
- package/backend/fetch.js +37 -29
- package/backend/index.js +1 -1
- package/backend/license.js +20 -4
- package/backend/lockfile.js +60 -36
- package/backend/pdf.js +107 -28
- package/backend/policy.js +183 -56
- package/backend/provenance.js +28 -3
- package/backend/report.js +136 -70
- package/backend/sbom.js +33 -27
- package/backend/scripts/analyze-false-positives.js +152 -0
- package/backend/scripts/analyze-validation.js +157 -0
- package/backend/scripts/detect-false-positives.js +103 -0
- package/backend/scripts/fetch-top-packages.js +277 -0
- package/backend/scripts/validate-d10-d13.js +103 -0
- package/backend/scripts/validate-detectors.js +151 -0
- package/backend/siem/cef.js +23 -21
- package/backend/siem/ecs.js +3 -3
- package/backend/siem/index.js +1 -1
- package/backend/siem/qradar.js +3 -3
- package/backend/siem/sentinel.js +2 -2
- package/backend/tests-d5-enhanced.test.js +47 -0
- package/backend/tests-d6-version-anomaly.test.js +67 -0
- package/backend/tests-d6.test.js +126 -0
- package/backend/tests-d6c.test.js +119 -0
- package/backend/tests-d7-obfuscation.test.js +88 -0
- package/backend/tests.test.js +997 -0
- package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
- package/backend/vsix-scan/detectors/burst-publish.js +14 -7
- package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
- package/backend/vsix-scan/detectors/known-ioc.js +23 -8
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
- package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
- package/backend/vsix-scan/index.js +97 -41
- package/backend/vsix-scan/marketplace-client.js +29 -13
- package/cli/cli.js +154 -64
- package/package.json +36 -10
- package/.dockerignore +0 -20
- package/.husky/pre-commit +0 -1
- package/SECURITY.md +0 -73
- package/deploy/helm/npm-scan/Chart.yaml +0 -22
- package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
- package/deploy/helm/npm-scan/templates/api.yaml +0 -94
- package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
- package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
- package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
- package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
- package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
- package/deploy/helm/npm-scan/values.yaml +0 -103
- package/scripts/download-corpus.js +0 -30
- package/scripts/gen-mal-corpus.js +0 -35
- package/scripts/generate-campaign-fixtures.js +0 -170
- package/src/config/top-5000.json +0 -87
- package/test/fixtures/lockfiles/npm-lock.json +0 -69
- package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
- package/test/fixtures/lockfiles/yarn.lock +0 -104
- 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 =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 =>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const
|
|
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
|
|
84
|
+
for (const f of s.findings || []) {
|
|
77
85
|
const key = f.atk_id || f.id;
|
|
78
|
-
if (!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(
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
id
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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:
|
|
196
|
-
|
|
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 || [])
|
|
205
|
-
(s
|
|
206
|
-
f
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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': {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
'SRG-APP-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
'SRG-APP-
|
|
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 =
|
|
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')
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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);
|