@lateos/npm-scan 0.18.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +864 -826
- package/VALIDATION.md +92 -0
- package/backend/cra.js +113 -21
- package/backend/db/pg-schema.sql +155 -0
- package/backend/db.js +18 -10
- package/backend/detectors/atk-001-lifecycle.js +5 -5
- package/backend/detectors/atk-002-obfusc.js +126 -47
- package/backend/detectors/atk-003-creds.js +8 -4
- package/backend/detectors/atk-004-persist.js +3 -3
- package/backend/detectors/atk-005-exfil.js +8 -4
- package/backend/detectors/atk-006-depconf.js +3 -3
- package/backend/detectors/atk-007-typosquat.js +64 -10
- package/backend/detectors/atk-008-tarball-tamper.js +6 -6
- package/backend/detectors/atk-009-dormant-trigger.js +9 -5
- package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
- package/backend/detectors/atk-011-transitive-prop.js +14 -13
- package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
- package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
- package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
- package/backend/detectors/axios-poisoning/index.js +77 -60
- package/backend/detectors/config/thresholds.js +111 -0
- package/backend/detectors/config/whitelist.json +74 -0
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
- package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
- package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
- package/backend/detectors/hf-impersonation/index.js +94 -31
- package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
- package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
- package/backend/detectors/hf-impersonation/simhash.js +2 -2
- package/backend/detectors/index.js +184 -31
- package/backend/detectors/lib/ast-patterns.js +24 -0
- package/backend/detectors/lib/entropy-analyzer.js +32 -0
- package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
- package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
- package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
- package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
- package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
- package/backend/detectors/megalodon/index.js +35 -25
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
- package/backend/detectors/mini-shai-hulud/index.js +63 -26
- package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
- package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
- package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
- package/backend/detectors/msh-supplement/index.js +78 -63
- package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
- package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
- package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
- package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
- package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
- package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
- package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
- package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
- package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
- package/backend/detectors/node-ipc-compromise/index.js +21 -15
- package/backend/detectors/tier1-binary-embed.js +138 -41
- package/backend/detectors/tier1-cloud-imds.js +57 -37
- package/backend/detectors/tier1-encrypted-c2.js +198 -0
- package/backend/detectors/tier1-infostealer.js +121 -68
- package/backend/detectors/tier1-lifecycle-hook.js +63 -23
- package/backend/detectors/tier1-maintainer-compromise.js +157 -0
- package/backend/detectors/tier1-metadata-spoof.js +92 -42
- package/backend/detectors/tier1-multistage-postinstall.js +46 -19
- package/backend/detectors/tier1-obfuscation-heuristics.js +184 -0
- package/backend/detectors/tier1-self-propagation.js +115 -0
- package/backend/detectors/tier1-slsa-attestation.js +12 -0
- package/backend/detectors/tier1-transitive-deps.js +182 -0
- package/backend/detectors/tier1-typosquat.js +129 -50
- package/backend/detectors/tier1-version-anomaly.js +223 -0
- package/backend/detectors/tier1-version-confusion.js +79 -59
- package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
- package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
- package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
- package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
- package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
- package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
- package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
- package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
- package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
- package/backend/detectors/trapdoor/index.js +19 -14
- package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
- package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
- package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
- package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
- package/backend/detectors.test.js +147 -0
- package/backend/fetch.js +37 -29
- package/backend/index.js +1 -1
- package/backend/license.js +20 -4
- package/backend/lockfile.js +60 -36
- package/backend/pdf.js +107 -28
- package/backend/policy.js +183 -56
- package/backend/provenance.js +28 -3
- package/backend/report.js +136 -70
- package/backend/sbom.js +33 -27
- package/backend/scripts/analyze-false-positives.js +152 -0
- package/backend/scripts/analyze-validation.js +157 -0
- package/backend/scripts/detect-false-positives.js +103 -0
- package/backend/scripts/fetch-top-packages.js +277 -0
- package/backend/scripts/validate-d10-d13.js +103 -0
- package/backend/scripts/validate-detectors.js +151 -0
- package/backend/siem/cef.js +23 -21
- package/backend/siem/ecs.js +3 -3
- package/backend/siem/index.js +1 -1
- package/backend/siem/qradar.js +3 -3
- package/backend/siem/sentinel.js +2 -2
- package/backend/tests-d5-enhanced.test.js +47 -0
- package/backend/tests-d6-version-anomaly.test.js +67 -0
- package/backend/tests-d6.test.js +126 -0
- package/backend/tests-d6c.test.js +119 -0
- package/backend/tests-d7-obfuscation.test.js +88 -0
- package/backend/tests.test.js +997 -0
- package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
- package/backend/vsix-scan/detectors/burst-publish.js +14 -7
- package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
- package/backend/vsix-scan/detectors/known-ioc.js +23 -8
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
- package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
- package/backend/vsix-scan/index.js +97 -41
- package/backend/vsix-scan/marketplace-client.js +29 -13
- package/cli/cli.js +154 -64
- package/package.json +36 -10
- package/.dockerignore +0 -20
- package/.husky/pre-commit +0 -1
- package/SECURITY.md +0 -73
- package/deploy/helm/npm-scan/Chart.yaml +0 -22
- package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
- package/deploy/helm/npm-scan/templates/api.yaml +0 -94
- package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
- package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
- package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
- package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
- package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
- package/deploy/helm/npm-scan/values.yaml +0 -103
- package/scripts/download-corpus.js +0 -30
- package/scripts/gen-mal-corpus.js +0 -35
- package/scripts/generate-campaign-fixtures.js +0 -170
- package/src/config/top-5000.json +0 -87
- package/test/fixtures/lockfiles/npm-lock.json +0 -69
- package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
- package/test/fixtures/lockfiles/yarn.lock +0 -104
- package/test/fixtures/mock-data.js +0 -69
|
@@ -7,7 +7,7 @@ export function scanCjsPayloadInjection(allFiles) {
|
|
|
7
7
|
let cjsContent = null;
|
|
8
8
|
let mjsContent = null;
|
|
9
9
|
let cjsPath = null;
|
|
10
|
-
let
|
|
10
|
+
let _mjsPath = null;
|
|
11
11
|
|
|
12
12
|
for (const file of allFiles) {
|
|
13
13
|
const path = file.path?.replace(/\\/g, '/') || '';
|
|
@@ -17,7 +17,7 @@ export function scanCjsPayloadInjection(allFiles) {
|
|
|
17
17
|
}
|
|
18
18
|
if (path.endsWith('node-ipc.mjs')) {
|
|
19
19
|
mjsContent = file.content || '';
|
|
20
|
-
|
|
20
|
+
_mjsPath = path;
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -52,19 +52,21 @@ export function scanCjsPayloadInjection(allFiles) {
|
|
|
52
52
|
matches.push({
|
|
53
53
|
file: cjsPath,
|
|
54
54
|
finding: 'iife-suffix',
|
|
55
|
-
detail:
|
|
55
|
+
detail:
|
|
56
|
+
'node-ipc.cjs ends with IIFE pattern — potential obfuscated payload appended after module closure',
|
|
56
57
|
});
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
if (cjsContent && IIFE_END_PATTERN.test(cjsContent.trim())) {
|
|
62
|
-
const alreadyReported = matches.some(m => m.finding === 'iife-suffix');
|
|
63
|
+
const alreadyReported = matches.some((m) => m.finding === 'iife-suffix');
|
|
63
64
|
if (!alreadyReported) {
|
|
64
65
|
matches.push({
|
|
65
66
|
file: cjsPath,
|
|
66
67
|
finding: 'iife-suffix',
|
|
67
|
-
detail:
|
|
68
|
+
detail:
|
|
69
|
+
'node-ipc.cjs ends with IIFE pattern — potential obfuscated payload appended after module closure',
|
|
68
70
|
});
|
|
69
71
|
}
|
|
70
72
|
}
|
|
@@ -2,14 +2,16 @@ import { createHash } from 'crypto';
|
|
|
2
2
|
|
|
3
3
|
const INJECTED_PAYLOAD_HASH = '3427a90c8cb9af764445448648176e120ebc6af0a538158340cf6220de4d01b7';
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const _IIFE_BOUNDARY = /}\)\(\);\s*$/;
|
|
6
6
|
|
|
7
7
|
export function scanInjectedPayloadHash(allFiles) {
|
|
8
8
|
const matches = [];
|
|
9
9
|
|
|
10
10
|
for (const file of allFiles) {
|
|
11
11
|
const path = file.path?.replace(/\\/g, '/') || '';
|
|
12
|
-
if (!path.endsWith('node-ipc.cjs'))
|
|
12
|
+
if (!path.endsWith('node-ipc.cjs')) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
13
15
|
|
|
14
16
|
const content = file.content || '';
|
|
15
17
|
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
const PUBLIC_RESOLVERS = new Set([
|
|
2
|
-
'1.1.1.1',
|
|
3
|
-
'8.8.8.8',
|
|
4
|
-
'8.8.4.4',
|
|
5
|
-
'9.9.9.9',
|
|
6
|
-
]);
|
|
1
|
+
const PUBLIC_RESOLVERS = new Set(['1.1.1.1', '8.8.8.8', '8.8.4.4', '9.9.9.9']);
|
|
7
2
|
|
|
8
3
|
const IP_PATTERN = /setServers\(\s*\[?\s*['"`](\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})['"`]/;
|
|
9
4
|
|
|
@@ -21,19 +16,27 @@ export function scanDnsC2Pattern(allFiles, pkgJson) {
|
|
|
21
16
|
|
|
22
17
|
for (const file of allFiles) {
|
|
23
18
|
const path = file.path || '';
|
|
24
|
-
if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs'))
|
|
19
|
+
if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs')) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
25
22
|
sources.push({ file: path, content: file.content || '' });
|
|
26
23
|
}
|
|
27
24
|
|
|
28
25
|
for (const { file, content } of sources) {
|
|
29
26
|
const hasDnsResolver = /\bdns\.promises\s*\.\s*Resolver\b/.test(content);
|
|
30
|
-
if (!hasDnsResolver)
|
|
27
|
+
if (!hasDnsResolver) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
31
30
|
|
|
32
31
|
const ipMatch = content.match(IP_PATTERN);
|
|
33
|
-
if (!ipMatch)
|
|
32
|
+
if (!ipMatch) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
34
35
|
|
|
35
36
|
const customIP = ipMatch[1];
|
|
36
|
-
if (PUBLIC_RESOLVERS.has(customIP))
|
|
37
|
+
if (PUBLIC_RESOLVERS.has(customIP)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
37
40
|
|
|
38
41
|
const hasResolveTxt = /\bresolveTxt\b/.test(content);
|
|
39
42
|
|
|
@@ -15,7 +15,9 @@ export function scanDnsTxtExfil(allFiles, pkgJson) {
|
|
|
15
15
|
|
|
16
16
|
for (const file of allFiles) {
|
|
17
17
|
const path = file.path || '';
|
|
18
|
-
if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs'))
|
|
18
|
+
if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs')) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
19
21
|
sources.push({ file: path, content: file.content || '' });
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -10,7 +10,9 @@ export function scanRuntimeTrigger(allFiles, pkgJson) {
|
|
|
10
10
|
|
|
11
11
|
for (const file of allFiles) {
|
|
12
12
|
const path = file.path || '';
|
|
13
|
-
if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs'))
|
|
13
|
+
if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs')) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
14
16
|
sources.push({ file: path, content: file.content || '' });
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -18,7 +20,8 @@ export function scanRuntimeTrigger(allFiles, pkgJson) {
|
|
|
18
20
|
if (/\bsetImmediate\s*\(/.test(content)) {
|
|
19
21
|
matches.push({
|
|
20
22
|
file,
|
|
21
|
-
detail:
|
|
23
|
+
detail:
|
|
24
|
+
'setImmediate() call found — node-ipc malware fires at require() time, not via postinstall',
|
|
22
25
|
});
|
|
23
26
|
}
|
|
24
27
|
}
|
|
@@ -28,7 +28,9 @@ const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low', 'info', 'none'];
|
|
|
28
28
|
|
|
29
29
|
function highestSeverity(severities) {
|
|
30
30
|
for (const s of SEVERITY_ORDER) {
|
|
31
|
-
if (severities.includes(s))
|
|
31
|
+
if (severities.includes(s)) {
|
|
32
|
+
return s;
|
|
33
|
+
}
|
|
32
34
|
}
|
|
33
35
|
return 'none';
|
|
34
36
|
}
|
|
@@ -42,7 +44,9 @@ function buildRemediation(triggered) {
|
|
|
42
44
|
lines.push('PRESERVE ~/nt-*/ artifacts for incident response');
|
|
43
45
|
}
|
|
44
46
|
if (triggered.includes('D5') || triggered.includes('D6') || triggered.includes('D7')) {
|
|
45
|
-
lines.push(
|
|
47
|
+
lines.push(
|
|
48
|
+
'Review DNS egress logs for sh.azurestaticprovider.net and 37.16.75.69 post May 14, 2026'
|
|
49
|
+
);
|
|
46
50
|
}
|
|
47
51
|
lines.push('Rotate all CI/CD secrets and OIDC tokens');
|
|
48
52
|
lines.push('Audit maintainer email domain expiry for all critical dependencies');
|
|
@@ -70,24 +74,26 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
|
|
|
70
74
|
.filter(([_, r]) => r.triggered)
|
|
71
75
|
.map(([id]) => id);
|
|
72
76
|
|
|
73
|
-
if (triggered.length === 0)
|
|
77
|
+
if (triggered.length === 0) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
74
80
|
|
|
75
|
-
const severity = highestSeverity(triggered.map(id => RULE_SEVERITY[id]));
|
|
81
|
+
const severity = highestSeverity(triggered.map((id) => RULE_SEVERITY[id]));
|
|
76
82
|
|
|
77
83
|
const evidence = {
|
|
78
84
|
campaign: 'NODE_IPC_COMPROMISE',
|
|
79
85
|
triggeredRules: triggered,
|
|
80
|
-
details: Object.fromEntries(
|
|
81
|
-
Object.entries(results).filter(([_, r]) => r.triggered)
|
|
82
|
-
),
|
|
86
|
+
details: Object.fromEntries(Object.entries(results).filter(([_, r]) => r.triggered)),
|
|
83
87
|
};
|
|
84
88
|
|
|
85
|
-
return [
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
id: 'NODE_IPC_COMPROMISE',
|
|
92
|
+
severity,
|
|
93
|
+
title: 'node-ipc supply chain compromise (May 14, 2026)',
|
|
94
|
+
description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
|
|
95
|
+
evidence: JSON.stringify(evidence),
|
|
96
|
+
mitigation: buildRemediation(triggered),
|
|
97
|
+
},
|
|
98
|
+
];
|
|
93
99
|
}
|
|
@@ -8,36 +8,49 @@ const CHILD_PROC_RE = /\b(?:spawn|exec|execSync|spawnSync|fork)\s*\(/g;
|
|
|
8
8
|
const FS_CHMOD_RE = /fs\.chmod\s*\(/g;
|
|
9
9
|
|
|
10
10
|
function detectMagicBytes(content) {
|
|
11
|
-
if (!content || content.length < 4)
|
|
11
|
+
if (!content || content.length < 4) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
12
14
|
|
|
13
15
|
const c0 = content.charCodeAt(0);
|
|
14
16
|
const c1 = content.charCodeAt(1);
|
|
15
17
|
const c2 = content.charCodeAt(2);
|
|
16
18
|
const c3 = content.charCodeAt(3);
|
|
17
19
|
|
|
18
|
-
if (c0 === 0x7f && content.slice(1, 4) === 'ELF')
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
if (c0 === 0x7f && content.slice(1, 4) === 'ELF') {
|
|
21
|
+
return 'elf_embedded';
|
|
22
|
+
}
|
|
23
|
+
if (c0 === 0x4d && c1 === 0x5a) {
|
|
24
|
+
return 'pe_embedded';
|
|
25
|
+
}
|
|
26
|
+
if (c0 === 0x00 && content.slice(1, 4) === 'asm') {
|
|
27
|
+
return 'wasm_embedded';
|
|
28
|
+
}
|
|
21
29
|
|
|
22
|
-
const machO =
|
|
30
|
+
const machO =
|
|
31
|
+
(c0 === 0xfe && c1 === 0xed && c2 === 0xfa && (c3 === 0xce || c3 === 0xcf)) ||
|
|
23
32
|
(c0 === 0xce && c1 === 0xfa && c2 === 0xed && (c3 === 0xfe || c3 === 0xcf)) ||
|
|
24
33
|
(c0 === 0xcf && c1 === 0xfa && c2 === 0xed && c3 === 0xfe);
|
|
25
|
-
if (machO)
|
|
34
|
+
if (machO) {
|
|
35
|
+
return 'macho_embedded';
|
|
36
|
+
}
|
|
26
37
|
|
|
27
38
|
const universal = c0 === 0xca && c1 === 0xfe && c2 === 0xba && c3 === 0xbe;
|
|
28
|
-
if (universal)
|
|
39
|
+
if (universal) {
|
|
40
|
+
return 'macho_embedded';
|
|
41
|
+
}
|
|
29
42
|
|
|
30
43
|
return null;
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
function isInBinaryDir(filePath) {
|
|
34
47
|
const normalized = filePath.replace(/\\/g, '/');
|
|
35
|
-
return BINARY_DIRS.some(dir => normalized.includes(`/${dir}`) || normalized.startsWith(dir));
|
|
48
|
+
return BINARY_DIRS.some((dir) => normalized.includes(`/${dir}`) || normalized.startsWith(dir));
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
function hasBinaryExt(filePath) {
|
|
39
52
|
const lower = filePath.toLowerCase();
|
|
40
|
-
return BINARY_EXTS.some(ext => lower.endsWith(ext));
|
|
53
|
+
return BINARY_EXTS.some((ext) => lower.endsWith(ext));
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
function isKnownBinaryName(fileName) {
|
|
@@ -45,23 +58,60 @@ function isKnownBinaryName(fileName) {
|
|
|
45
58
|
return BINARY_FILENAMES.includes(base);
|
|
46
59
|
}
|
|
47
60
|
|
|
61
|
+
const CROSS_PLATFORM_RE =
|
|
62
|
+
/-(?:linux|darwin|macos|win32|windows|win)-(?:x64|x86|arm64|ia32)\.?(?:exe)?$/i;
|
|
63
|
+
|
|
64
|
+
function detectCrossPlatformSets(binaries) {
|
|
65
|
+
const sets = {};
|
|
66
|
+
for (const bin of binaries) {
|
|
67
|
+
const base = bin.file.replace(CROSS_PLATFORM_RE, '').split(/[/\\]/).pop();
|
|
68
|
+
if (!sets[base]) {
|
|
69
|
+
sets[base] = [];
|
|
70
|
+
}
|
|
71
|
+
sets[base].push(bin.file);
|
|
72
|
+
}
|
|
73
|
+
for (const [base, files] of Object.entries(sets)) {
|
|
74
|
+
if (files.length >= 2) {
|
|
75
|
+
return { base, files, count: files.length };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
48
81
|
function isDeclared(pkgJson, fileName) {
|
|
49
|
-
if (!pkgJson)
|
|
82
|
+
if (!pkgJson) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
50
85
|
const baseName = fileName.split(/[/\\]/).pop();
|
|
51
86
|
|
|
52
87
|
if (pkgJson.bin) {
|
|
53
|
-
if (typeof pkgJson.bin === 'string' && pkgJson.bin === baseName)
|
|
54
|
-
|
|
88
|
+
if (typeof pkgJson.bin === 'string' && pkgJson.bin === baseName) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
if (
|
|
92
|
+
typeof pkgJson.bin === 'object' &&
|
|
93
|
+
Object.values(pkgJson.bin).some((v) => v === baseName || v.endsWith(`/${baseName}`))
|
|
94
|
+
) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
55
97
|
}
|
|
56
98
|
|
|
57
99
|
if (pkgJson.optionalDependencies) {
|
|
58
|
-
for (const [name,
|
|
59
|
-
if (name === baseName)
|
|
100
|
+
for (const [name, _val] of Object.entries(pkgJson.optionalDependencies)) {
|
|
101
|
+
if (name === baseName) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
60
104
|
}
|
|
61
105
|
}
|
|
62
106
|
|
|
63
|
-
if (
|
|
64
|
-
|
|
107
|
+
if (
|
|
108
|
+
pkgJson.gypfile === true ||
|
|
109
|
+
pkgJson.scripts?.install?.includes('node-gyp') ||
|
|
110
|
+
pkgJson.scripts?.install?.includes('node-pre-gyp')
|
|
111
|
+
) {
|
|
112
|
+
if (baseName.endsWith('.node')) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
65
115
|
}
|
|
66
116
|
|
|
67
117
|
return false;
|
|
@@ -71,15 +121,26 @@ export const name = 'tier1-binary-embed';
|
|
|
71
121
|
|
|
72
122
|
export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
73
123
|
const pkgName = pkgJson?.name;
|
|
74
|
-
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName))
|
|
124
|
+
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
75
127
|
|
|
76
|
-
if (!allFiles || allFiles.length === 0)
|
|
128
|
+
if (!allFiles || allFiles.length === 0) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
77
131
|
|
|
78
|
-
if (
|
|
79
|
-
pkgName
|
|
80
|
-
pkgName === '
|
|
81
|
-
|
|
82
|
-
|
|
132
|
+
if (
|
|
133
|
+
pkgName &&
|
|
134
|
+
(pkgName === 'electron' ||
|
|
135
|
+
pkgName === 'puppeteer' ||
|
|
136
|
+
pkgName === 'sharp' ||
|
|
137
|
+
pkgName === 'esbuild' ||
|
|
138
|
+
pkgName === 'node-gyp' ||
|
|
139
|
+
pkgName === 'node-pre-gyp' ||
|
|
140
|
+
pkgName === '@mapbox/node-pre-gyp')
|
|
141
|
+
) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
83
144
|
|
|
84
145
|
const binaries = [];
|
|
85
146
|
|
|
@@ -111,9 +172,13 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
111
172
|
}
|
|
112
173
|
}
|
|
113
174
|
|
|
114
|
-
if (binaries.length === 0)
|
|
175
|
+
if (binaries.length === 0) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
115
178
|
|
|
116
|
-
const
|
|
179
|
+
const crossPlatformSet = detectCrossPlatformSets(binaries);
|
|
180
|
+
|
|
181
|
+
const jsCode = (jsFiles || []).map((f) => f.content || '').join('\n');
|
|
117
182
|
const invoked = CHILD_PROC_RE.test(jsCode) || FS_CHMOD_RE.test(jsCode);
|
|
118
183
|
|
|
119
184
|
const invokedFiles = [];
|
|
@@ -134,43 +199,70 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
134
199
|
let baseScore;
|
|
135
200
|
let subtype;
|
|
136
201
|
|
|
202
|
+
// Cross-platform platform set boost
|
|
203
|
+
const isCrossPlatform =
|
|
204
|
+
crossPlatformSet &&
|
|
205
|
+
crossPlatformSet.files.some(
|
|
206
|
+
(f) => f === bin.file || f.includes(bin.file) || bin.file.includes(f.replace(/\.exe$/, ''))
|
|
207
|
+
);
|
|
208
|
+
|
|
137
209
|
if (bin.magic === 'elf_embedded') {
|
|
138
210
|
baseScore = 95;
|
|
139
|
-
subtype = 'elf_embedded';
|
|
211
|
+
subtype = isCrossPlatform ? 'cross_platform_elf' : 'elf_embedded';
|
|
140
212
|
} else if (bin.magic === 'pe_embedded') {
|
|
141
213
|
baseScore = 95;
|
|
142
|
-
subtype = 'pe_embedded';
|
|
214
|
+
subtype = isCrossPlatform ? 'cross_platform_pe' : 'pe_embedded';
|
|
143
215
|
} else if (bin.magic === 'macho_embedded') {
|
|
144
216
|
baseScore = 95;
|
|
145
|
-
subtype = 'macho_embedded';
|
|
217
|
+
subtype = isCrossPlatform ? 'cross_platform_macho' : 'macho_embedded';
|
|
146
218
|
} else if (bin.magic === 'wasm_embedded') {
|
|
147
219
|
baseScore = 60;
|
|
148
|
-
subtype = 'wasm_embedded';
|
|
220
|
+
subtype = isCrossPlatform ? 'cross_platform_wasm' : 'wasm_embedded';
|
|
149
221
|
} else {
|
|
150
222
|
baseScore = 60;
|
|
151
|
-
subtype = 'magic_byte_unknown';
|
|
223
|
+
subtype = isCrossPlatform ? 'cross_platform_unknown' : 'magic_byte_unknown';
|
|
152
224
|
}
|
|
153
225
|
|
|
154
226
|
let score = baseScore;
|
|
155
227
|
|
|
156
|
-
if (
|
|
228
|
+
if (isCrossPlatform) {
|
|
229
|
+
score += 25;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (bin.inBinDir) {
|
|
233
|
+
score += 15;
|
|
234
|
+
}
|
|
157
235
|
|
|
158
|
-
if (!bin.declared)
|
|
236
|
+
if (!bin.declared) {
|
|
237
|
+
score += 50;
|
|
238
|
+
}
|
|
159
239
|
|
|
160
|
-
if (invoked && invokedFiles.length > 0)
|
|
240
|
+
if (invoked && invokedFiles.length > 0) {
|
|
241
|
+
score += 25;
|
|
242
|
+
}
|
|
161
243
|
|
|
162
244
|
const confidenceScore = Math.max(50, Math.min(100, score));
|
|
163
245
|
|
|
164
246
|
function severityLabel(sc) {
|
|
165
|
-
if (sc >= 90)
|
|
166
|
-
|
|
247
|
+
if (sc >= 90) {
|
|
248
|
+
return 'critical';
|
|
249
|
+
}
|
|
250
|
+
if (sc >= 70) {
|
|
251
|
+
return 'high';
|
|
252
|
+
}
|
|
167
253
|
return 'medium';
|
|
168
254
|
}
|
|
169
255
|
|
|
170
256
|
function confidenceLabel(sc) {
|
|
171
|
-
if (sc >= 95)
|
|
172
|
-
|
|
173
|
-
|
|
257
|
+
if (sc >= 95) {
|
|
258
|
+
return 'CRITICAL';
|
|
259
|
+
}
|
|
260
|
+
if (sc >= 80) {
|
|
261
|
+
return 'HIGH';
|
|
262
|
+
}
|
|
263
|
+
if (sc >= 60) {
|
|
264
|
+
return 'MEDIUM';
|
|
265
|
+
}
|
|
174
266
|
return 'LOW';
|
|
175
267
|
}
|
|
176
268
|
|
|
@@ -179,14 +271,19 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
179
271
|
`path: ${bin.file}`,
|
|
180
272
|
`declared: ${bin.declared}`,
|
|
181
273
|
];
|
|
274
|
+
if (isCrossPlatform) {
|
|
275
|
+
evidence.push(
|
|
276
|
+
`cross-platform binary set: ${crossPlatformSet.count} variants of "${crossPlatformSet.base}"`
|
|
277
|
+
);
|
|
278
|
+
evidence.push(`platform_files: ${crossPlatformSet.files.join(', ')}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
182
281
|
if (invoked && invokedFiles.length > 0) {
|
|
183
282
|
evidence.push(`invoked: child_process usage in ${invokedFiles.length} file(s)`);
|
|
184
283
|
evidence.push(`invoked_file: ${invokedFiles[0]}`);
|
|
185
284
|
}
|
|
186
285
|
|
|
187
|
-
const locations = [
|
|
188
|
-
{ file: bin.file, size: bin.size },
|
|
189
|
-
];
|
|
286
|
+
const locations = [{ file: bin.file, size: bin.size }];
|
|
190
287
|
|
|
191
288
|
if (invokedFiles.length > 0) {
|
|
192
289
|
locations.push({ file: invokedFiles[0], line: 0 });
|
|
@@ -4,41 +4,48 @@ const GCP_PATTERNS = [
|
|
|
4
4
|
'metadata.google.internal/computeMetadata',
|
|
5
5
|
];
|
|
6
6
|
|
|
7
|
-
const AZURE_PATTERNS = [
|
|
8
|
-
'169.254.169.254/metadata/instance',
|
|
9
|
-
'169.254.169.254/metadata/identity',
|
|
10
|
-
];
|
|
7
|
+
const AZURE_PATTERNS = ['169.254.169.254/metadata/instance', '169.254.169.254/metadata/identity'];
|
|
11
8
|
|
|
12
9
|
const AZURE_IP = '169.254.169.254';
|
|
13
10
|
const METADATA_HEADER_RE = /Metadata\s*:\s*true/i;
|
|
14
11
|
|
|
15
12
|
function severityLabel(score) {
|
|
16
|
-
if (score >= 80)
|
|
13
|
+
if (score >= 80) {
|
|
14
|
+
return 'high';
|
|
15
|
+
}
|
|
17
16
|
return 'medium';
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
function confidenceLabel(score) {
|
|
21
|
-
if (score >= 80)
|
|
22
|
-
|
|
20
|
+
if (score >= 80) {
|
|
21
|
+
return 'HIGH';
|
|
22
|
+
}
|
|
23
|
+
if (score >= 60) {
|
|
24
|
+
return 'MEDIUM';
|
|
25
|
+
}
|
|
23
26
|
return 'LOW';
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
function hasGcpPattern(text) {
|
|
27
|
-
return GCP_PATTERNS.some(p => text.includes(p));
|
|
30
|
+
return GCP_PATTERNS.some((p) => text.includes(p));
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
function hasAzurePath(text) {
|
|
31
|
-
return AZURE_PATTERNS.some(p => text.includes(p));
|
|
34
|
+
return AZURE_PATTERNS.some((p) => text.includes(p));
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
function hasAzureHeaderPattern(text) {
|
|
35
38
|
const lines = text.split('\n');
|
|
36
39
|
for (let i = 0; i < lines.length; i++) {
|
|
37
|
-
if (!lines[i].includes(AZURE_IP))
|
|
40
|
+
if (!lines[i].includes(AZURE_IP)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
38
43
|
const start = Math.max(0, i - 5);
|
|
39
44
|
const end = Math.min(lines.length, i + 6);
|
|
40
45
|
for (let j = start; j < end; j++) {
|
|
41
|
-
if (METADATA_HEADER_RE.test(lines[j]))
|
|
46
|
+
if (METADATA_HEADER_RE.test(lines[j])) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
51
|
return false;
|
|
@@ -72,20 +79,30 @@ function collectTexts(pkgJson, jsFiles) {
|
|
|
72
79
|
|
|
73
80
|
export const name = 'tier1-cloud-imds';
|
|
74
81
|
|
|
75
|
-
export async function scan(pkgJson, jsFiles,
|
|
82
|
+
export async function scan(pkgJson, jsFiles, _registryMeta, _allFiles) {
|
|
76
83
|
const texts = collectTexts(pkgJson, jsFiles);
|
|
77
|
-
if (texts.length === 0)
|
|
84
|
+
if (texts.length === 0) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
78
87
|
|
|
79
88
|
let hasGcp = false;
|
|
80
89
|
let hasAzure = false;
|
|
81
90
|
|
|
82
91
|
for (const text of texts) {
|
|
83
|
-
if (!hasGcp && hasGcpPattern(text))
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
if (!hasGcp && hasGcpPattern(text)) {
|
|
93
|
+
hasGcp = true;
|
|
94
|
+
}
|
|
95
|
+
if (!hasAzure && hasAzurePattern(text)) {
|
|
96
|
+
hasAzure = true;
|
|
97
|
+
}
|
|
98
|
+
if (hasGcp && hasAzure) {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
86
101
|
}
|
|
87
102
|
|
|
88
|
-
if (!hasGcp && !hasAzure)
|
|
103
|
+
if (!hasGcp && !hasAzure) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
89
106
|
|
|
90
107
|
let confidenceScore;
|
|
91
108
|
let subtype;
|
|
@@ -101,24 +118,27 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
101
118
|
subtype = 'azure_imds';
|
|
102
119
|
}
|
|
103
120
|
|
|
104
|
-
return [
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
return [
|
|
122
|
+
{
|
|
123
|
+
detector: 'tier1-cloud-imds',
|
|
124
|
+
id: 'TIER1-CLOUD-IMDS',
|
|
125
|
+
severity: severityLabel(confidenceScore),
|
|
126
|
+
confidence: confidenceLabel(confidenceScore),
|
|
127
|
+
confidenceScore,
|
|
128
|
+
subtype,
|
|
129
|
+
message:
|
|
130
|
+
hasGcp && hasAzure
|
|
131
|
+
? `Package references both GCP metadata and Azure IMDS endpoints — cloud credential harvesting`
|
|
132
|
+
: hasGcp
|
|
133
|
+
? `Package references GCP metadata server endpoint — cloud credential harvesting`
|
|
134
|
+
: `Package references Azure IMDS endpoint — cloud credential harvesting`,
|
|
135
|
+
evidence: [
|
|
136
|
+
...(hasGcp ? ['gcp: metadata.google.internal / computeMetadata/v1 pattern detected'] : []),
|
|
137
|
+
...(hasAzure ? ['azure: 169.254.169.254/metadata pattern detected'] : []),
|
|
138
|
+
],
|
|
139
|
+
crossFiles: [],
|
|
140
|
+
locations: [{ file: '', line: 0 }],
|
|
141
|
+
reference: 'Miasma Cloud IMDS',
|
|
142
|
+
},
|
|
143
|
+
];
|
|
124
144
|
}
|