@lateos/npm-scan 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.de.md +3 -98
  2. package/README.fr.md +3 -98
  3. package/README.ja.md +3 -98
  4. package/README.md +2 -122
  5. package/README.zh.md +3 -98
  6. package/backend/cra.js +113 -21
  7. package/backend/db.js +18 -10
  8. package/backend/detectors/atk-001-lifecycle.js +5 -5
  9. package/backend/detectors/atk-002-obfusc.js +126 -47
  10. package/backend/detectors/atk-003-creds.js +8 -4
  11. package/backend/detectors/atk-004-persist.js +3 -3
  12. package/backend/detectors/atk-005-exfil.js +8 -4
  13. package/backend/detectors/atk-006-depconf.js +3 -3
  14. package/backend/detectors/atk-007-typosquat.js +64 -10
  15. package/backend/detectors/atk-008-tarball-tamper.js +6 -6
  16. package/backend/detectors/atk-009-dormant-trigger.js +9 -5
  17. package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
  18. package/backend/detectors/atk-011-transitive-prop.js +14 -13
  19. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
  20. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
  21. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
  22. package/backend/detectors/axios-poisoning/index.js +77 -60
  23. package/backend/detectors/config/thresholds.js +48 -3
  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 +181 -34
  34. package/backend/detectors/lib/ast-patterns.js +4 -1
  35. package/backend/detectors/lib/entropy-analyzer.js +12 -4
  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 +109 -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 +45 -17
  73. package/backend/detectors/tier1-self-propagation.js +115 -0
  74. package/backend/detectors/tier1-slsa-attestation.js +1 -1
  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 +77 -41
  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 +78 -19
  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 +14 -8
  104. package/backend/scripts/analyze-validation.js +27 -21
  105. package/backend/scripts/detect-false-positives.js +20 -10
  106. package/backend/scripts/fetch-top-packages.js +197 -49
  107. package/backend/scripts/validate-d10-d13.js +103 -0
  108. package/backend/scripts/validate-detectors.js +26 -17
  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 +13 -12
  115. package/backend/tests-d6-version-anomaly.test.js +17 -8
  116. package/backend/tests-d6.test.js +24 -14
  117. package/backend/tests-d6c.test.js +27 -14
  118. package/backend/tests-d7-obfuscation.test.js +9 -12
  119. package/backend/tests.test.js +182 -83
  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 +12 -3
@@ -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,13 +58,16 @@ function isKnownBinaryName(fileName) {
45
58
  return BINARY_FILENAMES.includes(base);
46
59
  }
47
60
 
48
- const CROSS_PLATFORM_RE = /-(?:linux|darwin|macos|win32|windows|win)-(?:x64|x86|arm64|ia32)\.?(?:exe)?$/i;
61
+ const CROSS_PLATFORM_RE =
62
+ /-(?:linux|darwin|macos|win32|windows|win)-(?:x64|x86|arm64|ia32)\.?(?:exe)?$/i;
49
63
 
50
64
  function detectCrossPlatformSets(binaries) {
51
65
  const sets = {};
52
66
  for (const bin of binaries) {
53
67
  const base = bin.file.replace(CROSS_PLATFORM_RE, '').split(/[/\\]/).pop();
54
- if (!sets[base]) sets[base] = [];
68
+ if (!sets[base]) {
69
+ sets[base] = [];
70
+ }
55
71
  sets[base].push(bin.file);
56
72
  }
57
73
  for (const [base, files] of Object.entries(sets)) {
@@ -63,22 +79,39 @@ function detectCrossPlatformSets(binaries) {
63
79
  }
64
80
 
65
81
  function isDeclared(pkgJson, fileName) {
66
- if (!pkgJson) return false;
82
+ if (!pkgJson) {
83
+ return false;
84
+ }
67
85
  const baseName = fileName.split(/[/\\]/).pop();
68
86
 
69
87
  if (pkgJson.bin) {
70
- if (typeof pkgJson.bin === 'string' && pkgJson.bin === baseName) return true;
71
- 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
+ }
72
97
  }
73
98
 
74
99
  if (pkgJson.optionalDependencies) {
75
- for (const [name, val] of Object.entries(pkgJson.optionalDependencies)) {
76
- if (name === baseName) return true;
100
+ for (const [name, _val] of Object.entries(pkgJson.optionalDependencies)) {
101
+ if (name === baseName) {
102
+ return true;
103
+ }
77
104
  }
78
105
  }
79
106
 
80
- if (pkgJson.gypfile === true || pkgJson.scripts?.install?.includes('node-gyp') || pkgJson.scripts?.install?.includes('node-pre-gyp')) {
81
- 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
+ }
82
115
  }
83
116
 
84
117
  return false;
@@ -88,15 +121,26 @@ export const name = 'tier1-binary-embed';
88
121
 
89
122
  export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
90
123
  const pkgName = pkgJson?.name;
91
- if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
124
+ if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
125
+ return [];
126
+ }
92
127
 
93
- if (!allFiles || allFiles.length === 0) return [];
128
+ if (!allFiles || allFiles.length === 0) {
129
+ return [];
130
+ }
94
131
 
95
- if (pkgName && (
96
- pkgName === 'electron' || pkgName === 'puppeteer' || pkgName === 'sharp' ||
97
- pkgName === 'esbuild' || pkgName === 'node-gyp' || pkgName === 'node-pre-gyp' ||
98
- pkgName === '@mapbox/node-pre-gyp'
99
- )) 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
+ }
100
144
 
101
145
  const binaries = [];
102
146
 
@@ -128,11 +172,13 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
128
172
  }
129
173
  }
130
174
 
131
- if (binaries.length === 0) return [];
175
+ if (binaries.length === 0) {
176
+ return [];
177
+ }
132
178
 
133
179
  const crossPlatformSet = detectCrossPlatformSets(binaries);
134
180
 
135
- const jsCode = (jsFiles || []).map(f => f.content || '').join('\n');
181
+ const jsCode = (jsFiles || []).map((f) => f.content || '').join('\n');
136
182
  const invoked = CHILD_PROC_RE.test(jsCode) || FS_CHMOD_RE.test(jsCode);
137
183
 
138
184
  const invokedFiles = [];
@@ -154,7 +200,11 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
154
200
  let subtype;
155
201
 
156
202
  // 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$/, '')));
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
+ );
158
208
 
159
209
  if (bin.magic === 'elf_embedded') {
160
210
  baseScore = 95;
@@ -175,26 +225,44 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
175
225
 
176
226
  let score = baseScore;
177
227
 
178
- if (isCrossPlatform) score += 25;
228
+ if (isCrossPlatform) {
229
+ score += 25;
230
+ }
179
231
 
180
- if (bin.inBinDir) score += 15;
232
+ if (bin.inBinDir) {
233
+ score += 15;
234
+ }
181
235
 
182
- if (!bin.declared) score += 50;
236
+ if (!bin.declared) {
237
+ score += 50;
238
+ }
183
239
 
184
- if (invoked && invokedFiles.length > 0) score += 25;
240
+ if (invoked && invokedFiles.length > 0) {
241
+ score += 25;
242
+ }
185
243
 
186
244
  const confidenceScore = Math.max(50, Math.min(100, score));
187
245
 
188
246
  function severityLabel(sc) {
189
- if (sc >= 90) return 'critical';
190
- if (sc >= 70) return 'high';
247
+ if (sc >= 90) {
248
+ return 'critical';
249
+ }
250
+ if (sc >= 70) {
251
+ return 'high';
252
+ }
191
253
  return 'medium';
192
254
  }
193
255
 
194
256
  function confidenceLabel(sc) {
195
- if (sc >= 95) return 'CRITICAL';
196
- if (sc >= 80) return 'HIGH';
197
- 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
+ }
198
266
  return 'LOW';
199
267
  }
200
268
 
@@ -204,7 +272,9 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
204
272
  `declared: ${bin.declared}`,
205
273
  ];
206
274
  if (isCrossPlatform) {
207
- evidence.push(`cross-platform binary set: ${crossPlatformSet.count} variants of "${crossPlatformSet.base}"`);
275
+ evidence.push(
276
+ `cross-platform binary set: ${crossPlatformSet.count} variants of "${crossPlatformSet.base}"`
277
+ );
208
278
  evidence.push(`platform_files: ${crossPlatformSet.files.join(', ')}`);
209
279
  }
210
280
 
@@ -213,9 +283,7 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
213
283
  evidence.push(`invoked_file: ${invokedFiles[0]}`);
214
284
  }
215
285
 
216
- const locations = [
217
- { file: bin.file, size: bin.size },
218
- ];
286
+ const locations = [{ file: bin.file, size: bin.size }];
219
287
 
220
288
  if (invokedFiles.length > 0) {
221
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
  }
@@ -0,0 +1,198 @@
1
+ import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
2
+
3
+ const THRESHOLDS = {
4
+ flag_threshold: 70,
5
+ warn_threshold: 50,
6
+ known_c2_endpoints: [
7
+ 'filev2.getsession.org',
8
+ 'api.signal.org',
9
+ '*.briarproject.org',
10
+ 'api.ricochet.im',
11
+ ],
12
+ onion_pattern_weight: 30,
13
+ encoded_url_weight: 35,
14
+ env_var_c2_weight: 40,
15
+ };
16
+
17
+ const KNOWN_C2_RE =
18
+ /(?:filev2\.getsession\.org|api\.signal\.org|(?:[\w-]+\.)?briarproject\.org|api\.ricochet\.im|signal-cli|signal-desktop|tor\s*(?:proxy|socks|connect|bridge))/gi;
19
+ const ONION_RE = /(?:[a-z2-7]{16,56}\.onion|\.onion|tor\s*(?:proxy|socks|connect|bridge))/gi;
20
+ const ENCODED_URL_RE =
21
+ /(?:atob|Buffer\.from|decodeURIComponent)\s*\((?:['"`][A-Za-z0-9+/=]{20,}['"`]|['"`](?:[0-9a-fA-F]{2,})['"`])/gi;
22
+ const HEX_DOMAIN_RE = /(?:0x[0-9a-fA-F]{2,}){4,}/g;
23
+ const SESSION_RE = /session|oxen|filev2|getsession/i;
24
+ const SIGNAL_RE = /signal|signal-cli|signal-desktop/i;
25
+ const BRIAR_RE = /briar|briarproject/i;
26
+
27
+ function detectC2InContent(content) {
28
+ const findings = [];
29
+
30
+ let match;
31
+ KNOWN_C2_RE.lastIndex = 0;
32
+ while ((match = KNOWN_C2_RE.exec(content)) !== null) {
33
+ findings.push({
34
+ type: 'known_endpoint',
35
+ endpoint: match[0],
36
+ weight: 80,
37
+ });
38
+ }
39
+
40
+ ONION_RE.lastIndex = 0;
41
+ while ((match = ONION_RE.exec(content)) !== null) {
42
+ findings.push({
43
+ type: 'onion_service',
44
+ pattern: match[0],
45
+ weight: THRESHOLDS.onion_pattern_weight,
46
+ });
47
+ }
48
+
49
+ ENCODED_URL_RE.lastIndex = 0;
50
+ while ((match = ENCODED_URL_RE.exec(content)) !== null) {
51
+ findings.push({
52
+ type: 'encoded_url',
53
+ snippet: match[0].substring(0, 60),
54
+ weight: THRESHOLDS.encoded_url_weight,
55
+ encoding: match[0].includes('atob')
56
+ ? 'base64'
57
+ : match[0].includes('Buffer.from')
58
+ ? 'hex_or_other'
59
+ : 'other',
60
+ });
61
+ }
62
+
63
+ HEX_DOMAIN_RE.lastIndex = 0;
64
+ while ((match = HEX_DOMAIN_RE.exec(content)) !== null) {
65
+ findings.push({
66
+ type: 'hex_encoded_domain',
67
+ snippet: match[0].substring(0, 40),
68
+ weight: THRESHOLDS.encoded_url_weight,
69
+ encoding: 'hex',
70
+ });
71
+ }
72
+
73
+ return findings;
74
+ }
75
+
76
+ function computeConfidence(c2Findings, hasSession, hasSignal, hasBriar) {
77
+ let base = 0;
78
+
79
+ const totalWeight = c2Findings.reduce((s, f) => s + f.weight, 0);
80
+ base += Math.min(totalWeight, 80);
81
+
82
+ if (c2Findings.length === 0) {
83
+ base = 20;
84
+ }
85
+
86
+ if (hasSession) base += 20;
87
+ if (hasSignal) base += 20;
88
+ if (hasBriar) base += 15;
89
+
90
+ if (c2Findings.length > 0) {
91
+ base += 20;
92
+ }
93
+
94
+ if (c2Findings.length > 1) {
95
+ base += Math.min(c2Findings.length * 5, 15);
96
+ }
97
+
98
+ return Math.min(100, Math.max(0, base));
99
+ }
100
+
101
+ function severityLabel(score) {
102
+ if (score >= 80) return 'critical';
103
+ if (score >= 60) return 'high';
104
+ return 'medium';
105
+ }
106
+
107
+ function confidenceLabel(score) {
108
+ if (score >= 80) return 'CRITICAL';
109
+ if (score >= 60) return 'HIGH';
110
+ return 'MEDIUM';
111
+ }
112
+
113
+ export const name = 'tier1-encrypted-c2';
114
+
115
+ export async function scan(pkgJson, jsFiles, _registryMeta, _allFiles) {
116
+ const pkgName = pkgJson?.name;
117
+ if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
118
+
119
+ const allContents = [];
120
+ if (pkgJson?.scripts && typeof pkgJson.scripts === 'object') {
121
+ for (const val of Object.values(pkgJson.scripts)) {
122
+ if (typeof val === 'string') {
123
+ allContents.push({ source: 'package.json scripts', content: val });
124
+ }
125
+ }
126
+ }
127
+
128
+ const files = jsFiles || [];
129
+ for (const f of files) {
130
+ if (f?.content) {
131
+ allContents.push({ source: f.path || f.name || 'unknown', content: f.content });
132
+ }
133
+ }
134
+
135
+ if (allContents.length === 0) return [];
136
+
137
+ const hasSession = SESSION_RE.test(JSON.stringify(allContents.map((c) => c.content)));
138
+ const hasSignal = SIGNAL_RE.test(JSON.stringify(allContents.map((c) => c.content)));
139
+ const hasBriar = BRIAR_RE.test(JSON.stringify(allContents.map((c) => c.content)));
140
+
141
+ const allFindings = [];
142
+ for (const { source, content } of allContents) {
143
+ const c2Findings = detectC2InContent(content);
144
+ if (c2Findings.length > 0) {
145
+ allFindings.push({ source, c2Findings });
146
+ }
147
+ }
148
+
149
+ const flatC2 = allFindings.flatMap((f) => f.c2Findings);
150
+ const totalSignals =
151
+ flatC2.length + (hasSession ? 1 : 0) + (hasSignal ? 1 : 0) + (hasBriar ? 1 : 0);
152
+ if (totalSignals === 0) return [];
153
+
154
+ const confidenceScore = computeConfidence(flatC2, hasSession, hasSignal, hasBriar);
155
+ if (confidenceScore < THRESHOLDS.warn_threshold) return [];
156
+
157
+ const endpointTypes = [...new Set(flatC2.map((f) => f.type))];
158
+ const primaryType = endpointTypes.includes('known_endpoint')
159
+ ? 'known_endpoint'
160
+ : endpointTypes.includes('onion_service')
161
+ ? 'onion_service'
162
+ : endpointTypes.includes('encoded_url')
163
+ ? 'encoded_url'
164
+ : endpointTypes.includes('hex_encoded_domain')
165
+ ? 'hex_encoded_domain'
166
+ : 'protocol_signal';
167
+
168
+ const evidence = flatC2.map((f) => {
169
+ if (f.type === 'known_endpoint') return `c2_endpoint: ${f.endpoint}`;
170
+ if (f.type === 'onion_service') return `onion_pattern: ${f.pattern}`;
171
+ return `encoded: ${f.snippet}`;
172
+ });
173
+
174
+ if (hasSession) evidence.push('protocol: Session/Oxen messenger');
175
+ if (hasSignal) evidence.push('protocol: Signal messenger');
176
+ if (hasBriar) evidence.push('protocol: Briar project');
177
+
178
+ const locations =
179
+ allFindings.length > 0
180
+ ? allFindings.map((f) => ({ file: f.source, line: 1, column: 1 })).slice(0, 5)
181
+ : [{ file: 'package.json', line: 1, column: 1 }];
182
+
183
+ return [
184
+ {
185
+ detector: 'tier1-encrypted-c2',
186
+ id: 'TIER1-ENCRYPTED-C2',
187
+ severity: severityLabel(confidenceScore),
188
+ confidence: confidenceLabel(confidenceScore),
189
+ confidenceScore,
190
+ subtype: primaryType,
191
+ message: `${flatC2.length > 0 ? flatC2.length + ' encrypted C2 signal(s)' : 'Encrypted C2 protocol(s) detected'}${hasSession || hasSignal || hasBriar ? ' (' + [hasSession ? 'Session' : '', hasSignal ? 'Signal' : '', hasBriar ? 'Briar' : ''].filter(Boolean).join(', ') + ')' : ''}`,
192
+ evidence: evidence.slice(0, 8),
193
+ locations,
194
+ crossFiles: [],
195
+ reference: 'D11: TanStack Mini Shai-Hulud encrypted C2',
196
+ },
197
+ ];
198
+ }