@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
|
@@ -15,48 +15,57 @@ const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low', 'info', 'none'];
|
|
|
15
15
|
|
|
16
16
|
function highestSeverity(severities) {
|
|
17
17
|
for (const s of SEVERITY_ORDER) {
|
|
18
|
-
if (severities.includes(s))
|
|
18
|
+
if (severities.includes(s)) {
|
|
19
|
+
return s;
|
|
20
|
+
}
|
|
19
21
|
}
|
|
20
22
|
return 'none';
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
export async function scan(pkgJson, files = [],
|
|
25
|
+
export async function scan(pkgJson, files = [], _registryMeta = null, allFiles = null) {
|
|
24
26
|
const fileList = allFiles || files || [];
|
|
25
27
|
const pkgName = pkgJson?.name || 'unknown';
|
|
26
28
|
const pkgVersion = pkgJson?.version || '0.0.0';
|
|
27
29
|
|
|
28
30
|
const d1Results = scanCtfScramble(fileList);
|
|
29
31
|
if (d1Results.stopCondition) {
|
|
30
|
-
const evidence = attachProvenance(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
32
|
+
const evidence = attachProvenance(
|
|
33
|
+
{
|
|
34
|
+
rule: 'MSH-OBF-001',
|
|
35
|
+
campaign: 'MINI_SHAI_HULUD',
|
|
36
|
+
triggeredChecks: ['D1'],
|
|
37
|
+
filePath: d1Results.filePath,
|
|
38
|
+
patternMatched: d1Results.patternMatched,
|
|
39
|
+
action: 'BLOCK_IMMEDIATELY',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
ruleId: 'MSH-OBF-001',
|
|
43
|
+
ruleName: 'ctf-scramble-v2 Obfuscation Detection',
|
|
44
|
+
campaignName: 'Mini Shai-Hulud',
|
|
45
|
+
pkgName,
|
|
46
|
+
pkgVersion,
|
|
47
|
+
triggered: true,
|
|
48
|
+
severity: 'critical',
|
|
49
|
+
indicators: [{ type: 'obfuscation_found', value: d1Results.patternMatched }],
|
|
50
|
+
ruleProvenanceUrl:
|
|
51
|
+
'https://github.com/lateos/npm-scan/blob/main/backend/detectors/msh-supplement/d1-obfuscation.js',
|
|
52
|
+
campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
|
|
53
|
+
}
|
|
54
|
+
);
|
|
50
55
|
|
|
51
|
-
return [
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
return [
|
|
57
|
+
{
|
|
58
|
+
id: 'MINI_SHAI_HULUD',
|
|
59
|
+
severity: 'critical',
|
|
60
|
+
title: 'Mini Shai-Hulud worm campaign — ctf-scramble-v2 malware obfuscation detected',
|
|
61
|
+
description:
|
|
62
|
+
'HALT: ctf-scramble-v2 obfuscation layer detected. Package is compromised. Block install immediately.',
|
|
63
|
+
evidence: JSON.stringify(evidence),
|
|
64
|
+
mitigation:
|
|
65
|
+
'BLOCK IMMEDIATELY. Do not install this package version. Revoke any npm tokens exposed to this package. Rotate all CI/CD secrets. Run full malware scan on any system that processed this package.',
|
|
66
|
+
stopCondition: true,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
const results = {
|
|
@@ -69,39 +78,45 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
|
|
|
69
78
|
.filter(([_, r]) => r.triggered)
|
|
70
79
|
.map(([id]) => id);
|
|
71
80
|
|
|
72
|
-
if (triggered.length === 0)
|
|
81
|
+
if (triggered.length === 0) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
73
84
|
|
|
74
|
-
const severity = highestSeverity(triggered.map(id => RULE_SEVERITY[id]));
|
|
85
|
+
const severity = highestSeverity(triggered.map((id) => RULE_SEVERITY[id]));
|
|
75
86
|
|
|
76
|
-
const evidence = attachProvenance(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
Object.entries(results).filter(([_, r]) => r.triggered)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
87
|
+
const evidence = attachProvenance(
|
|
88
|
+
{
|
|
89
|
+
campaign: 'MINI_SHAI_HULUD',
|
|
90
|
+
triggeredChecks: triggered,
|
|
91
|
+
details: Object.fromEntries(Object.entries(results).filter(([_, r]) => r.triggered)),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
ruleId: 'MSH-SUPPLEMENT',
|
|
95
|
+
ruleName: 'Mini Shai-Hulud Supplement Detection',
|
|
96
|
+
campaignName: 'Mini Shai-Hulud',
|
|
97
|
+
pkgName,
|
|
98
|
+
pkgVersion,
|
|
99
|
+
triggered: true,
|
|
100
|
+
severity,
|
|
101
|
+
indicators: triggered.map((id) => ({
|
|
102
|
+
type: `rule_${id}`,
|
|
103
|
+
value: RULE_SEVERITY[id],
|
|
104
|
+
})),
|
|
105
|
+
ruleProvenanceUrl:
|
|
106
|
+
'https://github.com/lateos/npm-scan/blob/main/backend/detectors/msh-supplement/',
|
|
107
|
+
campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
|
|
108
|
+
}
|
|
109
|
+
);
|
|
98
110
|
|
|
99
|
-
return [
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
return [
|
|
112
|
+
{
|
|
113
|
+
id: 'MINI_SHAI_HULUD',
|
|
114
|
+
severity,
|
|
115
|
+
title: 'Mini Shai-Hulud worm campaign — supplement indicators',
|
|
116
|
+
description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
|
|
117
|
+
evidence: JSON.stringify(evidence),
|
|
118
|
+
mitigation:
|
|
119
|
+
'If daemonization detected: revoke npm tokens and rotate CI/CD secrets. If geographic killswitch detected: verify running in expected region; attacker may be avoiding certain locales. If C2 dead-drop detected: check for unauthorized GitHub API access and token exfiltration. Review recent version publish history for anomalous bursts.',
|
|
120
|
+
},
|
|
121
|
+
];
|
|
107
122
|
}
|
|
@@ -6,9 +6,11 @@ const SAFE_PINS = {
|
|
|
6
6
|
'12.0.1': '12.0.0',
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
export function scanVersionBlocklist(pkgJson,
|
|
9
|
+
export function scanVersionBlocklist(pkgJson, _registryMeta) {
|
|
10
10
|
const pkgName = pkgJson?.name || '';
|
|
11
|
-
if (pkgName !== 'node-ipc')
|
|
11
|
+
if (pkgName !== 'node-ipc') {
|
|
12
|
+
return { triggered: false };
|
|
13
|
+
}
|
|
12
14
|
|
|
13
15
|
const version = pkgJson?.version || '';
|
|
14
16
|
if (BLOCKED_VERSIONS.has(version)) {
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
export function scanUnauthorizedPublisher(pkgJson, registryMeta) {
|
|
2
2
|
const pkgName = pkgJson?.name || '';
|
|
3
|
-
if (pkgName !== 'node-ipc')
|
|
3
|
+
if (pkgName !== 'node-ipc') {
|
|
4
|
+
return { triggered: false };
|
|
5
|
+
}
|
|
4
6
|
|
|
5
|
-
const publisherAccount =
|
|
6
|
-
|
|
7
|
-
||
|
|
7
|
+
const publisherAccount =
|
|
8
|
+
registryMeta?.versions?.[pkgJson?.version]?._npmUser?.name ||
|
|
9
|
+
registryMeta?.versions?.[Object.keys(registryMeta.versions || {})[0]]?._npmUser?.name ||
|
|
10
|
+
null;
|
|
8
11
|
|
|
9
12
|
if (publisherAccount === 'atiertant') {
|
|
10
13
|
return {
|
|
11
14
|
triggered: true,
|
|
12
15
|
publisher: publisherAccount,
|
|
13
16
|
package: pkgName,
|
|
14
|
-
detail:
|
|
17
|
+
detail:
|
|
18
|
+
'Account atiertant has no prior release history on node-ipc — account recovery via expired email domain takeover',
|
|
15
19
|
};
|
|
16
20
|
}
|
|
17
21
|
|
|
@@ -16,12 +16,16 @@ export function scanBlastRadius(allFiles) {
|
|
|
16
16
|
|
|
17
17
|
for (const file of allFiles) {
|
|
18
18
|
const path = file.path?.replace(/\\/g, '/') || '';
|
|
19
|
-
const isLockfile = LOCKFILE_PATTERNS.some(p => p.test(path));
|
|
20
|
-
if (!isLockfile)
|
|
19
|
+
const isLockfile = LOCKFILE_PATTERNS.some((p) => p.test(path));
|
|
20
|
+
if (!isLockfile) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
21
23
|
|
|
22
24
|
const content = file.content || '';
|
|
23
25
|
const hasNodeIpc = /\bnode-ipc\b/i.test(content);
|
|
24
|
-
if (!hasNodeIpc)
|
|
26
|
+
if (!hasNodeIpc) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
25
29
|
|
|
26
30
|
for (const [badVersion, info] of Object.entries(COMPROMISED_VERSIONS)) {
|
|
27
31
|
const versionInQuotes = `"${badVersion}"`;
|
|
@@ -11,7 +11,9 @@ export function scanTarballHash(allFiles) {
|
|
|
11
11
|
|
|
12
12
|
for (const file of allFiles) {
|
|
13
13
|
const path = file.path || '';
|
|
14
|
-
if (!path.endsWith('.tgz') && !path.endsWith('.tar.gz'))
|
|
14
|
+
if (!path.endsWith('.tgz') && !path.endsWith('.tar.gz')) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
15
17
|
|
|
16
18
|
const content = file.content || '';
|
|
17
19
|
const hash = createHash('sha256').update(content, 'utf8').digest('hex');
|
|
@@ -20,9 +22,12 @@ export function scanTarballHash(allFiles) {
|
|
|
20
22
|
matches.push({
|
|
21
23
|
file: path,
|
|
22
24
|
sha256: hash,
|
|
23
|
-
version:
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
version:
|
|
26
|
+
hash === '449e4265979b5fdb2d3446c021af437e815debd66de7da2fe54f1ad93cbcc75e'
|
|
27
|
+
? '9.1.6'
|
|
28
|
+
: hash === 'c2f4dc64aec4631540a568e88932b61daebbfb7e8281b812fa01b7215f9be9ea'
|
|
29
|
+
? '9.2.3'
|
|
30
|
+
: '12.0.1',
|
|
26
31
|
});
|
|
27
32
|
}
|
|
28
33
|
}
|
|
@@ -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
|
}
|