@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.
Files changed (149) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +864 -826
  3. package/VALIDATION.md +92 -0
  4. package/backend/cra.js +113 -21
  5. package/backend/db/pg-schema.sql +155 -0
  6. package/backend/db.js +18 -10
  7. package/backend/detectors/atk-001-lifecycle.js +5 -5
  8. package/backend/detectors/atk-002-obfusc.js +126 -47
  9. package/backend/detectors/atk-003-creds.js +8 -4
  10. package/backend/detectors/atk-004-persist.js +3 -3
  11. package/backend/detectors/atk-005-exfil.js +8 -4
  12. package/backend/detectors/atk-006-depconf.js +3 -3
  13. package/backend/detectors/atk-007-typosquat.js +64 -10
  14. package/backend/detectors/atk-008-tarball-tamper.js +6 -6
  15. package/backend/detectors/atk-009-dormant-trigger.js +9 -5
  16. package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
  17. package/backend/detectors/atk-011-transitive-prop.js +14 -13
  18. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
  19. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
  20. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
  21. package/backend/detectors/axios-poisoning/index.js +77 -60
  22. package/backend/detectors/config/thresholds.js +111 -0
  23. package/backend/detectors/config/whitelist.json +74 -0
  24. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
  25. package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
  26. package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
  27. package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
  28. package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
  29. package/backend/detectors/hf-impersonation/index.js +94 -31
  30. package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
  31. package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
  32. package/backend/detectors/hf-impersonation/simhash.js +2 -2
  33. package/backend/detectors/index.js +184 -31
  34. package/backend/detectors/lib/ast-patterns.js +24 -0
  35. package/backend/detectors/lib/entropy-analyzer.js +32 -0
  36. package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
  37. package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
  38. package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
  39. package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
  40. package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
  41. package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
  42. package/backend/detectors/megalodon/index.js +35 -25
  43. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
  44. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
  45. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
  46. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
  47. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
  48. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
  49. package/backend/detectors/mini-shai-hulud/index.js +63 -26
  50. package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
  51. package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
  52. package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
  53. package/backend/detectors/msh-supplement/index.js +78 -63
  54. package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
  55. package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
  56. package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
  57. package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
  58. package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
  59. package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
  60. package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
  61. package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
  62. package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
  63. package/backend/detectors/node-ipc-compromise/index.js +21 -15
  64. package/backend/detectors/tier1-binary-embed.js +138 -41
  65. package/backend/detectors/tier1-cloud-imds.js +57 -37
  66. package/backend/detectors/tier1-encrypted-c2.js +198 -0
  67. package/backend/detectors/tier1-infostealer.js +121 -68
  68. package/backend/detectors/tier1-lifecycle-hook.js +63 -23
  69. package/backend/detectors/tier1-maintainer-compromise.js +157 -0
  70. package/backend/detectors/tier1-metadata-spoof.js +92 -42
  71. package/backend/detectors/tier1-multistage-postinstall.js +46 -19
  72. package/backend/detectors/tier1-obfuscation-heuristics.js +184 -0
  73. package/backend/detectors/tier1-self-propagation.js +115 -0
  74. package/backend/detectors/tier1-slsa-attestation.js +12 -0
  75. package/backend/detectors/tier1-transitive-deps.js +182 -0
  76. package/backend/detectors/tier1-typosquat.js +129 -50
  77. package/backend/detectors/tier1-version-anomaly.js +223 -0
  78. package/backend/detectors/tier1-version-confusion.js +79 -59
  79. package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
  80. package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
  81. package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
  82. package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
  83. package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
  84. package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
  85. package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
  86. package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
  87. package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
  88. package/backend/detectors/trapdoor/index.js +19 -14
  89. package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
  90. package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
  91. package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
  92. package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
  93. package/backend/detectors.test.js +147 -0
  94. package/backend/fetch.js +37 -29
  95. package/backend/index.js +1 -1
  96. package/backend/license.js +20 -4
  97. package/backend/lockfile.js +60 -36
  98. package/backend/pdf.js +107 -28
  99. package/backend/policy.js +183 -56
  100. package/backend/provenance.js +28 -3
  101. package/backend/report.js +136 -70
  102. package/backend/sbom.js +33 -27
  103. package/backend/scripts/analyze-false-positives.js +152 -0
  104. package/backend/scripts/analyze-validation.js +157 -0
  105. package/backend/scripts/detect-false-positives.js +103 -0
  106. package/backend/scripts/fetch-top-packages.js +277 -0
  107. package/backend/scripts/validate-d10-d13.js +103 -0
  108. package/backend/scripts/validate-detectors.js +151 -0
  109. package/backend/siem/cef.js +23 -21
  110. package/backend/siem/ecs.js +3 -3
  111. package/backend/siem/index.js +1 -1
  112. package/backend/siem/qradar.js +3 -3
  113. package/backend/siem/sentinel.js +2 -2
  114. package/backend/tests-d5-enhanced.test.js +47 -0
  115. package/backend/tests-d6-version-anomaly.test.js +67 -0
  116. package/backend/tests-d6.test.js +126 -0
  117. package/backend/tests-d6c.test.js +119 -0
  118. package/backend/tests-d7-obfuscation.test.js +88 -0
  119. package/backend/tests.test.js +997 -0
  120. package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
  121. package/backend/vsix-scan/detectors/burst-publish.js +14 -7
  122. package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
  123. package/backend/vsix-scan/detectors/known-ioc.js +23 -8
  124. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
  125. package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
  126. package/backend/vsix-scan/index.js +97 -41
  127. package/backend/vsix-scan/marketplace-client.js +29 -13
  128. package/cli/cli.js +154 -64
  129. package/package.json +36 -10
  130. package/.dockerignore +0 -20
  131. package/.husky/pre-commit +0 -1
  132. package/SECURITY.md +0 -73
  133. package/deploy/helm/npm-scan/Chart.yaml +0 -22
  134. package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
  135. package/deploy/helm/npm-scan/templates/api.yaml +0 -94
  136. package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
  137. package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
  138. package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
  139. package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
  140. package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
  141. package/deploy/helm/npm-scan/values.yaml +0 -103
  142. package/scripts/download-corpus.js +0 -30
  143. package/scripts/gen-mal-corpus.js +0 -35
  144. package/scripts/generate-campaign-fixtures.js +0 -170
  145. package/src/config/top-5000.json +0 -87
  146. package/test/fixtures/lockfiles/npm-lock.json +0 -69
  147. package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
  148. package/test/fixtures/lockfiles/yarn.lock +0 -104
  149. 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 mjsPath = null;
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
- mjsPath = path;
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: 'node-ipc.cjs ends with IIFE pattern — potential obfuscated payload appended after module closure',
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: 'node-ipc.cjs ends with IIFE pattern — potential obfuscated payload appended after module closure',
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 IIFE_BOUNDARY = /}\)\(\);\s*$/;
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')) continue;
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')) continue;
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) continue;
27
+ if (!hasDnsResolver) {
28
+ continue;
29
+ }
31
30
 
32
31
  const ipMatch = content.match(IP_PATTERN);
33
- if (!ipMatch) continue;
32
+ if (!ipMatch) {
33
+ continue;
34
+ }
34
35
 
35
36
  const customIP = ipMatch[1];
36
- if (PUBLIC_RESOLVERS.has(customIP)) continue;
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')) continue;
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')) continue;
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: 'setImmediate() call found — node-ipc malware fires at require() time, not via postinstall',
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)) return 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('Review DNS egress logs for sh.azurestaticprovider.net and 37.16.75.69 post May 14, 2026');
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) return [];
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
- id: 'NODE_IPC_COMPROMISE',
87
- severity,
88
- title: 'node-ipc supply chain compromise (May 14, 2026)',
89
- description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
90
- evidence: JSON.stringify(evidence),
91
- mitigation: buildRemediation(triggered),
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) return null;
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') return 'elf_embedded';
19
- if (c0 === 0x4d && c1 === 0x5a) return 'pe_embedded';
20
- if (c0 === 0x00 && content.slice(1, 4) === 'asm') return 'wasm_embedded';
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 = (c0 === 0xfe && c1 === 0xed && c2 === 0xfa && (c3 === 0xce || c3 === 0xcf)) ||
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) return 'macho_embedded';
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) return 'macho_embedded';
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) return false;
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) return true;
54
- if (typeof pkgJson.bin === 'object' && Object.values(pkgJson.bin).some(v => v === baseName || v.endsWith(`/${baseName}`))) return true;
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, val] of Object.entries(pkgJson.optionalDependencies)) {
59
- if (name === baseName) return true;
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 (pkgJson.gypfile === true || pkgJson.scripts?.install?.includes('node-gyp') || pkgJson.scripts?.install?.includes('node-pre-gyp')) {
64
- if (baseName.endsWith('.node')) return true;
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)) return [];
124
+ if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
125
+ return [];
126
+ }
75
127
 
76
- if (!allFiles || allFiles.length === 0) return [];
128
+ if (!allFiles || allFiles.length === 0) {
129
+ return [];
130
+ }
77
131
 
78
- if (pkgName && (
79
- pkgName === 'electron' || pkgName === 'puppeteer' || pkgName === 'sharp' ||
80
- pkgName === 'esbuild' || pkgName === 'node-gyp' || pkgName === 'node-pre-gyp' ||
81
- pkgName === '@mapbox/node-pre-gyp'
82
- )) return [];
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) return [];
175
+ if (binaries.length === 0) {
176
+ return [];
177
+ }
115
178
 
116
- const jsCode = (jsFiles || []).map(f => f.content || '').join('\n');
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 (bin.inBinDir) score += 15;
228
+ if (isCrossPlatform) {
229
+ score += 25;
230
+ }
231
+
232
+ if (bin.inBinDir) {
233
+ score += 15;
234
+ }
157
235
 
158
- if (!bin.declared) score += 50;
236
+ if (!bin.declared) {
237
+ score += 50;
238
+ }
159
239
 
160
- if (invoked && invokedFiles.length > 0) score += 25;
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) return 'critical';
166
- if (sc >= 70) return 'high';
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) return 'CRITICAL';
172
- if (sc >= 80) return 'HIGH';
173
- if (sc >= 60) return 'MEDIUM';
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) return 'high';
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) return 'HIGH';
22
- if (score >= 60) return 'MEDIUM';
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)) continue;
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])) return true;
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, registryMeta, allFiles) {
82
+ export async function scan(pkgJson, jsFiles, _registryMeta, _allFiles) {
76
83
  const texts = collectTexts(pkgJson, jsFiles);
77
- if (texts.length === 0) return [];
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)) hasGcp = true;
84
- if (!hasAzure && hasAzurePattern(text)) hasAzure = true;
85
- if (hasGcp && hasAzure) break;
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) return [];
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
- detector: 'tier1-cloud-imds',
106
- id: 'TIER1-CLOUD-IMDS',
107
- severity: severityLabel(confidenceScore),
108
- confidence: confidenceLabel(confidenceScore),
109
- confidenceScore,
110
- subtype,
111
- message: hasGcp && hasAzure
112
- ? `Package references both GCP metadata and Azure IMDS endpoints — cloud credential harvesting`
113
- : hasGcp
114
- ? `Package references GCP metadata server endpoint — cloud credential harvesting`
115
- : `Package references Azure IMDS endpoint — cloud credential harvesting`,
116
- evidence: [
117
- ...(hasGcp ? ['gcp: metadata.google.internal / computeMetadata/v1 pattern detected'] : []),
118
- ...(hasAzure ? ['azure: 169.254.169.254/metadata pattern detected'] : []),
119
- ],
120
- crossFiles: [],
121
- locations: [{ file: '', line: 0 }],
122
- reference: 'Miasma Cloud IMDS',
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
  }