@lateos/npm-scan 1.0.0 → 1.1.1
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/README.de.md +3 -98
- package/README.fr.md +3 -98
- package/README.ja.md +3 -98
- package/README.md +2 -122
- package/README.zh.md +3 -98
- package/backend/cra.js +113 -21
- 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 +48 -3
- 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 +181 -34
- package/backend/detectors/lib/ast-patterns.js +4 -1
- package/backend/detectors/lib/entropy-analyzer.js +12 -4
- 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 +109 -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 +45 -17
- package/backend/detectors/tier1-self-propagation.js +115 -0
- package/backend/detectors/tier1-slsa-attestation.js +1 -1
- package/backend/detectors/tier1-transitive-deps.js +182 -0
- package/backend/detectors/tier1-typosquat.js +129 -50
- package/backend/detectors/tier1-version-anomaly.js +77 -41
- 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 +78 -19
- 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 +14 -8
- package/backend/scripts/analyze-validation.js +27 -21
- package/backend/scripts/detect-false-positives.js +20 -10
- package/backend/scripts/fetch-top-packages.js +197 -49
- package/backend/scripts/validate-d10-d13.js +103 -0
- package/backend/scripts/validate-detectors.js +26 -17
- 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 +13 -12
- package/backend/tests-d6-version-anomaly.test.js +17 -8
- package/backend/tests-d6.test.js +24 -14
- package/backend/tests-d6c.test.js +27 -14
- package/backend/tests-d7-obfuscation.test.js +9 -12
- package/backend/tests.test.js +182 -83
- 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 +12 -3
package/backend/policy.js
CHANGED
|
@@ -5,26 +5,89 @@ const SEVERITY_ORDER = ['none', 'low', 'medium', 'high', 'critical'];
|
|
|
5
5
|
const VALID_SEVERITIES = new Set(SEVERITY_ORDER);
|
|
6
6
|
|
|
7
7
|
const KNOWN_REPUTABLE_PACKAGES = new Set([
|
|
8
|
-
'react',
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
8
|
+
'react',
|
|
9
|
+
'react-dom',
|
|
10
|
+
'vue',
|
|
11
|
+
'angular',
|
|
12
|
+
'next',
|
|
13
|
+
'nuxt',
|
|
14
|
+
'express',
|
|
15
|
+
'fastify',
|
|
16
|
+
'hono',
|
|
17
|
+
'koa',
|
|
18
|
+
'connect',
|
|
19
|
+
'webpack',
|
|
20
|
+
'vite',
|
|
21
|
+
'rollup',
|
|
22
|
+
'esbuild',
|
|
23
|
+
'typescript',
|
|
24
|
+
'babel-core',
|
|
25
|
+
'lodash',
|
|
26
|
+
'ramda',
|
|
27
|
+
'underscore',
|
|
28
|
+
'axios',
|
|
29
|
+
'node-fetch',
|
|
30
|
+
'got',
|
|
31
|
+
'superagent',
|
|
32
|
+
'sequelize',
|
|
33
|
+
'prisma',
|
|
34
|
+
'typeorm',
|
|
35
|
+
'mongoose',
|
|
36
|
+
'jest',
|
|
37
|
+
'mocha',
|
|
38
|
+
'vitest',
|
|
39
|
+
'ava',
|
|
40
|
+
'prettier',
|
|
41
|
+
'eslint',
|
|
42
|
+
'stylelint',
|
|
43
|
+
'socket.io',
|
|
44
|
+
'ws',
|
|
45
|
+
'rimraf',
|
|
46
|
+
'glob',
|
|
47
|
+
'minimatch',
|
|
48
|
+
'fs-extra',
|
|
49
|
+
'electron',
|
|
50
|
+
'puppeteer',
|
|
51
|
+
'playwright',
|
|
52
|
+
'sharp',
|
|
53
|
+
'node-canvas',
|
|
54
|
+
'ffmpeg-static',
|
|
55
|
+
'turbo',
|
|
56
|
+
'react-scripts',
|
|
57
|
+
'@angular/cli',
|
|
58
|
+
'gatsby',
|
|
59
|
+
'parcel',
|
|
60
|
+
'tslib',
|
|
61
|
+
'core-js',
|
|
62
|
+
'regenerator-runtime',
|
|
63
|
+
'buffer',
|
|
64
|
+
'node-gyp',
|
|
65
|
+
'node-pre-gyp',
|
|
66
|
+
'winston',
|
|
67
|
+
'uuid',
|
|
68
|
+
'moment',
|
|
69
|
+
'dotenv',
|
|
70
|
+
'pg',
|
|
71
|
+
'semver',
|
|
72
|
+
'redux',
|
|
73
|
+
'redis',
|
|
74
|
+
'dayjs',
|
|
75
|
+
'luxon',
|
|
76
|
+
'chalk',
|
|
77
|
+
'debug',
|
|
78
|
+
'cors',
|
|
79
|
+
'helmet',
|
|
80
|
+
'multer',
|
|
81
|
+
'body-parser',
|
|
82
|
+
'cheerio',
|
|
83
|
+
'bluebird',
|
|
84
|
+
'bcrypt',
|
|
85
|
+
'commander',
|
|
86
|
+
'yargs',
|
|
87
|
+
'passport',
|
|
88
|
+
'jsonwebtoken',
|
|
89
|
+
'nodemailer',
|
|
90
|
+
'class-validator',
|
|
28
91
|
]);
|
|
29
92
|
|
|
30
93
|
function severityIndex(s) {
|
|
@@ -32,8 +95,12 @@ function severityIndex(s) {
|
|
|
32
95
|
}
|
|
33
96
|
|
|
34
97
|
function matchesFilePath(filePath, pattern) {
|
|
35
|
-
if (!pattern)
|
|
36
|
-
|
|
98
|
+
if (!pattern) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if (pattern === '*') {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
37
104
|
const regexPattern = pattern
|
|
38
105
|
.replace(/\./g, '\\.')
|
|
39
106
|
.replace(/\*\*/g, '___DOUBLE_STAR___')
|
|
@@ -44,49 +111,90 @@ function matchesFilePath(filePath, pattern) {
|
|
|
44
111
|
|
|
45
112
|
function matchesContext(finding, rule) {
|
|
46
113
|
const ctx = finding.context;
|
|
47
|
-
if (!ctx)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (rule.context?.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (rule.context?.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
114
|
+
if (!ctx) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (rule.context?.is_dist_build === true && !ctx.is_dist_build) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (rule.context?.is_dist_build === false && ctx.is_dist_build) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
if (rule.context?.is_test_fixture === true && !ctx.is_test_fixture) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (rule.context?.is_test_fixture === false && ctx.is_test_fixture) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (rule.context?.is_lifecycle_hook === true && !ctx.is_lifecycle_hook) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (rule.context?.is_lifecycle_hook === false && ctx.is_lifecycle_hook) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
if (rule.context?.is_known_safe_domain === true && !ctx.is_known_safe_domain) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
if (rule.context?.is_known_safe_domain === false && ctx.is_known_safe_domain) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (rule.context?.file_path && !matchesFilePath(ctx.file_path, rule.context.file_path)) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
59
146
|
if (rule.context?.url_domain) {
|
|
60
|
-
if (!ctx.url_domain)
|
|
147
|
+
if (!ctx.url_domain) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
61
150
|
const domainPattern = rule.context.url_domain.replace(/\*/g, '.*');
|
|
62
|
-
if (!new RegExp(`^${domainPattern}$`).test(ctx.url_domain))
|
|
151
|
+
if (!new RegExp(`^${domainPattern}$`).test(ctx.url_domain)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
63
154
|
}
|
|
64
155
|
|
|
65
156
|
return true;
|
|
66
157
|
}
|
|
67
158
|
|
|
68
159
|
function matchesKnownReputable(packageName) {
|
|
69
|
-
if (KNOWN_REPUTABLE_PACKAGES.has(packageName))
|
|
160
|
+
if (KNOWN_REPUTABLE_PACKAGES.has(packageName)) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
70
163
|
const [scope, name] = packageName.split('/');
|
|
71
|
-
if (scope && name && KNOWN_REPUTABLE_PACKAGES.has(`${scope}/*`))
|
|
164
|
+
if (scope && name && KNOWN_REPUTABLE_PACKAGES.has(`${scope}/*`)) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
72
167
|
return false;
|
|
73
168
|
}
|
|
74
169
|
|
|
75
170
|
function getPackageReputationTier(pkgName) {
|
|
76
171
|
const name = pkgName?.replace(/^@/, '').replace(/\/.*/, '') || '';
|
|
77
|
-
if (matchesKnownReputable(name))
|
|
172
|
+
if (matchesKnownReputable(name)) {
|
|
173
|
+
return 'trusted';
|
|
174
|
+
}
|
|
78
175
|
return 'unknown';
|
|
79
176
|
}
|
|
80
177
|
|
|
81
178
|
function matchesSuppressRule(finding, pkgName, rule) {
|
|
82
|
-
if (rule.atk_id !== (finding.atk_id || finding.id))
|
|
83
|
-
|
|
179
|
+
if (rule.atk_id !== (finding.atk_id || finding.id)) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if (rule.package && rule.package !== '*' && rule.package !== pkgName) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
84
185
|
|
|
85
|
-
if (rule.context && !matchesContext(finding, rule))
|
|
186
|
+
if (rule.context && !matchesContext(finding, rule)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
86
189
|
|
|
87
190
|
if (rule.reputation_tier) {
|
|
88
191
|
const tier = getPackageReputationTier(pkgName);
|
|
89
|
-
if (
|
|
192
|
+
if (
|
|
193
|
+
rule.reputation_tier !== tier &&
|
|
194
|
+
!(rule.reputation_tier === '*' || rule.reputation_tier === 'any')
|
|
195
|
+
) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
90
198
|
}
|
|
91
199
|
|
|
92
200
|
return true;
|
|
@@ -109,13 +217,17 @@ function loadPolicy(path) {
|
|
|
109
217
|
if (policy.severity_overrides) {
|
|
110
218
|
for (const [atkId, severity] of Object.entries(policy.severity_overrides)) {
|
|
111
219
|
if (!VALID_SEVERITIES.has(severity)) {
|
|
112
|
-
throw new Error(
|
|
220
|
+
throw new Error(
|
|
221
|
+
`Invalid severity "${severity}" for ${atkId} — must be one of: low, medium, high, critical`
|
|
222
|
+
);
|
|
113
223
|
}
|
|
114
224
|
}
|
|
115
225
|
}
|
|
116
226
|
|
|
117
227
|
if (policy.fail_on && !VALID_SEVERITIES.has(policy.fail_on)) {
|
|
118
|
-
throw new Error(
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Invalid fail_on "${policy.fail_on}" — must be one of: none, low, medium, high, critical`
|
|
230
|
+
);
|
|
119
231
|
}
|
|
120
232
|
|
|
121
233
|
if (policy.suppress) {
|
|
@@ -143,7 +255,7 @@ function sanitizePolicy(policy) {
|
|
|
143
255
|
allow: { packages: policy.allow?.packages ?? [] },
|
|
144
256
|
severity_overrides: policy.severity_overrides ?? {},
|
|
145
257
|
fail_on: policy.fail_on ?? 'none',
|
|
146
|
-
suppress: (policy.suppress ?? []).map(r => ({
|
|
258
|
+
suppress: (policy.suppress ?? []).map((r) => ({
|
|
147
259
|
atk_id: r.atk_id,
|
|
148
260
|
package: r.package || '*',
|
|
149
261
|
reason: r.reason || '',
|
|
@@ -154,23 +266,29 @@ function sanitizePolicy(policy) {
|
|
|
154
266
|
}
|
|
155
267
|
|
|
156
268
|
function isAllowed(packageName, policy) {
|
|
157
|
-
if (!policy.allow.packages.length)
|
|
269
|
+
if (!policy.allow.packages.length) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
158
272
|
const nameOnly = packageName.split('@')[0];
|
|
159
|
-
return policy.allow.packages.some(p => p === packageName || p === nameOnly);
|
|
273
|
+
return policy.allow.packages.some((p) => p === packageName || p === nameOnly);
|
|
160
274
|
}
|
|
161
275
|
|
|
162
276
|
function applyPolicy(findings, packageName, policy) {
|
|
163
277
|
let filtered = [...findings];
|
|
164
278
|
|
|
165
279
|
if (policy.suppress.length) {
|
|
166
|
-
filtered = filtered.filter(f => {
|
|
167
|
-
if (f.context?.is_lifecycle_hook)
|
|
168
|
-
|
|
169
|
-
|
|
280
|
+
filtered = filtered.filter((f) => {
|
|
281
|
+
if (f.context?.is_lifecycle_hook) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
if (f.context?.is_multi_layer) {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
return !policy.suppress.some((r) => matchesSuppressRule(f, packageName, r));
|
|
170
288
|
});
|
|
171
289
|
}
|
|
172
290
|
|
|
173
|
-
filtered = filtered.map(f => {
|
|
291
|
+
filtered = filtered.map((f) => {
|
|
174
292
|
const override = policy.severity_overrides[f.atk_id || f.id];
|
|
175
293
|
if (override) {
|
|
176
294
|
return { ...f, severity: override, _severityOverridden: true };
|
|
@@ -184,10 +302,19 @@ function applyPolicy(findings, packageName, policy) {
|
|
|
184
302
|
}
|
|
185
303
|
|
|
186
304
|
function checkFailOn(findings, policy) {
|
|
187
|
-
if (policy.fail_on === 'none')
|
|
305
|
+
if (policy.fail_on === 'none') {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
188
308
|
|
|
189
309
|
const threshold = severityIndex(policy.fail_on);
|
|
190
|
-
return findings.some(f => severityIndex(f.severity) >= threshold);
|
|
310
|
+
return findings.some((f) => severityIndex(f.severity) >= threshold);
|
|
191
311
|
}
|
|
192
312
|
|
|
193
|
-
export {
|
|
313
|
+
export {
|
|
314
|
+
loadPolicy,
|
|
315
|
+
applyPolicy,
|
|
316
|
+
isAllowed,
|
|
317
|
+
getPackageReputationTier,
|
|
318
|
+
matchesContext,
|
|
319
|
+
KNOWN_REPUTABLE_PACKAGES,
|
|
320
|
+
};
|
package/backend/provenance.js
CHANGED
|
@@ -12,7 +12,13 @@ export function signManifest(manifest, key = HMAC_KEY) {
|
|
|
12
12
|
return createHmac('sha256', key).update(JSON.stringify(manifest)).digest('hex');
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function buildDetectionRule({
|
|
15
|
+
export function buildDetectionRule({
|
|
16
|
+
ruleId,
|
|
17
|
+
ruleName,
|
|
18
|
+
severity,
|
|
19
|
+
cveReferences = [],
|
|
20
|
+
campaignName,
|
|
21
|
+
}) {
|
|
16
22
|
return {
|
|
17
23
|
rule_id: ruleId,
|
|
18
24
|
rule_name: ruleName,
|
|
@@ -41,7 +47,12 @@ export function buildDetectionResult({ triggered, severity, indicators = [] }) {
|
|
|
41
47
|
|
|
42
48
|
export function buildAuditTrail({ detectionLogic, ruleProvenanceUrl, campaignSourceUrl }) {
|
|
43
49
|
const contentHash = hashContent(detectionLogic);
|
|
44
|
-
const manifest = {
|
|
50
|
+
const manifest = {
|
|
51
|
+
contentHash,
|
|
52
|
+
ruleProvenanceUrl,
|
|
53
|
+
campaignSourceUrl,
|
|
54
|
+
generatedAt: new Date().toISOString(),
|
|
55
|
+
};
|
|
45
56
|
return {
|
|
46
57
|
content_hash: contentHash,
|
|
47
58
|
rule_provenance_url: ruleProvenanceUrl,
|
|
@@ -60,7 +71,21 @@ export function buildDetectionRecord({ rule, scanMetadata, detectionResult, audi
|
|
|
60
71
|
};
|
|
61
72
|
}
|
|
62
73
|
|
|
63
|
-
export function attachProvenance(
|
|
74
|
+
export function attachProvenance(
|
|
75
|
+
evidence,
|
|
76
|
+
{
|
|
77
|
+
ruleId,
|
|
78
|
+
ruleName,
|
|
79
|
+
severity,
|
|
80
|
+
campaignName,
|
|
81
|
+
pkgName,
|
|
82
|
+
pkgVersion,
|
|
83
|
+
triggered,
|
|
84
|
+
indicators,
|
|
85
|
+
ruleProvenanceUrl,
|
|
86
|
+
campaignSourceUrl,
|
|
87
|
+
}
|
|
88
|
+
) {
|
|
64
89
|
const rule = buildDetectionRule({ ruleId, ruleName, severity, campaignName });
|
|
65
90
|
const scanMetadata = buildScanMetadata({
|
|
66
91
|
scannerVersion: '@lateos/npm-scan',
|
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
|
+
}
|