@lateos/npm-scan 0.18.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +265 -233
- package/LICENSING.md +19 -19
- package/README.de.md +708 -708
- package/README.fr.md +707 -707
- package/README.ja.md +704 -704
- package/README.md +861 -826
- package/README.zh.md +708 -708
- package/VALIDATION.md +92 -0
- package/backend/cra.js +68 -68
- package/backend/db/pg-schema.sql +155 -0
- package/backend/db/schema.sql +32 -32
- package/backend/db.js +88 -88
- package/backend/detectors/atk-001-lifecycle.js +17 -17
- package/backend/detectors/atk-002-obfusc.js +261 -261
- package/backend/detectors/atk-003-creds.js +13 -13
- package/backend/detectors/atk-004-persist.js +13 -13
- package/backend/detectors/atk-005-exfil.js +13 -13
- package/backend/detectors/atk-006-depconf.js +14 -14
- package/backend/detectors/atk-007-typosquat.js +34 -34
- package/backend/detectors/atk-008-tarball-tamper.js +91 -91
- package/backend/detectors/atk-009-dormant-trigger.js +62 -62
- package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
- package/backend/detectors/atk-011-transitive-prop.js +76 -76
- package/backend/detectors/config/thresholds.js +66 -0
- package/backend/detectors/config/whitelist.json +74 -0
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
- package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
- package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
- package/backend/detectors/hf-impersonation/index.js +396 -396
- package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
- package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
- package/backend/detectors/hf-impersonation/simhash.js +46 -46
- package/backend/detectors/index.js +87 -81
- package/backend/detectors/lib/ast-patterns.js +21 -0
- package/backend/detectors/lib/entropy-analyzer.js +24 -0
- package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
- package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
- package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
- package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
- package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
- package/backend/detectors/megalodon/index.js +80 -80
- package/backend/detectors/megalodon/types.js +9 -9
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
- package/backend/detectors/mini-shai-hulud/index.js +118 -118
- package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
- package/backend/detectors/tier1-binary-embed.js +34 -5
- package/backend/detectors/tier1-obfuscation-heuristics.js +156 -0
- package/backend/detectors/tier1-slsa-attestation.js +12 -0
- package/backend/detectors/tier1-version-anomaly.js +187 -0
- package/backend/detectors.test.js +88 -0
- package/backend/fetch.js +175 -175
- package/backend/index.js +4 -4
- package/backend/license.js +89 -89
- package/backend/lockfile.js +379 -379
- package/backend/pdf.js +245 -245
- package/backend/policy.js +193 -193
- package/backend/report.js +254 -254
- package/backend/sbom.js +66 -66
- package/backend/scripts/analyze-false-positives.js +146 -0
- package/backend/scripts/analyze-validation.js +151 -0
- package/backend/scripts/detect-false-positives.js +93 -0
- package/backend/scripts/fetch-top-packages.js +129 -0
- package/backend/scripts/validate-detectors.js +142 -0
- package/backend/siem/cef.js +32 -32
- package/backend/siem/ecs.js +40 -40
- package/backend/siem/index.js +18 -18
- package/backend/siem/qradar.js +56 -56
- package/backend/siem/sentinel.js +27 -27
- package/backend/tests-d5-enhanced.test.js +46 -0
- package/backend/tests-d6-version-anomaly.test.js +58 -0
- package/backend/tests-d6.test.js +116 -0
- package/backend/tests-d6c.test.js +106 -0
- package/backend/tests-d7-obfuscation.test.js +91 -0
- package/backend/tests.test.js +898 -0
- package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
- package/backend/vsix-scan/detectors/burst-publish.js +52 -52
- package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
- package/backend/vsix-scan/detectors/known-ioc.js +105 -105
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
- package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
- package/backend/vsix-scan/index.js +183 -183
- package/backend/vsix-scan/marketplace-client.js +145 -145
- package/backend/vsix-scan/vsix-iocs.json +31 -31
- package/cli/cli.js +458 -458
- package/package.json +74 -57
- package/.dockerignore +0 -20
- package/.husky/pre-commit +0 -1
- package/SECURITY.md +0 -73
- package/deploy/helm/npm-scan/Chart.yaml +0 -22
- package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
- package/deploy/helm/npm-scan/templates/api.yaml +0 -94
- package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
- package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
- package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
- package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
- package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
- package/deploy/helm/npm-scan/values.yaml +0 -103
- package/scripts/download-corpus.js +0 -30
- package/scripts/gen-mal-corpus.js +0 -35
- package/scripts/generate-campaign-fixtures.js +0 -170
- package/src/config/top-5000.json +0 -87
- package/test/fixtures/lockfiles/npm-lock.json +0 -69
- package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
- package/test/fixtures/lockfiles/yarn.lock +0 -104
- package/test/fixtures/mock-data.js +0 -69
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
const EXFIL_PATTERNS = [
|
|
2
|
-
/NPM_TOKEN|NODE_AUTH_TOKEN|GH_TOKEN|GITHUB_TOKEN|npm_token|node_auth_token/i,
|
|
3
|
-
/~\/(\.npmrc|\.gitconfig|\.aws\/credentials)/,
|
|
4
|
-
/\/run\/secrets\//,
|
|
5
|
-
/\$GITHUB_ENV/,
|
|
6
|
-
/process\.env\.(NPM_TOKEN|NODE_AUTH_TOKEN|GH_TOKEN|GITHUB_TOKEN)/,
|
|
7
|
-
/Buffer\.from\s*\([^)]*\)\s*\.\s*toString\s*\(\s*['"]base64['"]\s*\)/,
|
|
8
|
-
/\batob\s*\(/,
|
|
9
|
-
/\bbtoa\s*\(/,
|
|
10
|
-
];
|
|
11
|
-
|
|
12
|
-
const SUSPICIOUS_SCRIPTS = ['preinstall', 'install', 'postinstall', 'prepare'];
|
|
13
|
-
|
|
14
|
-
const MAX_SNIPPET_LENGTH = 200;
|
|
15
|
-
|
|
16
|
-
function truncateSnippet(text) {
|
|
17
|
-
if (text.length <= MAX_SNIPPET_LENGTH) return text;
|
|
18
|
-
return text.slice(0, MAX_SNIPPET_LENGTH - 3) + '...';
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function checkTokenExfil(allFiles, pkgJson) {
|
|
22
|
-
const scripts = pkgJson?.scripts || {};
|
|
23
|
-
const snippets = [];
|
|
24
|
-
|
|
25
|
-
for (const hook of SUSPICIOUS_SCRIPTS) {
|
|
26
|
-
const scriptContent = scripts[hook];
|
|
27
|
-
if (!scriptContent) continue;
|
|
28
|
-
|
|
29
|
-
for (const pattern of EXFIL_PATTERNS) {
|
|
30
|
-
if (pattern.test(scriptContent)) {
|
|
31
|
-
snippets.push(truncateSnippet(scriptContent));
|
|
32
|
-
break;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return { triggered: snippets.length > 0, snippets };
|
|
38
|
-
}
|
|
1
|
+
const EXFIL_PATTERNS = [
|
|
2
|
+
/NPM_TOKEN|NODE_AUTH_TOKEN|GH_TOKEN|GITHUB_TOKEN|npm_token|node_auth_token/i,
|
|
3
|
+
/~\/(\.npmrc|\.gitconfig|\.aws\/credentials)/,
|
|
4
|
+
/\/run\/secrets\//,
|
|
5
|
+
/\$GITHUB_ENV/,
|
|
6
|
+
/process\.env\.(NPM_TOKEN|NODE_AUTH_TOKEN|GH_TOKEN|GITHUB_TOKEN)/,
|
|
7
|
+
/Buffer\.from\s*\([^)]*\)\s*\.\s*toString\s*\(\s*['"]base64['"]\s*\)/,
|
|
8
|
+
/\batob\s*\(/,
|
|
9
|
+
/\bbtoa\s*\(/,
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const SUSPICIOUS_SCRIPTS = ['preinstall', 'install', 'postinstall', 'prepare'];
|
|
13
|
+
|
|
14
|
+
const MAX_SNIPPET_LENGTH = 200;
|
|
15
|
+
|
|
16
|
+
function truncateSnippet(text) {
|
|
17
|
+
if (text.length <= MAX_SNIPPET_LENGTH) return text;
|
|
18
|
+
return text.slice(0, MAX_SNIPPET_LENGTH - 3) + '...';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function checkTokenExfil(allFiles, pkgJson) {
|
|
22
|
+
const scripts = pkgJson?.scripts || {};
|
|
23
|
+
const snippets = [];
|
|
24
|
+
|
|
25
|
+
for (const hook of SUSPICIOUS_SCRIPTS) {
|
|
26
|
+
const scriptContent = scripts[hook];
|
|
27
|
+
if (!scriptContent) continue;
|
|
28
|
+
|
|
29
|
+
for (const pattern of EXFIL_PATTERNS) {
|
|
30
|
+
if (pattern.test(scriptContent)) {
|
|
31
|
+
snippets.push(truncateSnippet(scriptContent));
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { triggered: snippets.length > 0, snippets };
|
|
38
|
+
}
|
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
import { checkBurstPublish } from './d1-burst-publish.js';
|
|
2
|
-
import { checkSiblingCompromise, clearSiblingCache } from './d2-sibling-compromise.js';
|
|
3
|
-
import { checkSlsaMismatch } from './d3-slsa-mismatch.js';
|
|
4
|
-
import { checkMaintainerAnomaly } from './d4-maintainer-anomaly.js';
|
|
5
|
-
import { checkIOC } from './d5-ioc-check.js';
|
|
6
|
-
import { checkTokenExfil } from './d6-token-exfil.js';
|
|
7
|
-
|
|
8
|
-
export async function scan(pkgJson, files = [], registryMeta = null, allFiles = null) {
|
|
9
|
-
const config = {};
|
|
10
|
-
|
|
11
|
-
const burstResult = await checkBurstPublish(registryMeta, config);
|
|
12
|
-
const maintainerResult = await checkMaintainerAnomaly(registryMeta, config);
|
|
13
|
-
|
|
14
|
-
const pkgName = pkgJson?.name || '';
|
|
15
|
-
const pkgVersion = pkgJson?.version || '';
|
|
16
|
-
const sha512 = registryMeta?.versions?.[pkgVersion]?.dist?.integrity || null;
|
|
17
|
-
const publisherAccount = registryMeta?.versions?.[pkgVersion]?._npmUser?.name || null;
|
|
18
|
-
const timeMap = registryMeta?.time || {};
|
|
19
|
-
|
|
20
|
-
const iocResult = await checkIOC(pkgName, pkgVersion, sha512, publisherAccount, timeMap);
|
|
21
|
-
const exfilResult = checkTokenExfil(allFiles || files, pkgJson);
|
|
22
|
-
|
|
23
|
-
let siblingResult = { triggered: false };
|
|
24
|
-
let slsaResult = { triggered: false };
|
|
25
|
-
|
|
26
|
-
if (burstResult.triggered) {
|
|
27
|
-
siblingResult = await checkSiblingCompromise(pkgJson, config);
|
|
28
|
-
slsaResult = await checkSlsaMismatch(pkgName, pkgVersion, burstResult, timeMap, config);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const nxDownstreamResult = checkNxConsoleDownstream(pkgJson, allFiles || files);
|
|
32
|
-
|
|
33
|
-
const triggeredChecks = [];
|
|
34
|
-
if (burstResult.triggered) triggeredChecks.push('D1_BURST');
|
|
35
|
-
if (siblingResult.triggered) triggeredChecks.push('D2_SIBLING');
|
|
36
|
-
if (slsaResult.triggered) triggeredChecks.push('D3_SLSA');
|
|
37
|
-
if (maintainerResult.triggered) triggeredChecks.push('D4_MAINTAINER');
|
|
38
|
-
if (iocResult.triggered) triggeredChecks.push('D5_IOC');
|
|
39
|
-
if (exfilResult.triggered) triggeredChecks.push('D6_EXFIL');
|
|
40
|
-
if (nxDownstreamResult.triggered) triggeredChecks.push('D7_NX_CONSOLE');
|
|
41
|
-
|
|
42
|
-
if (triggeredChecks.length === 0) return [];
|
|
43
|
-
|
|
44
|
-
let waveAttribution = 'unknown';
|
|
45
|
-
if (pkgName.startsWith('@tanstack')) {
|
|
46
|
-
waveAttribution = 'wave1-tanstack';
|
|
47
|
-
} else if (pkgName.startsWith('@antv')) {
|
|
48
|
-
waveAttribution = 'wave2-antv';
|
|
49
|
-
} else if (nxDownstreamResult.triggered) {
|
|
50
|
-
waveAttribution = 'wave3-nx-console';
|
|
51
|
-
} else if (iocResult.matches && iocResult.matches.length > 0) {
|
|
52
|
-
const waves = [...new Set(iocResult.matches.map(m => m.wave))];
|
|
53
|
-
if (waves.length === 1) {
|
|
54
|
-
waveAttribution = waves[0] === 1 ? 'wave1-tanstack' : waves[0] === 2 ? 'wave2-antv' : 'wave3-nx-console';
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const isCritical = slsaResult.triggered || iocResult.triggered || nxDownstreamResult.triggered;
|
|
59
|
-
|
|
60
|
-
const evidence = {
|
|
61
|
-
campaign: 'MINI_SHAI_HULUD',
|
|
62
|
-
waveAttribution,
|
|
63
|
-
triggeredChecks,
|
|
64
|
-
burstWindow: burstResult.triggered
|
|
65
|
-
? { start: burstResult.windowStart, end: burstResult.windowEnd, versionCount: burstResult.versionCount }
|
|
66
|
-
: null,
|
|
67
|
-
siblingPackages: siblingResult.triggered
|
|
68
|
-
? siblingResult.results.flatMap(r => r.siblingPackages)
|
|
69
|
-
: null,
|
|
70
|
-
attestationAnomalies: slsaResult.triggered ? slsaResult.anomalies : null,
|
|
71
|
-
iocMatches: iocResult.triggered ? iocResult.matches : null,
|
|
72
|
-
installScriptSnippets: exfilResult.triggered ? exfilResult.snippets : null,
|
|
73
|
-
nxConsoleDownstream: nxDownstreamResult.triggered
|
|
74
|
-
? { nxDeps: nxDownstreamResult.nxDeps, vsCodeExtensions: nxDownstreamResult.vsCodeExtensions }
|
|
75
|
-
: null,
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
return [{
|
|
79
|
-
id: 'MINI_SHAI_HULUD',
|
|
80
|
-
severity: isCritical ? 'critical' : 'high',
|
|
81
|
-
title: 'Mini Shai-Hulud worm campaign',
|
|
82
|
-
description: `${triggeredChecks.length} signal(s): ${triggeredChecks.join(', ')}`,
|
|
83
|
-
evidence: JSON.stringify(evidence),
|
|
84
|
-
mitigation: 'Revoke all npm tokens immediately. Rotate CI/CD secrets. Audit maintainer access on all scoped packages. Review recent version publish history for anomalous bursts. Check for postinstall scripts accessing credentials or environment variables. If Wave 1 (TanStack scope): inspect GitHub Actions workflow logs for unauthorized build steps. If Wave 2 (atool/AntV scope): rotate all npm tokens associated with @antv/* packages. If Wave 3 (Nx Console): remove nrwl.angular-console extension immediately, revoke all npm tokens used in CI/CD, and audit @nx/* dependency versions.',
|
|
85
|
-
}];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function checkNxConsoleDownstream(pkgJson, allFiles) {
|
|
89
|
-
const deps = { ...pkgJson?.dependencies, ...pkgJson?.devDependencies, ...pkgJson?.peerDependencies };
|
|
90
|
-
const nxDeps = Object.keys(deps).filter(d => d.startsWith('@nx/') || d.startsWith('nrwl/'));
|
|
91
|
-
if (nxDeps.length === 0) return { triggered: false, nxDeps: [], vsCodeExtensions: [] };
|
|
92
|
-
|
|
93
|
-
let vsCodeExtensions = [];
|
|
94
|
-
if (allFiles && Array.isArray(allFiles)) {
|
|
95
|
-
for (const file of allFiles) {
|
|
96
|
-
if (file.path && (file.path.endsWith('.vscode/extensions.json') || file.path.endsWith('.vscode/extensions.json'))) {
|
|
97
|
-
try {
|
|
98
|
-
const content = typeof file.content === 'string' ? file.content : '';
|
|
99
|
-
const parsed = JSON.parse(content);
|
|
100
|
-
const allExts = [
|
|
101
|
-
...(parsed.recommendations || []),
|
|
102
|
-
...(parsed.unwantedRecommendations || []),
|
|
103
|
-
];
|
|
104
|
-
const matched = allExts.filter(e => e.includes('nrwl.angular-console'));
|
|
105
|
-
if (matched.length > 0) {
|
|
106
|
-
vsCodeExtensions = matched;
|
|
107
|
-
}
|
|
108
|
-
} catch {
|
|
109
|
-
// non-JSON extensions.json, skip
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return { triggered: true, nxDeps, vsCodeExtensions };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export { clearSiblingCache } from './d2-sibling-compromise.js';
|
|
1
|
+
import { checkBurstPublish } from './d1-burst-publish.js';
|
|
2
|
+
import { checkSiblingCompromise, clearSiblingCache } from './d2-sibling-compromise.js';
|
|
3
|
+
import { checkSlsaMismatch } from './d3-slsa-mismatch.js';
|
|
4
|
+
import { checkMaintainerAnomaly } from './d4-maintainer-anomaly.js';
|
|
5
|
+
import { checkIOC } from './d5-ioc-check.js';
|
|
6
|
+
import { checkTokenExfil } from './d6-token-exfil.js';
|
|
7
|
+
|
|
8
|
+
export async function scan(pkgJson, files = [], registryMeta = null, allFiles = null) {
|
|
9
|
+
const config = {};
|
|
10
|
+
|
|
11
|
+
const burstResult = await checkBurstPublish(registryMeta, config);
|
|
12
|
+
const maintainerResult = await checkMaintainerAnomaly(registryMeta, config);
|
|
13
|
+
|
|
14
|
+
const pkgName = pkgJson?.name || '';
|
|
15
|
+
const pkgVersion = pkgJson?.version || '';
|
|
16
|
+
const sha512 = registryMeta?.versions?.[pkgVersion]?.dist?.integrity || null;
|
|
17
|
+
const publisherAccount = registryMeta?.versions?.[pkgVersion]?._npmUser?.name || null;
|
|
18
|
+
const timeMap = registryMeta?.time || {};
|
|
19
|
+
|
|
20
|
+
const iocResult = await checkIOC(pkgName, pkgVersion, sha512, publisherAccount, timeMap);
|
|
21
|
+
const exfilResult = checkTokenExfil(allFiles || files, pkgJson);
|
|
22
|
+
|
|
23
|
+
let siblingResult = { triggered: false };
|
|
24
|
+
let slsaResult = { triggered: false };
|
|
25
|
+
|
|
26
|
+
if (burstResult.triggered) {
|
|
27
|
+
siblingResult = await checkSiblingCompromise(pkgJson, config);
|
|
28
|
+
slsaResult = await checkSlsaMismatch(pkgName, pkgVersion, burstResult, timeMap, config);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const nxDownstreamResult = checkNxConsoleDownstream(pkgJson, allFiles || files);
|
|
32
|
+
|
|
33
|
+
const triggeredChecks = [];
|
|
34
|
+
if (burstResult.triggered) triggeredChecks.push('D1_BURST');
|
|
35
|
+
if (siblingResult.triggered) triggeredChecks.push('D2_SIBLING');
|
|
36
|
+
if (slsaResult.triggered) triggeredChecks.push('D3_SLSA');
|
|
37
|
+
if (maintainerResult.triggered) triggeredChecks.push('D4_MAINTAINER');
|
|
38
|
+
if (iocResult.triggered) triggeredChecks.push('D5_IOC');
|
|
39
|
+
if (exfilResult.triggered) triggeredChecks.push('D6_EXFIL');
|
|
40
|
+
if (nxDownstreamResult.triggered) triggeredChecks.push('D7_NX_CONSOLE');
|
|
41
|
+
|
|
42
|
+
if (triggeredChecks.length === 0) return [];
|
|
43
|
+
|
|
44
|
+
let waveAttribution = 'unknown';
|
|
45
|
+
if (pkgName.startsWith('@tanstack')) {
|
|
46
|
+
waveAttribution = 'wave1-tanstack';
|
|
47
|
+
} else if (pkgName.startsWith('@antv')) {
|
|
48
|
+
waveAttribution = 'wave2-antv';
|
|
49
|
+
} else if (nxDownstreamResult.triggered) {
|
|
50
|
+
waveAttribution = 'wave3-nx-console';
|
|
51
|
+
} else if (iocResult.matches && iocResult.matches.length > 0) {
|
|
52
|
+
const waves = [...new Set(iocResult.matches.map(m => m.wave))];
|
|
53
|
+
if (waves.length === 1) {
|
|
54
|
+
waveAttribution = waves[0] === 1 ? 'wave1-tanstack' : waves[0] === 2 ? 'wave2-antv' : 'wave3-nx-console';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const isCritical = slsaResult.triggered || iocResult.triggered || nxDownstreamResult.triggered;
|
|
59
|
+
|
|
60
|
+
const evidence = {
|
|
61
|
+
campaign: 'MINI_SHAI_HULUD',
|
|
62
|
+
waveAttribution,
|
|
63
|
+
triggeredChecks,
|
|
64
|
+
burstWindow: burstResult.triggered
|
|
65
|
+
? { start: burstResult.windowStart, end: burstResult.windowEnd, versionCount: burstResult.versionCount }
|
|
66
|
+
: null,
|
|
67
|
+
siblingPackages: siblingResult.triggered
|
|
68
|
+
? siblingResult.results.flatMap(r => r.siblingPackages)
|
|
69
|
+
: null,
|
|
70
|
+
attestationAnomalies: slsaResult.triggered ? slsaResult.anomalies : null,
|
|
71
|
+
iocMatches: iocResult.triggered ? iocResult.matches : null,
|
|
72
|
+
installScriptSnippets: exfilResult.triggered ? exfilResult.snippets : null,
|
|
73
|
+
nxConsoleDownstream: nxDownstreamResult.triggered
|
|
74
|
+
? { nxDeps: nxDownstreamResult.nxDeps, vsCodeExtensions: nxDownstreamResult.vsCodeExtensions }
|
|
75
|
+
: null,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return [{
|
|
79
|
+
id: 'MINI_SHAI_HULUD',
|
|
80
|
+
severity: isCritical ? 'critical' : 'high',
|
|
81
|
+
title: 'Mini Shai-Hulud worm campaign',
|
|
82
|
+
description: `${triggeredChecks.length} signal(s): ${triggeredChecks.join(', ')}`,
|
|
83
|
+
evidence: JSON.stringify(evidence),
|
|
84
|
+
mitigation: 'Revoke all npm tokens immediately. Rotate CI/CD secrets. Audit maintainer access on all scoped packages. Review recent version publish history for anomalous bursts. Check for postinstall scripts accessing credentials or environment variables. If Wave 1 (TanStack scope): inspect GitHub Actions workflow logs for unauthorized build steps. If Wave 2 (atool/AntV scope): rotate all npm tokens associated with @antv/* packages. If Wave 3 (Nx Console): remove nrwl.angular-console extension immediately, revoke all npm tokens used in CI/CD, and audit @nx/* dependency versions.',
|
|
85
|
+
}];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function checkNxConsoleDownstream(pkgJson, allFiles) {
|
|
89
|
+
const deps = { ...pkgJson?.dependencies, ...pkgJson?.devDependencies, ...pkgJson?.peerDependencies };
|
|
90
|
+
const nxDeps = Object.keys(deps).filter(d => d.startsWith('@nx/') || d.startsWith('nrwl/'));
|
|
91
|
+
if (nxDeps.length === 0) return { triggered: false, nxDeps: [], vsCodeExtensions: [] };
|
|
92
|
+
|
|
93
|
+
let vsCodeExtensions = [];
|
|
94
|
+
if (allFiles && Array.isArray(allFiles)) {
|
|
95
|
+
for (const file of allFiles) {
|
|
96
|
+
if (file.path && (file.path.endsWith('.vscode/extensions.json') || file.path.endsWith('.vscode/extensions.json'))) {
|
|
97
|
+
try {
|
|
98
|
+
const content = typeof file.content === 'string' ? file.content : '';
|
|
99
|
+
const parsed = JSON.parse(content);
|
|
100
|
+
const allExts = [
|
|
101
|
+
...(parsed.recommendations || []),
|
|
102
|
+
...(parsed.unwantedRecommendations || []),
|
|
103
|
+
];
|
|
104
|
+
const matched = allExts.filter(e => e.includes('nrwl.angular-console'));
|
|
105
|
+
if (matched.length > 0) {
|
|
106
|
+
vsCodeExtensions = matched;
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// non-JSON extensions.json, skip
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { triggered: true, nxDeps, vsCodeExtensions };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export { clearSiblingCache } from './d2-sibling-compromise.js';
|
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
{
|
|
2
|
-
"lastUpdated": "2026-05-24T00:00:00.000Z",
|
|
3
|
-
"waves": {
|
|
4
|
-
"wave1": {
|
|
5
|
-
"id": "mini-shai-hulud-wave1",
|
|
6
|
-
"description": "TanStack CI/CD hijack (mid-May 2026) — 84 malicious versions across 42 packages in ~6 minutes via compromised GitHub Actions CI. Forged SLSA BL3 provenance attestations.",
|
|
7
|
-
"windowMinutes": 6,
|
|
8
|
-
"iocs": [
|
|
9
|
-
{
|
|
10
|
-
"type": "packageScope",
|
|
11
|
-
"value": "@tanstack",
|
|
12
|
-
"maliciousVersionRanges": [],
|
|
13
|
-
"notes": "Seed IOC — update from threat intel feed. Affected: @tanstack/router, @tanstack/react-router, @tanstack/query, @tanstack/form, @tanstack/store, @tanstack/virtual, @tanstack/ranger, @tanstack/table."
|
|
14
|
-
}
|
|
15
|
-
]
|
|
16
|
-
},
|
|
17
|
-
"wave2": {
|
|
18
|
-
"id": "mini-shai-hulud-wave2",
|
|
19
|
-
"description": "AntV/atool maintainer account compromise (late May 2026) — 600+ malicious versions across 300+ packages in ~22 minutes. ~16M weekly download blast radius.",
|
|
20
|
-
"windowMinutes": 22,
|
|
21
|
-
"iocs": [
|
|
22
|
-
{
|
|
23
|
-
"type": "publisherAccount",
|
|
24
|
-
"value": "atool",
|
|
25
|
-
"compromiseWindowStart": "2026-05-20T00:00:00.000Z",
|
|
26
|
-
"compromiseWindowEnd": null,
|
|
27
|
-
"notes": "Seed IOC — compromised @antv/atool maintainer account. Update compromise window from threat intel."
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"type": "packageScope",
|
|
31
|
-
"value": "@antv",
|
|
32
|
-
"maliciousVersionRanges": [],
|
|
33
|
-
"notes": "Blast radius: @antv/g2, @antv/g6, @antv/x6, @antv/l7, echarts-for-react, timeago.js. Seed IOC — update from threat intel."
|
|
34
|
-
}
|
|
35
|
-
]
|
|
36
|
-
},
|
|
37
|
-
"wave3": {
|
|
38
|
-
"id": "nx-console-wave3",
|
|
39
|
-
"description": "Nx Console 18.95.0 VS Code extension compromise (May 18, 2026, CVE-2026-48027, TeamPCP) — contributor token stolen via TanStack wave1 (May 11), 7-day dwell, malicious extension published using npx to fetch 498KB obfuscated Bun payload from dangling orphan commit on nrwl/nx repo. ~3M installs exposed.",
|
|
40
|
-
"windowMinutes": 36,
|
|
41
|
-
"iocs": [
|
|
42
|
-
{
|
|
43
|
-
"type": "extensionId",
|
|
44
|
-
"value": "nrwl.angular-console",
|
|
45
|
-
"maliciousVersionRanges": ["18.95.0"],
|
|
46
|
-
"notes": "Nx Console v18.95.0 — malicious VS Code extension. CVE-2026-48027. Exposure window: 11 min on Marketplace, 36 min on Open VSX."
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"type": "publisherAccount",
|
|
50
|
-
"value": "nrwl",
|
|
51
|
-
"compromiseWindowStart": "2026-05-11T00:00:00.000Z",
|
|
52
|
-
"compromiseWindowEnd": "2026-05-18T13:09:00.000Z",
|
|
53
|
-
"notes": "Nx contributor token stolen via TanStack wave1 on May 11; 7-day dwell before publishing malicious extension on May 18."
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
"type": "packageScope",
|
|
57
|
-
"value": "@nx",
|
|
58
|
-
"maliciousVersionRanges": [],
|
|
59
|
-
"notes": "NX_CONSOLE_DOWNSTREAM: npm packages under @nx scope deployed by compromised Nx contributor. Check for versions published within 7 days of 2026-05-18."
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
"type": "packageScope",
|
|
63
|
-
"value": "nrwl",
|
|
64
|
-
"maliciousVersionRanges": [],
|
|
65
|
-
"notes": "NX_CONSOLE_DOWNSTREAM: nrwl-scoped npm packages — monitor for anomalous burst publishing."
|
|
66
|
-
}
|
|
67
|
-
]
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
"iocs": [
|
|
71
|
-
{
|
|
72
|
-
"type": "sha512",
|
|
73
|
-
"value": "sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
|
74
|
-
"package": "@antv/g2",
|
|
75
|
-
"wave": 2,
|
|
76
|
-
"notes": "Placeholder sha512 — replace with actual SHA-512 integrity hash from npm dist.integrity of a confirmed malicious version."
|
|
77
|
-
}
|
|
78
|
-
]
|
|
79
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"lastUpdated": "2026-05-24T00:00:00.000Z",
|
|
3
|
+
"waves": {
|
|
4
|
+
"wave1": {
|
|
5
|
+
"id": "mini-shai-hulud-wave1",
|
|
6
|
+
"description": "TanStack CI/CD hijack (mid-May 2026) — 84 malicious versions across 42 packages in ~6 minutes via compromised GitHub Actions CI. Forged SLSA BL3 provenance attestations.",
|
|
7
|
+
"windowMinutes": 6,
|
|
8
|
+
"iocs": [
|
|
9
|
+
{
|
|
10
|
+
"type": "packageScope",
|
|
11
|
+
"value": "@tanstack",
|
|
12
|
+
"maliciousVersionRanges": [],
|
|
13
|
+
"notes": "Seed IOC — update from threat intel feed. Affected: @tanstack/router, @tanstack/react-router, @tanstack/query, @tanstack/form, @tanstack/store, @tanstack/virtual, @tanstack/ranger, @tanstack/table."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"wave2": {
|
|
18
|
+
"id": "mini-shai-hulud-wave2",
|
|
19
|
+
"description": "AntV/atool maintainer account compromise (late May 2026) — 600+ malicious versions across 300+ packages in ~22 minutes. ~16M weekly download blast radius.",
|
|
20
|
+
"windowMinutes": 22,
|
|
21
|
+
"iocs": [
|
|
22
|
+
{
|
|
23
|
+
"type": "publisherAccount",
|
|
24
|
+
"value": "atool",
|
|
25
|
+
"compromiseWindowStart": "2026-05-20T00:00:00.000Z",
|
|
26
|
+
"compromiseWindowEnd": null,
|
|
27
|
+
"notes": "Seed IOC — compromised @antv/atool maintainer account. Update compromise window from threat intel."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"type": "packageScope",
|
|
31
|
+
"value": "@antv",
|
|
32
|
+
"maliciousVersionRanges": [],
|
|
33
|
+
"notes": "Blast radius: @antv/g2, @antv/g6, @antv/x6, @antv/l7, echarts-for-react, timeago.js. Seed IOC — update from threat intel."
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"wave3": {
|
|
38
|
+
"id": "nx-console-wave3",
|
|
39
|
+
"description": "Nx Console 18.95.0 VS Code extension compromise (May 18, 2026, CVE-2026-48027, TeamPCP) — contributor token stolen via TanStack wave1 (May 11), 7-day dwell, malicious extension published using npx to fetch 498KB obfuscated Bun payload from dangling orphan commit on nrwl/nx repo. ~3M installs exposed.",
|
|
40
|
+
"windowMinutes": 36,
|
|
41
|
+
"iocs": [
|
|
42
|
+
{
|
|
43
|
+
"type": "extensionId",
|
|
44
|
+
"value": "nrwl.angular-console",
|
|
45
|
+
"maliciousVersionRanges": ["18.95.0"],
|
|
46
|
+
"notes": "Nx Console v18.95.0 — malicious VS Code extension. CVE-2026-48027. Exposure window: 11 min on Marketplace, 36 min on Open VSX."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"type": "publisherAccount",
|
|
50
|
+
"value": "nrwl",
|
|
51
|
+
"compromiseWindowStart": "2026-05-11T00:00:00.000Z",
|
|
52
|
+
"compromiseWindowEnd": "2026-05-18T13:09:00.000Z",
|
|
53
|
+
"notes": "Nx contributor token stolen via TanStack wave1 on May 11; 7-day dwell before publishing malicious extension on May 18."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"type": "packageScope",
|
|
57
|
+
"value": "@nx",
|
|
58
|
+
"maliciousVersionRanges": [],
|
|
59
|
+
"notes": "NX_CONSOLE_DOWNSTREAM: npm packages under @nx scope deployed by compromised Nx contributor. Check for versions published within 7 days of 2026-05-18."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"type": "packageScope",
|
|
63
|
+
"value": "nrwl",
|
|
64
|
+
"maliciousVersionRanges": [],
|
|
65
|
+
"notes": "NX_CONSOLE_DOWNSTREAM: nrwl-scoped npm packages — monitor for anomalous burst publishing."
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"iocs": [
|
|
71
|
+
{
|
|
72
|
+
"type": "sha512",
|
|
73
|
+
"value": "sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
|
74
|
+
"package": "@antv/g2",
|
|
75
|
+
"wave": 2,
|
|
76
|
+
"notes": "Placeholder sha512 — replace with actual SHA-512 integrity hash from npm dist.integrity of a confirmed malicious version."
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
@@ -45,6 +45,23 @@ function isKnownBinaryName(fileName) {
|
|
|
45
45
|
return BINARY_FILENAMES.includes(base);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
const CROSS_PLATFORM_RE = /-(?:linux|darwin|macos|win32|windows|win)-(?:x64|x86|arm64|ia32)\.?(?:exe)?$/i;
|
|
49
|
+
|
|
50
|
+
function detectCrossPlatformSets(binaries) {
|
|
51
|
+
const sets = {};
|
|
52
|
+
for (const bin of binaries) {
|
|
53
|
+
const base = bin.file.replace(CROSS_PLATFORM_RE, '').split(/[/\\]/).pop();
|
|
54
|
+
if (!sets[base]) sets[base] = [];
|
|
55
|
+
sets[base].push(bin.file);
|
|
56
|
+
}
|
|
57
|
+
for (const [base, files] of Object.entries(sets)) {
|
|
58
|
+
if (files.length >= 2) {
|
|
59
|
+
return { base, files, count: files.length };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
function isDeclared(pkgJson, fileName) {
|
|
49
66
|
if (!pkgJson) return false;
|
|
50
67
|
const baseName = fileName.split(/[/\\]/).pop();
|
|
@@ -113,6 +130,8 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
113
130
|
|
|
114
131
|
if (binaries.length === 0) return [];
|
|
115
132
|
|
|
133
|
+
const crossPlatformSet = detectCrossPlatformSets(binaries);
|
|
134
|
+
|
|
116
135
|
const jsCode = (jsFiles || []).map(f => f.content || '').join('\n');
|
|
117
136
|
const invoked = CHILD_PROC_RE.test(jsCode) || FS_CHMOD_RE.test(jsCode);
|
|
118
137
|
|
|
@@ -134,25 +153,30 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
134
153
|
let baseScore;
|
|
135
154
|
let subtype;
|
|
136
155
|
|
|
156
|
+
// Cross-platform platform set boost
|
|
157
|
+
const isCrossPlatform = crossPlatformSet && crossPlatformSet.files.some(f => f === bin.file || f.includes(bin.file) || bin.file.includes(f.replace(/\.exe$/, '')));
|
|
158
|
+
|
|
137
159
|
if (bin.magic === 'elf_embedded') {
|
|
138
160
|
baseScore = 95;
|
|
139
|
-
subtype = 'elf_embedded';
|
|
161
|
+
subtype = isCrossPlatform ? 'cross_platform_elf' : 'elf_embedded';
|
|
140
162
|
} else if (bin.magic === 'pe_embedded') {
|
|
141
163
|
baseScore = 95;
|
|
142
|
-
subtype = 'pe_embedded';
|
|
164
|
+
subtype = isCrossPlatform ? 'cross_platform_pe' : 'pe_embedded';
|
|
143
165
|
} else if (bin.magic === 'macho_embedded') {
|
|
144
166
|
baseScore = 95;
|
|
145
|
-
subtype = 'macho_embedded';
|
|
167
|
+
subtype = isCrossPlatform ? 'cross_platform_macho' : 'macho_embedded';
|
|
146
168
|
} else if (bin.magic === 'wasm_embedded') {
|
|
147
169
|
baseScore = 60;
|
|
148
|
-
subtype = 'wasm_embedded';
|
|
170
|
+
subtype = isCrossPlatform ? 'cross_platform_wasm' : 'wasm_embedded';
|
|
149
171
|
} else {
|
|
150
172
|
baseScore = 60;
|
|
151
|
-
subtype = 'magic_byte_unknown';
|
|
173
|
+
subtype = isCrossPlatform ? 'cross_platform_unknown' : 'magic_byte_unknown';
|
|
152
174
|
}
|
|
153
175
|
|
|
154
176
|
let score = baseScore;
|
|
155
177
|
|
|
178
|
+
if (isCrossPlatform) score += 25;
|
|
179
|
+
|
|
156
180
|
if (bin.inBinDir) score += 15;
|
|
157
181
|
|
|
158
182
|
if (!bin.declared) score += 50;
|
|
@@ -179,6 +203,11 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
179
203
|
`path: ${bin.file}`,
|
|
180
204
|
`declared: ${bin.declared}`,
|
|
181
205
|
];
|
|
206
|
+
if (isCrossPlatform) {
|
|
207
|
+
evidence.push(`cross-platform binary set: ${crossPlatformSet.count} variants of "${crossPlatformSet.base}"`);
|
|
208
|
+
evidence.push(`platform_files: ${crossPlatformSet.files.join(', ')}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
182
211
|
if (invoked && invokedFiles.length > 0) {
|
|
183
212
|
evidence.push(`invoked: child_process usage in ${invokedFiles.length} file(s)`);
|
|
184
213
|
evidence.push(`invoked_file: ${invokedFiles[0]}`);
|