@lateos/npm-scan 0.16.4 → 0.17.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/.dockerignore +20 -20
- package/.husky/pre-commit +1 -1
- package/CHANGELOG.md +199 -199
- 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 +826 -826
- package/README.zh.md +708 -708
- package/SECURITY.md +72 -72
- package/backend/cra.js +68 -68
- 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/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 +75 -44
- 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 +219 -0
- package/backend/detectors/tier1-infostealer.js +280 -0
- package/backend/detectors/tier1-lifecycle-hook.js +176 -0
- package/backend/detectors/tier1-metadata-spoof.js +180 -0
- package/backend/detectors/tier1-typosquat.js +219 -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 -176
- package/backend/report.js +254 -254
- package/backend/sbom.js +66 -66
- 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/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/deploy/helm/npm-scan/Chart.yaml +21 -21
- package/deploy/helm/npm-scan/templates/_helpers.tpl +8 -8
- package/deploy/helm/npm-scan/templates/api.yaml +93 -93
- package/deploy/helm/npm-scan/templates/ingress.yaml +27 -27
- package/deploy/helm/npm-scan/templates/postgresql.yaml +66 -66
- package/deploy/helm/npm-scan/templates/secrets.yaml +18 -18
- package/deploy/helm/npm-scan/templates/worker.yaml +31 -31
- package/deploy/helm/npm-scan/values.byoc.yaml +74 -74
- package/deploy/helm/npm-scan/values.yaml +102 -102
- package/package.json +57 -57
- package/scripts/download-corpus.js +30 -30
- package/scripts/gen-mal-corpus.js +34 -34
- package/scripts/generate-campaign-fixtures.js +170 -0
- package/src/config/top-5000.json +87 -0
- package/test/fixtures/lockfiles/npm-lock.json +68 -68
- package/test/fixtures/lockfiles/pnpm-lock.yaml +117 -117
- package/test/fixtures/lockfiles/yarn.lock +103 -103
- package/test/fixtures/mock-data.js +69 -69
package/backend/siem/cef.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
export function generateCEF(scans) {
|
|
2
|
-
const entries = [];
|
|
3
|
-
for (const s of scans) {
|
|
4
|
-
for (const f of (s.findings || [])) {
|
|
5
|
-
const atkId = f.atk_id || f.id;
|
|
6
|
-
const desc = (f.description || f.title || '').replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
|
|
7
|
-
const sevMap = { critical: 10, high: 8, medium: 5, low: 2 };
|
|
8
|
-
const sev = sevMap[f.severity] || 5;
|
|
9
|
-
const pkgName = (s.package_name || 'unknown').replace(/\|/g, '\\|');
|
|
10
|
-
const pkgVer = (s.version || '').replace(/\|/g, '\\|');
|
|
11
|
-
entries.push([
|
|
12
|
-
'CEF:0',
|
|
13
|
-
'npm-scan',
|
|
14
|
-
'npm-scan',
|
|
15
|
-
process.env.npm_package_version || '0.4.0',
|
|
16
|
-
atkId,
|
|
17
|
-
desc,
|
|
18
|
-
String(sev),
|
|
19
|
-
`suser=${pkgName} ${pkgVer}`,
|
|
20
|
-
`msg=${desc}`,
|
|
21
|
-
`cs1=${atkId}`,
|
|
22
|
-
`cs1Label=atkId`,
|
|
23
|
-
`cs2=${f.severity}`,
|
|
24
|
-
`cs2Label=severity`,
|
|
25
|
-
`cs3=${pkgName}`,
|
|
26
|
-
`cs3Label=package`,
|
|
27
|
-
`cs4=${pkgVer}`,
|
|
28
|
-
`cs4Label=version`,
|
|
29
|
-
].join('|'));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return entries.join('\n');
|
|
1
|
+
export function generateCEF(scans) {
|
|
2
|
+
const entries = [];
|
|
3
|
+
for (const s of scans) {
|
|
4
|
+
for (const f of (s.findings || [])) {
|
|
5
|
+
const atkId = f.atk_id || f.id;
|
|
6
|
+
const desc = (f.description || f.title || '').replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
|
|
7
|
+
const sevMap = { critical: 10, high: 8, medium: 5, low: 2 };
|
|
8
|
+
const sev = sevMap[f.severity] || 5;
|
|
9
|
+
const pkgName = (s.package_name || 'unknown').replace(/\|/g, '\\|');
|
|
10
|
+
const pkgVer = (s.version || '').replace(/\|/g, '\\|');
|
|
11
|
+
entries.push([
|
|
12
|
+
'CEF:0',
|
|
13
|
+
'npm-scan',
|
|
14
|
+
'npm-scan',
|
|
15
|
+
process.env.npm_package_version || '0.4.0',
|
|
16
|
+
atkId,
|
|
17
|
+
desc,
|
|
18
|
+
String(sev),
|
|
19
|
+
`suser=${pkgName} ${pkgVer}`,
|
|
20
|
+
`msg=${desc}`,
|
|
21
|
+
`cs1=${atkId}`,
|
|
22
|
+
`cs1Label=atkId`,
|
|
23
|
+
`cs2=${f.severity}`,
|
|
24
|
+
`cs2Label=severity`,
|
|
25
|
+
`cs3=${pkgName}`,
|
|
26
|
+
`cs3Label=package`,
|
|
27
|
+
`cs4=${pkgVer}`,
|
|
28
|
+
`cs4Label=version`,
|
|
29
|
+
].join('|'));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return entries.join('\n');
|
|
33
33
|
}
|
package/backend/siem/ecs.js
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
export function generateECS(scans) {
|
|
2
|
-
const events = [];
|
|
3
|
-
for (const s of scans) {
|
|
4
|
-
for (const f of (s.findings || [])) {
|
|
5
|
-
const atkId = f.atk_id || f.id;
|
|
6
|
-
const sevMap = { critical: 100, high: 80, medium: 50, low: 20 };
|
|
7
|
-
events.push({
|
|
8
|
-
'@timestamp': new Date().toISOString(),
|
|
9
|
-
event: {
|
|
10
|
-
kind: 'alert',
|
|
11
|
-
category: 'threat',
|
|
12
|
-
type: ['indicator', 'threat'],
|
|
13
|
-
action: 'npm-scan-detected',
|
|
14
|
-
severity: sevMap[f.severity] || 50,
|
|
15
|
-
},
|
|
16
|
-
message: `[${atkId}] ${f.severity.toUpperCase()}: ${f.description || f.title || 'Unknown finding'}`,
|
|
17
|
-
log: { level: f.severity },
|
|
18
|
-
observer: {
|
|
19
|
-
vendor: 'Lateos',
|
|
20
|
-
product: 'npm-scan',
|
|
21
|
-
version: process.env.npm_package_version || '0.7.0',
|
|
22
|
-
},
|
|
23
|
-
labels: {
|
|
24
|
-
package: s.package_name || 'unknown',
|
|
25
|
-
version: s.version || 'unknown',
|
|
26
|
-
atk_id: atkId,
|
|
27
|
-
severity: f.severity,
|
|
28
|
-
},
|
|
29
|
-
vulnerability: {
|
|
30
|
-
classification: 'npm-supply-chain',
|
|
31
|
-
reference: `https://npm-scan.io/atk/${atkId}`,
|
|
32
|
-
id: atkId,
|
|
33
|
-
description: f.description || f.title || null,
|
|
34
|
-
enumeration: 'ATK',
|
|
35
|
-
},
|
|
36
|
-
file: f.evidence ? { name: f.evidence } : undefined,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return events.map(e => JSON.stringify(e)).join('\n');
|
|
1
|
+
export function generateECS(scans) {
|
|
2
|
+
const events = [];
|
|
3
|
+
for (const s of scans) {
|
|
4
|
+
for (const f of (s.findings || [])) {
|
|
5
|
+
const atkId = f.atk_id || f.id;
|
|
6
|
+
const sevMap = { critical: 100, high: 80, medium: 50, low: 20 };
|
|
7
|
+
events.push({
|
|
8
|
+
'@timestamp': new Date().toISOString(),
|
|
9
|
+
event: {
|
|
10
|
+
kind: 'alert',
|
|
11
|
+
category: 'threat',
|
|
12
|
+
type: ['indicator', 'threat'],
|
|
13
|
+
action: 'npm-scan-detected',
|
|
14
|
+
severity: sevMap[f.severity] || 50,
|
|
15
|
+
},
|
|
16
|
+
message: `[${atkId}] ${f.severity.toUpperCase()}: ${f.description || f.title || 'Unknown finding'}`,
|
|
17
|
+
log: { level: f.severity },
|
|
18
|
+
observer: {
|
|
19
|
+
vendor: 'Lateos',
|
|
20
|
+
product: 'npm-scan',
|
|
21
|
+
version: process.env.npm_package_version || '0.7.0',
|
|
22
|
+
},
|
|
23
|
+
labels: {
|
|
24
|
+
package: s.package_name || 'unknown',
|
|
25
|
+
version: s.version || 'unknown',
|
|
26
|
+
atk_id: atkId,
|
|
27
|
+
severity: f.severity,
|
|
28
|
+
},
|
|
29
|
+
vulnerability: {
|
|
30
|
+
classification: 'npm-supply-chain',
|
|
31
|
+
reference: `https://npm-scan.io/atk/${atkId}`,
|
|
32
|
+
id: atkId,
|
|
33
|
+
description: f.description || f.title || null,
|
|
34
|
+
enumeration: 'ATK',
|
|
35
|
+
},
|
|
36
|
+
file: f.evidence ? { name: f.evidence } : undefined,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return events.map(e => JSON.stringify(e)).join('\n');
|
|
41
41
|
}
|
package/backend/siem/index.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { generateCEF } from './cef.js';
|
|
2
|
-
import { generateECS } from './ecs.js';
|
|
3
|
-
import { generateSentinel } from './sentinel.js';
|
|
4
|
-
import { generateQRadar } from './qradar.js';
|
|
5
|
-
|
|
6
|
-
export function generateSIEM(scans, format = 'cef') {
|
|
7
|
-
switch (format) {
|
|
8
|
-
case 'cef':
|
|
9
|
-
return generateCEF(scans);
|
|
10
|
-
case 'ecs':
|
|
11
|
-
return generateECS(scans);
|
|
12
|
-
case 'sentinel':
|
|
13
|
-
return generateSentinel(scans);
|
|
14
|
-
case 'qradar':
|
|
15
|
-
return generateQRadar(scans);
|
|
16
|
-
default:
|
|
17
|
-
throw new Error(`Unknown SIEM format: ${format}. Supported: cef, ecs, sentinel, qradar`);
|
|
18
|
-
}
|
|
1
|
+
import { generateCEF } from './cef.js';
|
|
2
|
+
import { generateECS } from './ecs.js';
|
|
3
|
+
import { generateSentinel } from './sentinel.js';
|
|
4
|
+
import { generateQRadar } from './qradar.js';
|
|
5
|
+
|
|
6
|
+
export function generateSIEM(scans, format = 'cef') {
|
|
7
|
+
switch (format) {
|
|
8
|
+
case 'cef':
|
|
9
|
+
return generateCEF(scans);
|
|
10
|
+
case 'ecs':
|
|
11
|
+
return generateECS(scans);
|
|
12
|
+
case 'sentinel':
|
|
13
|
+
return generateSentinel(scans);
|
|
14
|
+
case 'qradar':
|
|
15
|
+
return generateQRadar(scans);
|
|
16
|
+
default:
|
|
17
|
+
throw new Error(`Unknown SIEM format: ${format}. Supported: cef, ecs, sentinel, qradar`);
|
|
18
|
+
}
|
|
19
19
|
}
|
package/backend/siem/qradar.js
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
export function generateQRadar(scans) {
|
|
2
|
-
const events = [];
|
|
3
|
-
for (const s of scans) {
|
|
4
|
-
for (const f of (s.findings || [])) {
|
|
5
|
-
const atkId = f.atk_id || f.id;
|
|
6
|
-
events.push({
|
|
7
|
-
source: 'npm-scan',
|
|
8
|
-
version: process.env.npm_package_version || '0.7.0',
|
|
9
|
-
devicetime: new Date().toISOString(),
|
|
10
|
-
devicepayload: [
|
|
11
|
-
s.package_name || 'unknown',
|
|
12
|
-
s.version || 'unknown',
|
|
13
|
-
atkId,
|
|
14
|
-
f.severity,
|
|
15
|
-
f.title || f.description || '',
|
|
16
|
-
f.evidence || '',
|
|
17
|
-
].join('\t'),
|
|
18
|
-
devicevendor: 'Lateos',
|
|
19
|
-
devicename: 'npm-scan',
|
|
20
|
-
deviceproduct: 'npm-scan',
|
|
21
|
-
atk_id: atkId,
|
|
22
|
-
severity: f.severity,
|
|
23
|
-
package_name: s.package_name || 'unknown',
|
|
24
|
-
package_version: s.version || 'unknown',
|
|
25
|
-
finding_title: f.title || f.description || '',
|
|
26
|
-
finding_description: f.description || f.title || '',
|
|
27
|
-
evidence: f.evidence || '',
|
|
28
|
-
mitigation: f.mitigation || '',
|
|
29
|
-
raw_category: 'NPM Supply Chain Threat',
|
|
30
|
-
qid: _qrQid(f.severity),
|
|
31
|
-
category: _qrCategory(f.severity),
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return events.map(e => JSON.stringify(e)).join('\n');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const QID_MAP = {
|
|
39
|
-
critical: 90050001,
|
|
40
|
-
high: 90050002,
|
|
41
|
-
medium: 90050003,
|
|
42
|
-
low: 90050004,
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
function _qrQid(severity) {
|
|
46
|
-
return QID_MAP[severity] || 90050003;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function _qrCategory(severity) {
|
|
50
|
-
const map = {
|
|
51
|
-
critical: 'Critical Severity Malware',
|
|
52
|
-
high: 'High Severity Malware',
|
|
53
|
-
medium: 'Medium Severity Malware',
|
|
54
|
-
low: 'Low Severity Malware',
|
|
55
|
-
};
|
|
56
|
-
return map[severity] || 'Medium Severity Malware';
|
|
1
|
+
export function generateQRadar(scans) {
|
|
2
|
+
const events = [];
|
|
3
|
+
for (const s of scans) {
|
|
4
|
+
for (const f of (s.findings || [])) {
|
|
5
|
+
const atkId = f.atk_id || f.id;
|
|
6
|
+
events.push({
|
|
7
|
+
source: 'npm-scan',
|
|
8
|
+
version: process.env.npm_package_version || '0.7.0',
|
|
9
|
+
devicetime: new Date().toISOString(),
|
|
10
|
+
devicepayload: [
|
|
11
|
+
s.package_name || 'unknown',
|
|
12
|
+
s.version || 'unknown',
|
|
13
|
+
atkId,
|
|
14
|
+
f.severity,
|
|
15
|
+
f.title || f.description || '',
|
|
16
|
+
f.evidence || '',
|
|
17
|
+
].join('\t'),
|
|
18
|
+
devicevendor: 'Lateos',
|
|
19
|
+
devicename: 'npm-scan',
|
|
20
|
+
deviceproduct: 'npm-scan',
|
|
21
|
+
atk_id: atkId,
|
|
22
|
+
severity: f.severity,
|
|
23
|
+
package_name: s.package_name || 'unknown',
|
|
24
|
+
package_version: s.version || 'unknown',
|
|
25
|
+
finding_title: f.title || f.description || '',
|
|
26
|
+
finding_description: f.description || f.title || '',
|
|
27
|
+
evidence: f.evidence || '',
|
|
28
|
+
mitigation: f.mitigation || '',
|
|
29
|
+
raw_category: 'NPM Supply Chain Threat',
|
|
30
|
+
qid: _qrQid(f.severity),
|
|
31
|
+
category: _qrCategory(f.severity),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return events.map(e => JSON.stringify(e)).join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const QID_MAP = {
|
|
39
|
+
critical: 90050001,
|
|
40
|
+
high: 90050002,
|
|
41
|
+
medium: 90050003,
|
|
42
|
+
low: 90050004,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function _qrQid(severity) {
|
|
46
|
+
return QID_MAP[severity] || 90050003;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function _qrCategory(severity) {
|
|
50
|
+
const map = {
|
|
51
|
+
critical: 'Critical Severity Malware',
|
|
52
|
+
high: 'High Severity Malware',
|
|
53
|
+
medium: 'Medium Severity Malware',
|
|
54
|
+
low: 'Low Severity Malware',
|
|
55
|
+
};
|
|
56
|
+
return map[severity] || 'Medium Severity Malware';
|
|
57
57
|
}
|
package/backend/siem/sentinel.js
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
export function generateSentinel(scans) {
|
|
2
|
-
const events = [];
|
|
3
|
-
for (const s of scans) {
|
|
4
|
-
for (const f of (s.findings || [])) {
|
|
5
|
-
const atkId = f.atk_id || f.id;
|
|
6
|
-
events.push({
|
|
7
|
-
TimeGenerated: new Date().toISOString(),
|
|
8
|
-
Computer: process.env.COMPUTERNAME || process.env.HOSTNAME || 'npm-scan-host',
|
|
9
|
-
SourceSystem: 'npm-scan',
|
|
10
|
-
DeviceVendor: 'Lateos',
|
|
11
|
-
DeviceProduct: 'npm-scan',
|
|
12
|
-
DeviceVersion: process.env.npm_package_version || '0.7.0',
|
|
13
|
-
SeverityLevel: f.severity,
|
|
14
|
-
Severity: f.severity.toUpperCase(),
|
|
15
|
-
EventType: 'npm-supply-chain-threat',
|
|
16
|
-
ATKId: atkId,
|
|
17
|
-
FindingTitle: f.title || f.description || '',
|
|
18
|
-
FindingDescription: f.description || f.title || '',
|
|
19
|
-
Evidence: f.evidence || '',
|
|
20
|
-
PackageName: s.package_name || 'unknown',
|
|
21
|
-
PackageVersion: s.version || 'unknown',
|
|
22
|
-
Mitigation: f.mitigation || '',
|
|
23
|
-
ThreatClassification: 'npm-supply-chain',
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return JSON.stringify(events, null, 2);
|
|
1
|
+
export function generateSentinel(scans) {
|
|
2
|
+
const events = [];
|
|
3
|
+
for (const s of scans) {
|
|
4
|
+
for (const f of (s.findings || [])) {
|
|
5
|
+
const atkId = f.atk_id || f.id;
|
|
6
|
+
events.push({
|
|
7
|
+
TimeGenerated: new Date().toISOString(),
|
|
8
|
+
Computer: process.env.COMPUTERNAME || process.env.HOSTNAME || 'npm-scan-host',
|
|
9
|
+
SourceSystem: 'npm-scan',
|
|
10
|
+
DeviceVendor: 'Lateos',
|
|
11
|
+
DeviceProduct: 'npm-scan',
|
|
12
|
+
DeviceVersion: process.env.npm_package_version || '0.7.0',
|
|
13
|
+
SeverityLevel: f.severity,
|
|
14
|
+
Severity: f.severity.toUpperCase(),
|
|
15
|
+
EventType: 'npm-supply-chain-threat',
|
|
16
|
+
ATKId: atkId,
|
|
17
|
+
FindingTitle: f.title || f.description || '',
|
|
18
|
+
FindingDescription: f.description || f.title || '',
|
|
19
|
+
Evidence: f.evidence || '',
|
|
20
|
+
PackageName: s.package_name || 'unknown',
|
|
21
|
+
PackageVersion: s.version || 'unknown',
|
|
22
|
+
Mitigation: f.mitigation || '',
|
|
23
|
+
ThreatClassification: 'npm-supply-chain',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return JSON.stringify(events, null, 2);
|
|
28
28
|
}
|
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
const ACTIVATION_RISK_MATRIX = {
|
|
2
|
-
'*': { base: 'critical', label: 'Wildcard (all files)' },
|
|
3
|
-
'onStartupFinished': { base: 'high', label: 'Startup finished' },
|
|
4
|
-
'workspaceContains:**/*': { base: 'high', label: 'Workspace contains wildcard' },
|
|
5
|
-
'workspaceContains': { base: 'high', label: 'Workspace contains' },
|
|
6
|
-
'onCommand:*': { base: 'low', label: 'Any command' },
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
const DEFAULT_BASE_RISK = 'medium';
|
|
10
|
-
|
|
11
|
-
const ESCALATION_KEYWORDS = [
|
|
12
|
-
'npx', 'bun', 'curl', 'wget', 'fetch(',
|
|
13
|
-
'exec(', 'spawn(', 'execSync', 'spawnSync',
|
|
14
|
-
'child_process', 'shell: true', 'detached: true',
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
const BUNDLED_BUN_PATTERN = /bun|runtime/;
|
|
18
|
-
|
|
19
|
-
const SIZE_DELTA_THRESHOLD = 400 * 1024;
|
|
20
|
-
|
|
21
|
-
const SHELL_CMDS = ['npx', 'bun', 'curl', 'wget', 'exec', 'spawn', 'execSync'];
|
|
22
|
-
|
|
23
|
-
export async function checkActivationEventRisk(extensionManifest, versionHistory = [], priorVersions = []) {
|
|
24
|
-
const signals = [];
|
|
25
|
-
|
|
26
|
-
const activationEvents = extensionManifest?.activationEvents || [];
|
|
27
|
-
if (activationEvents.length === 0 && extensionManifest?.main) {
|
|
28
|
-
return { triggered: false, signals: [], riskLevel: null, why: [] };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let maxBaseRisk = 0;
|
|
32
|
-
const riskLabels = ['none', 'low', 'medium', 'high', 'critical'];
|
|
33
|
-
const riskValues = { none: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
34
|
-
|
|
35
|
-
let worstEvent = null;
|
|
36
|
-
const why = [];
|
|
37
|
-
|
|
38
|
-
for (const event of activationEvents) {
|
|
39
|
-
const risk = ACTIVATION_RISK_MATRIX[event];
|
|
40
|
-
if (risk) {
|
|
41
|
-
const baseIdx = riskValues[risk.base] || riskValues[DEFAULT_BASE_RISK];
|
|
42
|
-
if (baseIdx > maxBaseRisk) {
|
|
43
|
-
maxBaseRisk = baseIdx;
|
|
44
|
-
worstEvent = event;
|
|
45
|
-
}
|
|
46
|
-
} else if (event.includes('*') && event !== 'onCommand:*') {
|
|
47
|
-
const baseIdx = riskValues['high'];
|
|
48
|
-
if (baseIdx > maxBaseRisk) {
|
|
49
|
-
maxBaseRisk = baseIdx;
|
|
50
|
-
worstEvent = event;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const contributes = extensionManifest?.contributes || {};
|
|
56
|
-
const commands = contributes?.commands || [];
|
|
57
|
-
const cmdTitles = commands.map(c => (c.title || '').toLowerCase()).join(' ');
|
|
58
|
-
|
|
59
|
-
const bundledDeps = extensionManifest?.bundledDependencies || [];
|
|
60
|
-
const bundledStr = Array.isArray(bundledDeps) ? bundledDeps.join(' ') : '';
|
|
61
|
-
|
|
62
|
-
const hasShellKeyword = SHELL_CMDS.some(cmd => cmdTitles.includes(cmd));
|
|
63
|
-
const hasBunBundled = BUNDLED_BUN_PATTERN.test(bundledStr);
|
|
64
|
-
|
|
65
|
-
const activationEventsStr = activationEvents.join(' ');
|
|
66
|
-
const hasShellInActivationContext = ESCALATION_KEYWORDS.some(kw => activationEventsStr.toLowerCase().includes(kw.toLowerCase()));
|
|
67
|
-
|
|
68
|
-
let escalateToCritical = false;
|
|
69
|
-
|
|
70
|
-
if (hasShellKeyword || hasBunBundled || hasShellInActivationContext) {
|
|
71
|
-
escalateToCritical = true;
|
|
72
|
-
why.push('HIGH activation event + shell/execution keywords');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (versionHistory.length >= 2) {
|
|
76
|
-
const sizes = versionHistory
|
|
77
|
-
.filter(v => v.assetSize)
|
|
78
|
-
.map(v => v.assetSize)
|
|
79
|
-
.sort((a, b) => b - a);
|
|
80
|
-
|
|
81
|
-
if (sizes.length >= 2 && (sizes[0] - sizes[sizes.length - 1]) > SIZE_DELTA_THRESHOLD) {
|
|
82
|
-
escalateToCritical = true;
|
|
83
|
-
why.push(`HIGH activation event + version size delta > ${SIZE_DELTA_THRESHOLD} bytes`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const priorActivationEvents = priorVersions
|
|
88
|
-
.filter(v => v.activationEvents)
|
|
89
|
-
.flatMap(v => v.activationEvents);
|
|
90
|
-
|
|
91
|
-
if (priorActivationEvents.length > 0) {
|
|
92
|
-
const newEvents = activationEvents.filter(e => !priorActivationEvents.includes(e));
|
|
93
|
-
if (newEvents.length > 0) {
|
|
94
|
-
why.push(`First-time activation event(s) added: ${newEvents.join(', ')}`);
|
|
95
|
-
if (!escalateToCritical && maxBaseRisk >= riskValues['high']) {
|
|
96
|
-
escalateToCritical = true;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
let riskLevel = maxBaseRisk > 0 ? riskLabels[maxBaseRisk] : null;
|
|
102
|
-
if (escalateToCritical && riskValues[riskLevel] <= riskValues['high']) {
|
|
103
|
-
riskLevel = 'critical';
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (!riskLevel) return { triggered: false, signals: [], riskLevel: null, why: [] };
|
|
107
|
-
|
|
108
|
-
signals.push({
|
|
109
|
-
type: 'ACTIVATION_EVENT_RISK',
|
|
110
|
-
activationEvents,
|
|
111
|
-
riskLevel,
|
|
112
|
-
why,
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
return { triggered: true, signals, riskLevel, why };
|
|
116
|
-
}
|
|
1
|
+
const ACTIVATION_RISK_MATRIX = {
|
|
2
|
+
'*': { base: 'critical', label: 'Wildcard (all files)' },
|
|
3
|
+
'onStartupFinished': { base: 'high', label: 'Startup finished' },
|
|
4
|
+
'workspaceContains:**/*': { base: 'high', label: 'Workspace contains wildcard' },
|
|
5
|
+
'workspaceContains': { base: 'high', label: 'Workspace contains' },
|
|
6
|
+
'onCommand:*': { base: 'low', label: 'Any command' },
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const DEFAULT_BASE_RISK = 'medium';
|
|
10
|
+
|
|
11
|
+
const ESCALATION_KEYWORDS = [
|
|
12
|
+
'npx', 'bun', 'curl', 'wget', 'fetch(',
|
|
13
|
+
'exec(', 'spawn(', 'execSync', 'spawnSync',
|
|
14
|
+
'child_process', 'shell: true', 'detached: true',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const BUNDLED_BUN_PATTERN = /bun|runtime/;
|
|
18
|
+
|
|
19
|
+
const SIZE_DELTA_THRESHOLD = 400 * 1024;
|
|
20
|
+
|
|
21
|
+
const SHELL_CMDS = ['npx', 'bun', 'curl', 'wget', 'exec', 'spawn', 'execSync'];
|
|
22
|
+
|
|
23
|
+
export async function checkActivationEventRisk(extensionManifest, versionHistory = [], priorVersions = []) {
|
|
24
|
+
const signals = [];
|
|
25
|
+
|
|
26
|
+
const activationEvents = extensionManifest?.activationEvents || [];
|
|
27
|
+
if (activationEvents.length === 0 && extensionManifest?.main) {
|
|
28
|
+
return { triggered: false, signals: [], riskLevel: null, why: [] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let maxBaseRisk = 0;
|
|
32
|
+
const riskLabels = ['none', 'low', 'medium', 'high', 'critical'];
|
|
33
|
+
const riskValues = { none: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
34
|
+
|
|
35
|
+
let worstEvent = null;
|
|
36
|
+
const why = [];
|
|
37
|
+
|
|
38
|
+
for (const event of activationEvents) {
|
|
39
|
+
const risk = ACTIVATION_RISK_MATRIX[event];
|
|
40
|
+
if (risk) {
|
|
41
|
+
const baseIdx = riskValues[risk.base] || riskValues[DEFAULT_BASE_RISK];
|
|
42
|
+
if (baseIdx > maxBaseRisk) {
|
|
43
|
+
maxBaseRisk = baseIdx;
|
|
44
|
+
worstEvent = event;
|
|
45
|
+
}
|
|
46
|
+
} else if (event.includes('*') && event !== 'onCommand:*') {
|
|
47
|
+
const baseIdx = riskValues['high'];
|
|
48
|
+
if (baseIdx > maxBaseRisk) {
|
|
49
|
+
maxBaseRisk = baseIdx;
|
|
50
|
+
worstEvent = event;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const contributes = extensionManifest?.contributes || {};
|
|
56
|
+
const commands = contributes?.commands || [];
|
|
57
|
+
const cmdTitles = commands.map(c => (c.title || '').toLowerCase()).join(' ');
|
|
58
|
+
|
|
59
|
+
const bundledDeps = extensionManifest?.bundledDependencies || [];
|
|
60
|
+
const bundledStr = Array.isArray(bundledDeps) ? bundledDeps.join(' ') : '';
|
|
61
|
+
|
|
62
|
+
const hasShellKeyword = SHELL_CMDS.some(cmd => cmdTitles.includes(cmd));
|
|
63
|
+
const hasBunBundled = BUNDLED_BUN_PATTERN.test(bundledStr);
|
|
64
|
+
|
|
65
|
+
const activationEventsStr = activationEvents.join(' ');
|
|
66
|
+
const hasShellInActivationContext = ESCALATION_KEYWORDS.some(kw => activationEventsStr.toLowerCase().includes(kw.toLowerCase()));
|
|
67
|
+
|
|
68
|
+
let escalateToCritical = false;
|
|
69
|
+
|
|
70
|
+
if (hasShellKeyword || hasBunBundled || hasShellInActivationContext) {
|
|
71
|
+
escalateToCritical = true;
|
|
72
|
+
why.push('HIGH activation event + shell/execution keywords');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (versionHistory.length >= 2) {
|
|
76
|
+
const sizes = versionHistory
|
|
77
|
+
.filter(v => v.assetSize)
|
|
78
|
+
.map(v => v.assetSize)
|
|
79
|
+
.sort((a, b) => b - a);
|
|
80
|
+
|
|
81
|
+
if (sizes.length >= 2 && (sizes[0] - sizes[sizes.length - 1]) > SIZE_DELTA_THRESHOLD) {
|
|
82
|
+
escalateToCritical = true;
|
|
83
|
+
why.push(`HIGH activation event + version size delta > ${SIZE_DELTA_THRESHOLD} bytes`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const priorActivationEvents = priorVersions
|
|
88
|
+
.filter(v => v.activationEvents)
|
|
89
|
+
.flatMap(v => v.activationEvents);
|
|
90
|
+
|
|
91
|
+
if (priorActivationEvents.length > 0) {
|
|
92
|
+
const newEvents = activationEvents.filter(e => !priorActivationEvents.includes(e));
|
|
93
|
+
if (newEvents.length > 0) {
|
|
94
|
+
why.push(`First-time activation event(s) added: ${newEvents.join(', ')}`);
|
|
95
|
+
if (!escalateToCritical && maxBaseRisk >= riskValues['high']) {
|
|
96
|
+
escalateToCritical = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let riskLevel = maxBaseRisk > 0 ? riskLabels[maxBaseRisk] : null;
|
|
102
|
+
if (escalateToCritical && riskValues[riskLevel] <= riskValues['high']) {
|
|
103
|
+
riskLevel = 'critical';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!riskLevel) return { triggered: false, signals: [], riskLevel: null, why: [] };
|
|
107
|
+
|
|
108
|
+
signals.push({
|
|
109
|
+
type: 'ACTIVATION_EVENT_RISK',
|
|
110
|
+
activationEvents,
|
|
111
|
+
riskLevel,
|
|
112
|
+
why,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return { triggered: true, signals, riskLevel, why };
|
|
116
|
+
}
|