@lateos/npm-scan 0.18.2 → 1.0.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 +265 -233
- package/LICENSING.md +19 -19
- package/README.de.md +708 -708
- package/README.fr.md +707 -707
- package/README.ja.md +704 -704
- package/README.md +861 -826
- package/README.zh.md +708 -708
- package/VALIDATION.md +92 -0
- package/backend/cra.js +68 -68
- package/backend/db/pg-schema.sql +155 -0
- package/backend/db/schema.sql +32 -32
- package/backend/db.js +88 -88
- package/backend/detectors/atk-001-lifecycle.js +17 -17
- package/backend/detectors/atk-002-obfusc.js +261 -261
- package/backend/detectors/atk-003-creds.js +13 -13
- package/backend/detectors/atk-004-persist.js +13 -13
- package/backend/detectors/atk-005-exfil.js +13 -13
- package/backend/detectors/atk-006-depconf.js +14 -14
- package/backend/detectors/atk-007-typosquat.js +34 -34
- package/backend/detectors/atk-008-tarball-tamper.js +91 -91
- package/backend/detectors/atk-009-dormant-trigger.js +62 -62
- package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
- package/backend/detectors/atk-011-transitive-prop.js +76 -76
- package/backend/detectors/config/thresholds.js +66 -0
- package/backend/detectors/config/whitelist.json +74 -0
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
- package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
- package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
- package/backend/detectors/hf-impersonation/index.js +396 -396
- package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
- package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
- package/backend/detectors/hf-impersonation/simhash.js +46 -46
- package/backend/detectors/index.js +87 -81
- package/backend/detectors/lib/ast-patterns.js +21 -0
- package/backend/detectors/lib/entropy-analyzer.js +24 -0
- package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
- package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
- package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
- package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
- package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
- package/backend/detectors/megalodon/index.js +80 -80
- package/backend/detectors/megalodon/types.js +9 -9
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
- package/backend/detectors/mini-shai-hulud/index.js +118 -118
- package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
- package/backend/detectors/tier1-binary-embed.js +34 -5
- package/backend/detectors/tier1-obfuscation-heuristics.js +156 -0
- package/backend/detectors/tier1-slsa-attestation.js +12 -0
- package/backend/detectors/tier1-version-anomaly.js +187 -0
- package/backend/detectors.test.js +88 -0
- package/backend/fetch.js +175 -175
- package/backend/index.js +4 -4
- package/backend/license.js +89 -89
- package/backend/lockfile.js +379 -379
- package/backend/pdf.js +245 -245
- package/backend/policy.js +193 -193
- package/backend/report.js +254 -254
- package/backend/sbom.js +66 -66
- package/backend/scripts/analyze-false-positives.js +146 -0
- package/backend/scripts/analyze-validation.js +151 -0
- package/backend/scripts/detect-false-positives.js +93 -0
- package/backend/scripts/fetch-top-packages.js +129 -0
- package/backend/scripts/validate-detectors.js +142 -0
- package/backend/siem/cef.js +32 -32
- package/backend/siem/ecs.js +40 -40
- package/backend/siem/index.js +18 -18
- package/backend/siem/qradar.js +56 -56
- package/backend/siem/sentinel.js +27 -27
- package/backend/tests-d5-enhanced.test.js +46 -0
- package/backend/tests-d6-version-anomaly.test.js +58 -0
- package/backend/tests-d6.test.js +116 -0
- package/backend/tests-d6c.test.js +106 -0
- package/backend/tests-d7-obfuscation.test.js +91 -0
- package/backend/tests.test.js +898 -0
- package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
- package/backend/vsix-scan/detectors/burst-publish.js +52 -52
- package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
- package/backend/vsix-scan/detectors/known-ioc.js +105 -105
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
- package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
- package/backend/vsix-scan/index.js +183 -183
- package/backend/vsix-scan/marketplace-client.js +145 -145
- package/backend/vsix-scan/vsix-iocs.json +31 -31
- package/cli/cli.js +458 -458
- package/package.json +74 -57
- 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,255 +1,255 @@
|
|
|
1
|
-
export function generateHTML(scans) {
|
|
2
|
-
const rows = scans.map(s => {
|
|
3
|
-
const findings = s.findings || [];
|
|
4
|
-
const sevMap = { critical: 5, high: 4, medium: 3, low: 2, info: 1 };
|
|
5
|
-
const worst = findings.reduce((m, f) => Math.max(m, sevMap[f.severity] || 0), 0);
|
|
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('');
|
|
11
|
-
return { name: s.package_name, worstLabel, color, count: findings.length, findingRows };
|
|
12
|
-
});
|
|
13
|
-
|
|
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;
|
|
19
|
-
|
|
20
|
-
const nistMap = generateNistTable(scans);
|
|
21
|
-
|
|
22
|
-
return `<!DOCTYPE html>
|
|
23
|
-
<html lang="en">
|
|
24
|
-
<head>
|
|
25
|
-
<meta charset="UTF-8">
|
|
26
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
27
|
-
<title>npm-scan Report</title>
|
|
28
|
-
<style>
|
|
29
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 960px; margin: 0 auto; padding: 20px; background: #0d1117; color: #c9d1d9; }
|
|
30
|
-
h1 { color: #58a6ff; border-bottom: 1px solid #30363d; padding-bottom: 10px; }
|
|
31
|
-
h2 { color: #8b949e; margin-top: 28px; }
|
|
32
|
-
table { width: 100%; border-collapse: collapse; margin: 12px 0; }
|
|
33
|
-
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #30363d; }
|
|
34
|
-
th { background: #161b22; font-weight: 600; }
|
|
35
|
-
.summary { display: flex; gap: 16px; margin: 16px 0; flex-wrap: wrap; }
|
|
36
|
-
.badge { padding: 4px 12px; border-radius: 12px; font-size: 13px; font-weight: 600; }
|
|
37
|
-
.critical { background: #d73a49; color: #fff; }
|
|
38
|
-
.high { background: #cb2431; color: #fff; }
|
|
39
|
-
.medium { background: #f66a0a; color: #fff; }
|
|
40
|
-
.low { background: #dbab09; color: #000; }
|
|
41
|
-
.clean { background: #28a745; color: #fff; }
|
|
42
|
-
.meta { color: #8b949e; font-size: 13px; margin-top: 30px; }
|
|
43
|
-
.nist-pass { background: #1b3a1b; color: #7ee787; }
|
|
44
|
-
.nist-fail { background: #3a1b1b; color: #ff7b72; }
|
|
45
|
-
</style>
|
|
46
|
-
</head>
|
|
47
|
-
<body>
|
|
48
|
-
<h1>npm-scan Report</h1>
|
|
49
|
-
<p>Generated ${new Date().toISOString()}. ${scans.length} packages scanned.</p>
|
|
50
|
-
|
|
51
|
-
<div class="summary">
|
|
52
|
-
<div class="badge critical">critical: ${criticalCount}</div>
|
|
53
|
-
<div class="badge high">high: ${highCount}</div>
|
|
54
|
-
<div class="badge medium">medium: ${mediumCount}</div>
|
|
55
|
-
<div class="badge low">low: ${lowCount}</div>
|
|
56
|
-
<div class="badge clean">clean: ${cleanCount}</div>
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
<h2>Findings</h2>
|
|
60
|
-
<table>
|
|
61
|
-
<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>
|
|
63
|
-
</table>
|
|
64
|
-
|
|
65
|
-
<h2>NIST SP 800-161 Compliance Summary</h2>
|
|
66
|
-
${nistMap}
|
|
67
|
-
|
|
68
|
-
<p class="meta">npm-scan v${process.env.npm_package_version || '0.3.2'} | Apache-2.0 + Commons Clause | NIST SP 800-161 mapped</p>
|
|
69
|
-
</body>
|
|
70
|
-
</html>`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function getAtkFindings(scans) {
|
|
74
|
-
const map = {};
|
|
75
|
-
for (const s of scans) {
|
|
76
|
-
for (const f of (s.findings || [])) {
|
|
77
|
-
const key = f.atk_id || f.id;
|
|
78
|
-
if (!map[key]) map[key] = [];
|
|
79
|
-
map[key].push(f);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return map;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const NIST_SR_MAP = {
|
|
86
|
-
'ATK-001': { control: 'SR-3.1', title: 'Malicious code detection' },
|
|
87
|
-
'ATK-002': { control: 'SR-4.2', title: 'Code obfuscation analysis' },
|
|
88
|
-
'ATK-003': { control: 'SR-5.3', title: 'Credential protection' },
|
|
89
|
-
'ATK-004': { control: 'SR-6.4', title: 'Persistence monitoring' },
|
|
90
|
-
'ATK-005': { control: 'SR-7.5', title: 'Data exfiltration prevention' },
|
|
91
|
-
'ATK-006': { control: 'SR-2.2', title: 'Dependency validation' },
|
|
92
|
-
'ATK-007': { control: 'SR-2.1', title: 'Typosquatting prevention' },
|
|
93
|
-
'ATK-008': { control: 'SR-8.1', title: 'Integrity verification' },
|
|
94
|
-
'ATK-009': { control: 'SR-9.2', title: 'Conditional behavior analysis' },
|
|
95
|
-
'ATK-010': { control: 'SR-10.3', title: 'Anti-evasion detection' },
|
|
96
|
-
'ATK-011': { control: 'SR-11.4', title: 'Supply chain propagation monitoring' },
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
export function generateText(scans) {
|
|
100
|
-
const lines = [];
|
|
101
|
-
lines.push('npm-scan Report');
|
|
102
|
-
lines.push('================');
|
|
103
|
-
lines.push(`Generated: ${new Date().toISOString()}`);
|
|
104
|
-
lines.push(`Packages scanned: ${scans.length}`);
|
|
105
|
-
lines.push('');
|
|
106
|
-
|
|
107
|
-
let totalFindings = 0;
|
|
108
|
-
const sevMap = { critical: 5, high: 4, medium: 3, low: 2, info: 1 };
|
|
109
|
-
|
|
110
|
-
const sevCounts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
|
111
|
-
const sevLabel = ['', 'info', 'low', 'medium', 'high', 'critical'];
|
|
112
|
-
|
|
113
|
-
for (const s of scans) {
|
|
114
|
-
const findings = s.findings || [];
|
|
115
|
-
totalFindings += findings.length;
|
|
116
|
-
|
|
117
|
-
const worst = findings.reduce((m, f) => Math.max(m, sevMap[f.severity] || 0), 0);
|
|
118
|
-
const worstLabel = sevLabel[worst] || 'clean';
|
|
119
|
-
|
|
120
|
-
lines.push(`${s.package_name}@${s.version || 'unknown'} \u2500\u2500 ${findings.length} findings (worst: ${worstLabel})`);
|
|
121
|
-
|
|
122
|
-
for (const f of findings) {
|
|
123
|
-
const desc = (f.description || f.title || '').slice(0, 80);
|
|
124
|
-
sevCounts[f.severity] = (sevCounts[f.severity] || 0) + 1;
|
|
125
|
-
lines.push(` ${f.atk_id || f.id} ${f.severity.padEnd(8)} ${desc}`);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (!findings.length) {
|
|
129
|
-
lines.push(` (clean \u2014 no findings)`);
|
|
130
|
-
}
|
|
131
|
-
lines.push('');
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
lines.push('--- Severity Summary ---');
|
|
135
|
-
for (const sev of ['critical', 'high', 'medium', 'low']) {
|
|
136
|
-
lines.push(` ${sev}: ${sevCounts[sev] || 0}`);
|
|
137
|
-
}
|
|
138
|
-
lines.push(` total: ${totalFindings} findings across ${scans.length} packages`);
|
|
139
|
-
lines.push('');
|
|
140
|
-
|
|
141
|
-
return lines.join('\n');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function generateNistTable(scans) {
|
|
145
|
-
const atkMap = getAtkFindings(scans);
|
|
146
|
-
let rows = '';
|
|
147
|
-
for (const [atkId, { control, title }] of Object.entries(NIST_SR_MAP)) {
|
|
148
|
-
const findings = atkMap[atkId] || [];
|
|
149
|
-
const status = findings.length > 0 ? 'fail' : 'pass';
|
|
150
|
-
const label = findings.length > 0 ? `${findings.length} findings` : 'No findings';
|
|
151
|
-
const colorClass = status === 'pass' ? 'nist-pass' : 'nist-fail';
|
|
152
|
-
rows += `<tr><td>${control}</td><td>${title}</td><td class="${colorClass}">${label}</td><td>${atkId}</td></tr>`;
|
|
153
|
-
}
|
|
154
|
-
return `<table>
|
|
155
|
-
<thead><tr><th>NIST Control</th><th>Control Title</th><th>Status</th><th>ATK ID</th></tr></thead>
|
|
156
|
-
<tbody>${rows}</tbody>
|
|
157
|
-
</table>`;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function generateSARIF(scan, format = 'json') {
|
|
161
|
-
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
|
-
}
|
|
176
|
-
},
|
|
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
|
-
}];
|
|
192
|
-
|
|
193
|
-
const sarif = {
|
|
194
|
-
version: '2.1.0',
|
|
195
|
-
schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
196
|
-
runs
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
return format === 'pretty' ? JSON.stringify(sarif, null, 2) : JSON.stringify(sarif);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export function generateCSV(scans) {
|
|
203
|
-
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');
|
|
215
|
-
return headers + rows;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export function calculateRiskScore(findings, totalPackages = 1) {
|
|
219
|
-
const weights = { low: 1, medium: 3, high: 7, critical: 10 };
|
|
220
|
-
const rawScore = findings.reduce((sum, f) => sum + (weights[f.severity] || 0), 0) / totalPackages;
|
|
221
|
-
return Math.min(rawScore, 10).toFixed(1);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
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' }
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
export function generateSTIG(scans) {
|
|
239
|
-
const rows = [];
|
|
240
|
-
for (const [stigId, info] of Object.entries(STIG_MAP)) {
|
|
241
|
-
const findings = scans.flatMap(s => (s.findings || []).filter(f => f.id === info.atk));
|
|
242
|
-
const status = findings.length > 0 ? 'NOT APPLICABLE' : 'COMPLETE';
|
|
243
|
-
const findingsList = findings.map(f => `${f.severity.toUpperCase()}: ${f.title}`).join('; ') || 'None';
|
|
244
|
-
rows.push(`| ${stigId} | ${info.title} | ${status} | ${findingsList} |`);
|
|
245
|
-
}
|
|
246
|
-
return `# STIG Compliance Report
|
|
247
|
-
Generated: ${new Date().toISOString()}
|
|
248
|
-
|
|
249
|
-
| STIG ID | Control Title | Status | Findings |
|
|
250
|
-
|---------|--------------|--------|----------|
|
|
251
|
-
${rows.join('\n')}
|
|
252
|
-
|
|
253
|
-
---
|
|
254
|
-
*This report maps application security controls to DISA STIG requirements.*`;
|
|
1
|
+
export function generateHTML(scans) {
|
|
2
|
+
const rows = scans.map(s => {
|
|
3
|
+
const findings = s.findings || [];
|
|
4
|
+
const sevMap = { critical: 5, high: 4, medium: 3, low: 2, info: 1 };
|
|
5
|
+
const worst = findings.reduce((m, f) => Math.max(m, sevMap[f.severity] || 0), 0);
|
|
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('');
|
|
11
|
+
return { name: s.package_name, worstLabel, color, count: findings.length, findingRows };
|
|
12
|
+
});
|
|
13
|
+
|
|
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;
|
|
19
|
+
|
|
20
|
+
const nistMap = generateNistTable(scans);
|
|
21
|
+
|
|
22
|
+
return `<!DOCTYPE html>
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="UTF-8">
|
|
26
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
27
|
+
<title>npm-scan Report</title>
|
|
28
|
+
<style>
|
|
29
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 960px; margin: 0 auto; padding: 20px; background: #0d1117; color: #c9d1d9; }
|
|
30
|
+
h1 { color: #58a6ff; border-bottom: 1px solid #30363d; padding-bottom: 10px; }
|
|
31
|
+
h2 { color: #8b949e; margin-top: 28px; }
|
|
32
|
+
table { width: 100%; border-collapse: collapse; margin: 12px 0; }
|
|
33
|
+
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #30363d; }
|
|
34
|
+
th { background: #161b22; font-weight: 600; }
|
|
35
|
+
.summary { display: flex; gap: 16px; margin: 16px 0; flex-wrap: wrap; }
|
|
36
|
+
.badge { padding: 4px 12px; border-radius: 12px; font-size: 13px; font-weight: 600; }
|
|
37
|
+
.critical { background: #d73a49; color: #fff; }
|
|
38
|
+
.high { background: #cb2431; color: #fff; }
|
|
39
|
+
.medium { background: #f66a0a; color: #fff; }
|
|
40
|
+
.low { background: #dbab09; color: #000; }
|
|
41
|
+
.clean { background: #28a745; color: #fff; }
|
|
42
|
+
.meta { color: #8b949e; font-size: 13px; margin-top: 30px; }
|
|
43
|
+
.nist-pass { background: #1b3a1b; color: #7ee787; }
|
|
44
|
+
.nist-fail { background: #3a1b1b; color: #ff7b72; }
|
|
45
|
+
</style>
|
|
46
|
+
</head>
|
|
47
|
+
<body>
|
|
48
|
+
<h1>npm-scan Report</h1>
|
|
49
|
+
<p>Generated ${new Date().toISOString()}. ${scans.length} packages scanned.</p>
|
|
50
|
+
|
|
51
|
+
<div class="summary">
|
|
52
|
+
<div class="badge critical">critical: ${criticalCount}</div>
|
|
53
|
+
<div class="badge high">high: ${highCount}</div>
|
|
54
|
+
<div class="badge medium">medium: ${mediumCount}</div>
|
|
55
|
+
<div class="badge low">low: ${lowCount}</div>
|
|
56
|
+
<div class="badge clean">clean: ${cleanCount}</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<h2>Findings</h2>
|
|
60
|
+
<table>
|
|
61
|
+
<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>
|
|
63
|
+
</table>
|
|
64
|
+
|
|
65
|
+
<h2>NIST SP 800-161 Compliance Summary</h2>
|
|
66
|
+
${nistMap}
|
|
67
|
+
|
|
68
|
+
<p class="meta">npm-scan v${process.env.npm_package_version || '0.3.2'} | Apache-2.0 + Commons Clause | NIST SP 800-161 mapped</p>
|
|
69
|
+
</body>
|
|
70
|
+
</html>`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getAtkFindings(scans) {
|
|
74
|
+
const map = {};
|
|
75
|
+
for (const s of scans) {
|
|
76
|
+
for (const f of (s.findings || [])) {
|
|
77
|
+
const key = f.atk_id || f.id;
|
|
78
|
+
if (!map[key]) map[key] = [];
|
|
79
|
+
map[key].push(f);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return map;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const NIST_SR_MAP = {
|
|
86
|
+
'ATK-001': { control: 'SR-3.1', title: 'Malicious code detection' },
|
|
87
|
+
'ATK-002': { control: 'SR-4.2', title: 'Code obfuscation analysis' },
|
|
88
|
+
'ATK-003': { control: 'SR-5.3', title: 'Credential protection' },
|
|
89
|
+
'ATK-004': { control: 'SR-6.4', title: 'Persistence monitoring' },
|
|
90
|
+
'ATK-005': { control: 'SR-7.5', title: 'Data exfiltration prevention' },
|
|
91
|
+
'ATK-006': { control: 'SR-2.2', title: 'Dependency validation' },
|
|
92
|
+
'ATK-007': { control: 'SR-2.1', title: 'Typosquatting prevention' },
|
|
93
|
+
'ATK-008': { control: 'SR-8.1', title: 'Integrity verification' },
|
|
94
|
+
'ATK-009': { control: 'SR-9.2', title: 'Conditional behavior analysis' },
|
|
95
|
+
'ATK-010': { control: 'SR-10.3', title: 'Anti-evasion detection' },
|
|
96
|
+
'ATK-011': { control: 'SR-11.4', title: 'Supply chain propagation monitoring' },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export function generateText(scans) {
|
|
100
|
+
const lines = [];
|
|
101
|
+
lines.push('npm-scan Report');
|
|
102
|
+
lines.push('================');
|
|
103
|
+
lines.push(`Generated: ${new Date().toISOString()}`);
|
|
104
|
+
lines.push(`Packages scanned: ${scans.length}`);
|
|
105
|
+
lines.push('');
|
|
106
|
+
|
|
107
|
+
let totalFindings = 0;
|
|
108
|
+
const sevMap = { critical: 5, high: 4, medium: 3, low: 2, info: 1 };
|
|
109
|
+
|
|
110
|
+
const sevCounts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
|
111
|
+
const sevLabel = ['', 'info', 'low', 'medium', 'high', 'critical'];
|
|
112
|
+
|
|
113
|
+
for (const s of scans) {
|
|
114
|
+
const findings = s.findings || [];
|
|
115
|
+
totalFindings += findings.length;
|
|
116
|
+
|
|
117
|
+
const worst = findings.reduce((m, f) => Math.max(m, sevMap[f.severity] || 0), 0);
|
|
118
|
+
const worstLabel = sevLabel[worst] || 'clean';
|
|
119
|
+
|
|
120
|
+
lines.push(`${s.package_name}@${s.version || 'unknown'} \u2500\u2500 ${findings.length} findings (worst: ${worstLabel})`);
|
|
121
|
+
|
|
122
|
+
for (const f of findings) {
|
|
123
|
+
const desc = (f.description || f.title || '').slice(0, 80);
|
|
124
|
+
sevCounts[f.severity] = (sevCounts[f.severity] || 0) + 1;
|
|
125
|
+
lines.push(` ${f.atk_id || f.id} ${f.severity.padEnd(8)} ${desc}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!findings.length) {
|
|
129
|
+
lines.push(` (clean \u2014 no findings)`);
|
|
130
|
+
}
|
|
131
|
+
lines.push('');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
lines.push('--- Severity Summary ---');
|
|
135
|
+
for (const sev of ['critical', 'high', 'medium', 'low']) {
|
|
136
|
+
lines.push(` ${sev}: ${sevCounts[sev] || 0}`);
|
|
137
|
+
}
|
|
138
|
+
lines.push(` total: ${totalFindings} findings across ${scans.length} packages`);
|
|
139
|
+
lines.push('');
|
|
140
|
+
|
|
141
|
+
return lines.join('\n');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function generateNistTable(scans) {
|
|
145
|
+
const atkMap = getAtkFindings(scans);
|
|
146
|
+
let rows = '';
|
|
147
|
+
for (const [atkId, { control, title }] of Object.entries(NIST_SR_MAP)) {
|
|
148
|
+
const findings = atkMap[atkId] || [];
|
|
149
|
+
const status = findings.length > 0 ? 'fail' : 'pass';
|
|
150
|
+
const label = findings.length > 0 ? `${findings.length} findings` : 'No findings';
|
|
151
|
+
const colorClass = status === 'pass' ? 'nist-pass' : 'nist-fail';
|
|
152
|
+
rows += `<tr><td>${control}</td><td>${title}</td><td class="${colorClass}">${label}</td><td>${atkId}</td></tr>`;
|
|
153
|
+
}
|
|
154
|
+
return `<table>
|
|
155
|
+
<thead><tr><th>NIST Control</th><th>Control Title</th><th>Status</th><th>ATK ID</th></tr></thead>
|
|
156
|
+
<tbody>${rows}</tbody>
|
|
157
|
+
</table>`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function generateSARIF(scan, format = 'json') {
|
|
161
|
+
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
|
+
}
|
|
176
|
+
},
|
|
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
|
+
}];
|
|
192
|
+
|
|
193
|
+
const sarif = {
|
|
194
|
+
version: '2.1.0',
|
|
195
|
+
schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
196
|
+
runs
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
return format === 'pretty' ? JSON.stringify(sarif, null, 2) : JSON.stringify(sarif);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function generateCSV(scans) {
|
|
203
|
+
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');
|
|
215
|
+
return headers + rows;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function calculateRiskScore(findings, totalPackages = 1) {
|
|
219
|
+
const weights = { low: 1, medium: 3, high: 7, critical: 10 };
|
|
220
|
+
const rawScore = findings.reduce((sum, f) => sum + (weights[f.severity] || 0), 0) / totalPackages;
|
|
221
|
+
return Math.min(rawScore, 10).toFixed(1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
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' }
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export function generateSTIG(scans) {
|
|
239
|
+
const rows = [];
|
|
240
|
+
for (const [stigId, info] of Object.entries(STIG_MAP)) {
|
|
241
|
+
const findings = scans.flatMap(s => (s.findings || []).filter(f => f.id === info.atk));
|
|
242
|
+
const status = findings.length > 0 ? 'NOT APPLICABLE' : 'COMPLETE';
|
|
243
|
+
const findingsList = findings.map(f => `${f.severity.toUpperCase()}: ${f.title}`).join('; ') || 'None';
|
|
244
|
+
rows.push(`| ${stigId} | ${info.title} | ${status} | ${findingsList} |`);
|
|
245
|
+
}
|
|
246
|
+
return `# STIG Compliance Report
|
|
247
|
+
Generated: ${new Date().toISOString()}
|
|
248
|
+
|
|
249
|
+
| STIG ID | Control Title | Status | Findings |
|
|
250
|
+
|---------|--------------|--------|----------|
|
|
251
|
+
${rows.join('\n')}
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
*This report maps application security controls to DISA STIG requirements.*`;
|
|
255
255
|
}
|
package/backend/sbom.js
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
export function generateSBOM(pkgJson, findings, format = 'json') {
|
|
2
|
-
if (format === 'spdx') return generateSPDX(pkgJson, findings);
|
|
3
|
-
return generateCycloneDX(pkgJson, findings);
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
function generateCycloneDX(pkgJson, findings) {
|
|
7
|
-
const bom = {
|
|
8
|
-
bomFormat: 'CycloneDX',
|
|
9
|
-
specVersion: '1.5',
|
|
10
|
-
version: 1,
|
|
11
|
-
metadata: {
|
|
12
|
-
component: {
|
|
13
|
-
type: 'library',
|
|
14
|
-
name: pkgJson.name || 'unknown',
|
|
15
|
-
version: pkgJson.version || 'unknown',
|
|
16
|
-
purl: `pkg:npm/${pkgJson.name || 'unknown'}@${pkgJson.version || 'unknown'}`
|
|
17
|
-
},
|
|
18
|
-
tools: [{ name: 'npm-scan', version: process.env.npm_package_version || '0.3.2' }]
|
|
19
|
-
},
|
|
20
|
-
vulnerabilities: findings.map(f => {
|
|
21
|
-
const atkId = f.atk_id || f.id;
|
|
22
|
-
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'
|
|
28
|
-
};
|
|
29
|
-
})
|
|
30
|
-
};
|
|
31
|
-
return JSON.stringify(bom, null, 2);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function generateSPDX(pkgJson, findings) {
|
|
35
|
-
const pkgName = pkgJson.name || 'unknown';
|
|
36
|
-
const pkgVer = pkgJson.version || 'unknown';
|
|
37
|
-
const spdx = {
|
|
38
|
-
spdxVersion: 'SPDX-2.3',
|
|
39
|
-
dataLicense: 'CC0-1.0',
|
|
40
|
-
SPDXID: 'SPDXRef-DOCUMENT',
|
|
41
|
-
name: `${pkgName}@${pkgVer} npm-scan SBOM`,
|
|
42
|
-
documentNamespace: `https://npm-scan.io/spdx/${pkgName}-${pkgVer}`,
|
|
43
|
-
creationInfo: {
|
|
44
|
-
creators: ['Tool: npm-scan'],
|
|
45
|
-
created: new Date().toISOString()
|
|
46
|
-
},
|
|
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 => ({
|
|
60
|
-
annotationDate: new Date().toISOString(),
|
|
61
|
-
annotationType: 'OTHER',
|
|
62
|
-
annotator: 'Tool: npm-scan',
|
|
63
|
-
comment: `[${f.atk_id || f.id}] ${f.severity.toUpperCase()}: ${f.description || f.title || ''}`
|
|
64
|
-
}))
|
|
65
|
-
};
|
|
66
|
-
return JSON.stringify(spdx, null, 2);
|
|
1
|
+
export function generateSBOM(pkgJson, findings, format = 'json') {
|
|
2
|
+
if (format === 'spdx') return generateSPDX(pkgJson, findings);
|
|
3
|
+
return generateCycloneDX(pkgJson, findings);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function generateCycloneDX(pkgJson, findings) {
|
|
7
|
+
const bom = {
|
|
8
|
+
bomFormat: 'CycloneDX',
|
|
9
|
+
specVersion: '1.5',
|
|
10
|
+
version: 1,
|
|
11
|
+
metadata: {
|
|
12
|
+
component: {
|
|
13
|
+
type: 'library',
|
|
14
|
+
name: pkgJson.name || 'unknown',
|
|
15
|
+
version: pkgJson.version || 'unknown',
|
|
16
|
+
purl: `pkg:npm/${pkgJson.name || 'unknown'}@${pkgJson.version || 'unknown'}`
|
|
17
|
+
},
|
|
18
|
+
tools: [{ name: 'npm-scan', version: process.env.npm_package_version || '0.3.2' }]
|
|
19
|
+
},
|
|
20
|
+
vulnerabilities: findings.map(f => {
|
|
21
|
+
const atkId = f.atk_id || f.id;
|
|
22
|
+
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'
|
|
28
|
+
};
|
|
29
|
+
})
|
|
30
|
+
};
|
|
31
|
+
return JSON.stringify(bom, null, 2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function generateSPDX(pkgJson, findings) {
|
|
35
|
+
const pkgName = pkgJson.name || 'unknown';
|
|
36
|
+
const pkgVer = pkgJson.version || 'unknown';
|
|
37
|
+
const spdx = {
|
|
38
|
+
spdxVersion: 'SPDX-2.3',
|
|
39
|
+
dataLicense: 'CC0-1.0',
|
|
40
|
+
SPDXID: 'SPDXRef-DOCUMENT',
|
|
41
|
+
name: `${pkgName}@${pkgVer} npm-scan SBOM`,
|
|
42
|
+
documentNamespace: `https://npm-scan.io/spdx/${pkgName}-${pkgVer}`,
|
|
43
|
+
creationInfo: {
|
|
44
|
+
creators: ['Tool: npm-scan'],
|
|
45
|
+
created: new Date().toISOString()
|
|
46
|
+
},
|
|
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 => ({
|
|
60
|
+
annotationDate: new Date().toISOString(),
|
|
61
|
+
annotationType: 'OTHER',
|
|
62
|
+
annotator: 'Tool: npm-scan',
|
|
63
|
+
comment: `[${f.atk_id || f.id}] ${f.severity.toUpperCase()}: ${f.description || f.title || ''}`
|
|
64
|
+
}))
|
|
65
|
+
};
|
|
66
|
+
return JSON.stringify(spdx, null, 2);
|
|
67
67
|
}
|