@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.
Files changed (113) hide show
  1. package/CHANGELOG.md +265 -233
  2. package/LICENSING.md +19 -19
  3. package/README.de.md +708 -708
  4. package/README.fr.md +707 -707
  5. package/README.ja.md +704 -704
  6. package/README.md +861 -826
  7. package/README.zh.md +708 -708
  8. package/VALIDATION.md +92 -0
  9. package/backend/cra.js +68 -68
  10. package/backend/db/pg-schema.sql +155 -0
  11. package/backend/db/schema.sql +32 -32
  12. package/backend/db.js +88 -88
  13. package/backend/detectors/atk-001-lifecycle.js +17 -17
  14. package/backend/detectors/atk-002-obfusc.js +261 -261
  15. package/backend/detectors/atk-003-creds.js +13 -13
  16. package/backend/detectors/atk-004-persist.js +13 -13
  17. package/backend/detectors/atk-005-exfil.js +13 -13
  18. package/backend/detectors/atk-006-depconf.js +14 -14
  19. package/backend/detectors/atk-007-typosquat.js +34 -34
  20. package/backend/detectors/atk-008-tarball-tamper.js +91 -91
  21. package/backend/detectors/atk-009-dormant-trigger.js +62 -62
  22. package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
  23. package/backend/detectors/atk-011-transitive-prop.js +76 -76
  24. package/backend/detectors/config/thresholds.js +66 -0
  25. package/backend/detectors/config/whitelist.json +74 -0
  26. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
  27. package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
  28. package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
  29. package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
  30. package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
  31. package/backend/detectors/hf-impersonation/index.js +396 -396
  32. package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
  33. package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
  34. package/backend/detectors/hf-impersonation/simhash.js +46 -46
  35. package/backend/detectors/index.js +87 -81
  36. package/backend/detectors/lib/ast-patterns.js +21 -0
  37. package/backend/detectors/lib/entropy-analyzer.js +24 -0
  38. package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
  39. package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
  40. package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
  41. package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
  42. package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
  43. package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
  44. package/backend/detectors/megalodon/index.js +80 -80
  45. package/backend/detectors/megalodon/types.js +9 -9
  46. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
  47. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
  48. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
  49. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
  50. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
  51. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
  52. package/backend/detectors/mini-shai-hulud/index.js +118 -118
  53. package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
  54. package/backend/detectors/tier1-binary-embed.js +34 -5
  55. package/backend/detectors/tier1-obfuscation-heuristics.js +156 -0
  56. package/backend/detectors/tier1-slsa-attestation.js +12 -0
  57. package/backend/detectors/tier1-version-anomaly.js +187 -0
  58. package/backend/detectors.test.js +88 -0
  59. package/backend/fetch.js +175 -175
  60. package/backend/index.js +4 -4
  61. package/backend/license.js +89 -89
  62. package/backend/lockfile.js +379 -379
  63. package/backend/pdf.js +245 -245
  64. package/backend/policy.js +193 -193
  65. package/backend/report.js +254 -254
  66. package/backend/sbom.js +66 -66
  67. package/backend/scripts/analyze-false-positives.js +146 -0
  68. package/backend/scripts/analyze-validation.js +151 -0
  69. package/backend/scripts/detect-false-positives.js +93 -0
  70. package/backend/scripts/fetch-top-packages.js +129 -0
  71. package/backend/scripts/validate-detectors.js +142 -0
  72. package/backend/siem/cef.js +32 -32
  73. package/backend/siem/ecs.js +40 -40
  74. package/backend/siem/index.js +18 -18
  75. package/backend/siem/qradar.js +56 -56
  76. package/backend/siem/sentinel.js +27 -27
  77. package/backend/tests-d5-enhanced.test.js +46 -0
  78. package/backend/tests-d6-version-anomaly.test.js +58 -0
  79. package/backend/tests-d6.test.js +116 -0
  80. package/backend/tests-d6c.test.js +106 -0
  81. package/backend/tests-d7-obfuscation.test.js +91 -0
  82. package/backend/tests.test.js +898 -0
  83. package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
  84. package/backend/vsix-scan/detectors/burst-publish.js +52 -52
  85. package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
  86. package/backend/vsix-scan/detectors/known-ioc.js +105 -105
  87. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
  88. package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
  89. package/backend/vsix-scan/index.js +183 -183
  90. package/backend/vsix-scan/marketplace-client.js +145 -145
  91. package/backend/vsix-scan/vsix-iocs.json +31 -31
  92. package/cli/cli.js +458 -458
  93. package/package.json +74 -57
  94. package/.dockerignore +0 -20
  95. package/.husky/pre-commit +0 -1
  96. package/SECURITY.md +0 -73
  97. package/deploy/helm/npm-scan/Chart.yaml +0 -22
  98. package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
  99. package/deploy/helm/npm-scan/templates/api.yaml +0 -94
  100. package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
  101. package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
  102. package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
  103. package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
  104. package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
  105. package/deploy/helm/npm-scan/values.yaml +0 -103
  106. package/scripts/download-corpus.js +0 -30
  107. package/scripts/gen-mal-corpus.js +0 -35
  108. package/scripts/generate-campaign-fixtures.js +0 -170
  109. package/src/config/top-5000.json +0 -87
  110. package/test/fixtures/lockfiles/npm-lock.json +0 -69
  111. package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
  112. package/test/fixtures/lockfiles/yarn.lock +0 -104
  113. 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]}`);