muaddib-scanner 2.10.2 → 2.10.3

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/bin/muaddib.js CHANGED
@@ -34,6 +34,7 @@ let noDeobfuscate = false;
34
34
  let noModuleGraph = false;
35
35
  let noReachability = false;
36
36
  let configPath = null;
37
+ let autoSandbox = false;
37
38
  let feedLimit = null;
38
39
  let feedSeverity = null;
39
40
  let feedSince = null;
@@ -137,6 +138,8 @@ for (let i = 0; i < options.length; i++) {
137
138
  }
138
139
  configPath = cfgPath;
139
140
  i++;
141
+ } else if (options[i] === '--auto-sandbox') {
142
+ autoSandbox = true;
140
143
  } else if (options[i] === '--temporal') {
141
144
  temporalMode = true;
142
145
  } else if (options[i] === '--limit') {
@@ -429,6 +432,7 @@ const helpText = `
429
432
  --temporal-publish Detect publish frequency anomalies (bursts, dormant spikes)
430
433
  --temporal-maintainer Detect maintainer changes (new maintainer, account takeover)
431
434
  --temporal-full All temporal analyses (lifecycle + AST + publish + maintainer)
435
+ --auto-sandbox Auto-trigger sandbox when static scan score >= 20 (requires Docker)
432
436
  --no-canary Disable honey token injection in sandbox
433
437
  --no-deobfuscate Disable deobfuscation pre-processing
434
438
  --no-module-graph Disable cross-file dataflow analysis
@@ -482,7 +486,8 @@ if (command === 'version' || command === '--version' || command === '-v') {
482
486
  noDeobfuscate: noDeobfuscate,
483
487
  noModuleGraph: noModuleGraph,
484
488
  noReachability: noReachability,
485
- configPath: configPath
489
+ configPath: configPath,
490
+ autoSandbox: autoSandbox
486
491
  }).then(exitCode => {
487
492
  process.exit(exitCode);
488
493
  }).catch(err => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.10.2",
3
+ "version": "2.10.3",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -71,6 +71,55 @@ function createCanaryNpmrc(tokens) {
71
71
  return `//registry.npmjs.org/:_authToken=${tokens.NPM_AUTH_TOKEN}\n`;
72
72
  }
73
73
 
74
+ /**
75
+ * Generate fake AWS credentials file content.
76
+ * Format matches ~/.aws/credentials (INI format, format-valid key IDs).
77
+ * @param {Record<string, string>} tokens - The token map from generateCanaryTokens()
78
+ * @returns {string} AWS credentials file content
79
+ */
80
+ function createCanaryAwsCredentials(tokens) {
81
+ return [
82
+ '[default]',
83
+ `aws_access_key_id = ${tokens.AWS_ACCESS_KEY_ID}`,
84
+ `aws_secret_access_key = ${tokens.AWS_SECRET_ACCESS_KEY}`,
85
+ 'region = us-east-1',
86
+ ''
87
+ ].join('\n');
88
+ }
89
+
90
+ /**
91
+ * Generate a fake SSH private key (Ed25519 format).
92
+ * The key is structurally valid PEM but cryptographically meaningless.
93
+ * Malware that reads ~/.ssh/id_rsa or id_ed25519 will exfiltrate this.
94
+ * @returns {string} Fake SSH private key content
95
+ */
96
+ function createCanarySshKey() {
97
+ const fakeKeyData = crypto.randomBytes(64).toString('base64');
98
+ return [
99
+ '-----BEGIN OPENSSH PRIVATE KEY-----',
100
+ fakeKeyData.substring(0, 70),
101
+ fakeKeyData.substring(0, 70),
102
+ '-----END OPENSSH PRIVATE KEY-----',
103
+ ''
104
+ ].join('\n');
105
+ }
106
+
107
+ /**
108
+ * Generate a fake .gitconfig with user identity.
109
+ * Malware fingerprinting the developer will exfiltrate this.
110
+ * @returns {string} Fake .gitconfig content
111
+ */
112
+ function createCanaryGitconfig() {
113
+ return [
114
+ '[user]',
115
+ '\tname = John Developer',
116
+ '\temail = john.dev@company-internal.example.com',
117
+ '[credential]',
118
+ '\thelper = store',
119
+ ''
120
+ ].join('\n');
121
+ }
122
+
74
123
  /**
75
124
  * Search for canary tokens in network logs from sandbox.
76
125
  * Network log structure matches sandbox.js report.network:
@@ -199,6 +248,9 @@ module.exports = {
199
248
  generateCanaryTokens,
200
249
  createCanaryEnvFile,
201
250
  createCanaryNpmrc,
251
+ createCanaryAwsCredentials,
252
+ createCanarySshKey,
253
+ createCanaryGitconfig,
202
254
  detectCanaryExfiltration,
203
255
  detectCanaryInOutput
204
256
  };
package/src/index.js CHANGED
@@ -523,6 +523,35 @@ async function run(targetPath, options = {}) {
523
523
  threats.push(...temporalThreats);
524
524
  }
525
525
 
526
+ // Auto-sandbox: trigger sandbox analysis when static scan detects threats.
527
+ // Preliminary score estimate: count CRITICAL/HIGH threats as a quick heuristic.
528
+ // Only when --auto-sandbox flag is set, no explicit sandboxResult, and Docker available.
529
+ if (options.autoSandbox && !options.sandboxResult) {
530
+ const critCount = threats.filter(t => t.severity === 'CRITICAL').length;
531
+ const highCount = threats.filter(t => t.severity === 'HIGH').length;
532
+ const prelimScore = Math.min(100, critCount * 25 + highCount * 10);
533
+ if (prelimScore >= 20) {
534
+ try {
535
+ const { isDockerAvailable, buildSandboxImage, runSandbox } = require('./sandbox/index.js');
536
+ if (isDockerAvailable()) {
537
+ console.log(`\n[AUTO-SANDBOX] Preliminary score ~${prelimScore} >= 20 — triggering sandbox analysis...`);
538
+ const built = await buildSandboxImage();
539
+ if (built) {
540
+ const sbResult = await runSandbox(targetPath, { local: true, strict: false });
541
+ if (sbResult && Array.isArray(sbResult.findings)) {
542
+ options.sandboxResult = sbResult;
543
+ }
544
+ }
545
+ } else {
546
+ debugLog('[AUTO-SANDBOX] Docker not available — skipping sandbox');
547
+ }
548
+ } catch (e) {
549
+ debugLog('[AUTO-SANDBOX] Error:', e && e.message);
550
+ // Graceful fallback — sandbox is best-effort
551
+ }
552
+ }
553
+ }
554
+
526
555
  // Sandbox integration
527
556
  let sandboxData = null;
528
557
  if (options.sandboxResult && Array.isArray(options.sandboxResult.findings)) {
@@ -585,6 +585,11 @@ const PLAYBOOKS = {
585
585
  'Le malware est cache derriere une indirection: package.json → node setup.js → payload malveillant. ' +
586
586
  'NE PAS installer. Supprimer le package immediatement. Auditer le fichier reference.',
587
587
 
588
+ uncaught_exception_exfil:
589
+ 'CRITIQUE: Exfiltration silencieuse via process.on("uncaughtException"). ' +
590
+ 'Le handler intercepte les erreurs pour envoyer les credentials (process.env) a un serveur externe. ' +
591
+ 'Regenerer immediatement tous les secrets. Supprimer le package. Auditer les connexions sortantes.',
592
+
588
593
  websocket_credential_exfil:
589
594
  'CRITIQUE: Exfiltration de credentials via canal non-HTTP (WebSocket, MQTT, Socket.io). ' +
590
595
  'Les proxies HTTP ne detectent pas ce trafic. Regenerer immediatement tous les secrets exposes. ' +
@@ -1719,6 +1719,18 @@ const RULES = {
1719
1719
  ],
1720
1720
  mitre: 'T1041'
1721
1721
  },
1722
+ uncaught_exception_exfil: {
1723
+ id: 'MUADDIB-COMPOUND-008',
1724
+ name: 'Uncaught Exception Handler Credential Exfiltration',
1725
+ severity: 'CRITICAL',
1726
+ confidence: 'high',
1727
+ description: 'process.on("uncaughtException") combine avec acces aux variables d\'environnement sensibles et appel reseau. Technique d\'exfiltration silencieuse: le handler intercepte les erreurs pour envoyer les credentials a un serveur externe sans interruption du processus.',
1728
+ references: [
1729
+ 'https://attack.mitre.org/techniques/T1041/',
1730
+ 'https://nodejs.org/api/process.html#event-uncaughtexception'
1731
+ ],
1732
+ mitre: 'T1041'
1733
+ },
1722
1734
  suspicious_module_sink: {
1723
1735
  id: 'MUADDIB-FLOW-005',
1724
1736
  name: 'Non-HTTP Network Module Sink',
@@ -6,6 +6,9 @@ const {
6
6
  generateCanaryTokens,
7
7
  createCanaryEnvFile,
8
8
  createCanaryNpmrc,
9
+ createCanaryAwsCredentials,
10
+ createCanarySshKey,
11
+ createCanaryGitconfig,
9
12
  detectCanaryExfiltration,
10
13
  detectCanaryInOutput
11
14
  } = require('../canary-tokens.js');
@@ -154,10 +157,15 @@ async function runSingleSandbox(packageName, options = {}) {
154
157
  let timedOut = false;
155
158
  const containerName = `npm-audit-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
156
159
 
160
+ // Realistic hostname to evade sandbox detection (T1497.001)
161
+ // Default Docker hostname is a 12-char hex hash — easily fingerprinted.
162
+ const fakeHostname = `dev-laptop-${crypto.randomBytes(2).toString('hex')}`;
163
+
157
164
  const dockerArgs = [
158
165
  'run',
159
166
  '--rm',
160
167
  `--name=${containerName}`,
168
+ `--hostname=${fakeHostname}`,
161
169
  '--network=bridge',
162
170
  '--memory=512m',
163
171
  '--cpus=1',
@@ -173,6 +181,9 @@ async function runSingleSandbox(packageName, options = {}) {
173
181
  // Also inject canary file contents as env vars for the entrypoint to write
174
182
  dockerArgs.push('-e', `CANARY_ENV_CONTENT=${createCanaryEnvFile(canaryTokens).replace(/\r?\n/g, '\\n')}`);
175
183
  dockerArgs.push('-e', `CANARY_NPMRC_CONTENT=${createCanaryNpmrc(canaryTokens).replace(/\r?\n/g, '\\n')}`);
184
+ dockerArgs.push('-e', `CANARY_AWS_CONTENT=${createCanaryAwsCredentials(canaryTokens).replace(/\r?\n/g, '\\n')}`);
185
+ dockerArgs.push('-e', `CANARY_SSH_KEY=${createCanarySshKey().replace(/\r?\n/g, '\\n')}`);
186
+ dockerArgs.push('-e', `CANARY_GITCONFIG=${createCanaryGitconfig().replace(/\r?\n/g, '\\n')}`);
176
187
  }
177
188
 
178
189
  // Inject time offset (preload.js deferred to entry point in sandbox-runner.sh)
@@ -537,7 +537,16 @@ function handleVariableDeclarator(node, ctx) {
537
537
  innerCall.arguments?.length >= 1) {
538
538
  const arg = innerCall.arguments[0];
539
539
  if (arg.type === 'FunctionExpression' && (arg.async || arg.generator)) {
540
+ const kind = arg.async ? 'AsyncFunction' : 'GeneratorFunction';
540
541
  ctx.evalAliases.set(node.id.name, 'Function');
542
+ // Emit CRITICAL at declaration — extracting constructor via prototype chain is never benign
543
+ ctx.hasDynamicExec = true;
544
+ ctx.threats.push({
545
+ type: 'dangerous_constructor',
546
+ severity: 'CRITICAL',
547
+ message: `${kind} constructor extracted via Object.getPrototypeOf() into "${node.id.name}" — prototype chain code execution evasion.`,
548
+ file: ctx.relFile
549
+ });
541
550
  }
542
551
  }
543
552
  }
@@ -664,6 +673,21 @@ function handleVariableDeclarator(node, ctx) {
664
673
  }
665
674
  }
666
675
 
676
+ // Audit v3 bypass fix: Array destructuring eval alias: const [fn] = [eval]
677
+ if (node.id?.type === 'ArrayPattern' && node.init?.type === 'ArrayExpression') {
678
+ const elements = node.init.elements;
679
+ const patterns = node.id.elements;
680
+ for (let i = 0; i < Math.min(elements?.length || 0, patterns?.length || 0); i++) {
681
+ const el = elements[i];
682
+ const pat = patterns[i];
683
+ if (el?.type === 'Identifier' && pat?.type === 'Identifier') {
684
+ if (el.name === 'eval' || el.name === 'Function') {
685
+ ctx.evalAliases.set(pat.name, el.name);
686
+ }
687
+ }
688
+ }
689
+ }
690
+
667
691
  // Audit v3 B3: Destructuring of require('module') → track _load as direct function alias
668
692
  if (node.id?.type === 'ObjectPattern' &&
669
693
  node.init?.type === 'CallExpression') {
@@ -1398,10 +1422,21 @@ function handleCallExpression(node, ctx) {
1398
1422
  } else {
1399
1423
  ctx.hasEvalInFile = true;
1400
1424
  ctx.hasDynamicExec = true;
1425
+ // Audit v3: elevate to CRITICAL when argument contains dangerous API calls
1426
+ let aliasSev = 'HIGH';
1427
+ let aliasMsg = `Indirect ${aliased} via alias "${node.callee.name}" — eval wrapper evasion.`;
1428
+ if (node.arguments.length >= 1) {
1429
+ const aliasArg = node.arguments[0];
1430
+ if (aliasArg?.type === 'Literal' && typeof aliasArg.value === 'string' &&
1431
+ /\b(require|import|exec|execSync|spawn|child_process|process\.env)\b/.test(aliasArg.value)) {
1432
+ aliasSev = 'CRITICAL';
1433
+ aliasMsg = `Indirect ${aliased} via alias "${node.callee.name}" with dangerous payload: "${aliasArg.value.substring(0, 80)}" — eval evasion + code execution.`;
1434
+ }
1435
+ }
1401
1436
  ctx.threats.push({
1402
1437
  type: aliased === 'eval' ? 'dangerous_call_eval' : 'dangerous_call_function',
1403
- severity: 'HIGH',
1404
- message: `Indirect ${aliased} via alias "${node.callee.name}" — eval wrapper evasion.`,
1438
+ severity: aliasSev,
1439
+ message: aliasMsg,
1405
1440
  file: ctx.relFile
1406
1441
  });
1407
1442
  }
@@ -1579,10 +1614,19 @@ function handleCallExpression(node, ctx) {
1579
1614
  firstArg.type === 'TemplateLiteral') {
1580
1615
  ctx.hasEvalInFile = true;
1581
1616
  ctx.hasDynamicExec = true;
1617
+ // Audit v3: elevate to CRITICAL when string contains dangerous API calls
1618
+ let timerSeverity = 'HIGH';
1619
+ let timerMsg = `${callName}() with string argument — eval equivalent, executes the string as code.`;
1620
+ if (firstArg.type === 'Literal' && typeof firstArg.value === 'string') {
1621
+ if (/\b(require|import|exec|execSync|spawn|child_process|\.readFile|\.writeFile|process\.env|\.homedir)\b/.test(firstArg.value)) {
1622
+ timerSeverity = 'CRITICAL';
1623
+ timerMsg = `${callName}() with dangerous API in string: "${firstArg.value.substring(0, 80)}" — eval equivalent code execution.`;
1624
+ }
1625
+ }
1582
1626
  ctx.threats.push({
1583
1627
  type: 'dangerous_call_eval',
1584
- severity: 'HIGH',
1585
- message: `${callName}() with string argument — eval equivalent, executes the string as code.`,
1628
+ severity: timerSeverity,
1629
+ message: timerMsg,
1586
1630
  file: ctx.relFile
1587
1631
  });
1588
1632
  }
@@ -1929,6 +1973,24 @@ function handleCallExpression(node, ctx) {
1929
1973
  }
1930
1974
  }
1931
1975
  }
1976
+
1977
+ // Audit v3 bypass fix: .constructor.constructor('code')() — double constructor chain
1978
+ // Any expression.constructor.constructor is traversing to Function constructor
1979
+ if (obj?.type === 'MemberExpression') {
1980
+ const innerPropName = obj.computed
1981
+ ? (obj.property?.type === 'Literal' ? String(obj.property.value) : null)
1982
+ : (obj.property?.type === 'Identifier' ? obj.property.name : null);
1983
+ if (innerPropName === 'constructor') {
1984
+ ctx.hasEvalInFile = true;
1985
+ ctx.hasDynamicExec = true;
1986
+ ctx.threats.push({
1987
+ type: 'dangerous_constructor',
1988
+ severity: 'CRITICAL',
1989
+ message: 'Constructor chain traversal: .constructor.constructor() — accesses Function constructor via prototype chain.',
1990
+ file: ctx.relFile
1991
+ });
1992
+ }
1993
+ }
1932
1994
  }
1933
1995
 
1934
1996
  // SANDWORM_MODE: Track writeFileSync/writeFile to temp paths
@@ -1980,17 +2042,45 @@ function handleCallExpression(node, ctx) {
1980
2042
  }
1981
2043
  }
1982
2044
 
2045
+ // Audit v3: JSON.stringify(process.env) — bulk env serialization = env enumeration
2046
+ if (node.callee.type === 'MemberExpression' &&
2047
+ node.callee.object?.type === 'Identifier' && node.callee.object.name === 'JSON' &&
2048
+ node.callee.property?.type === 'Identifier' && node.callee.property.name === 'stringify' &&
2049
+ node.arguments.length >= 1) {
2050
+ const strArg = node.arguments[0];
2051
+ if (strArg.type === 'MemberExpression' &&
2052
+ strArg.object?.type === 'Identifier' && strArg.object.name === 'process' &&
2053
+ strArg.property?.type === 'Identifier' && strArg.property.name === 'env') {
2054
+ ctx.hasEnvEnumeration = true;
2055
+ }
2056
+ }
2057
+
1983
2058
  // Batch 1: vm.* code execution — vm.runInThisContext, vm.runInNewContext, vm.compileFunction, vm.Script
1984
2059
  if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
1985
2060
  const vmMethod = node.callee.property.name;
1986
2061
  if (['runInThisContext', 'runInNewContext', 'compileFunction'].includes(vmMethod)) {
1987
- // NOTE: Do NOT set ctx.hasDynamicExec vm.* is legitimately used by bundlers
2062
+ // Audit v3: elevate to CRITICAL when argument contains dangerous API calls
2063
+ let vmSeverity = 'HIGH';
2064
+ let vmMsg = `vm.${vmMethod}() — dynamic code execution via Node.js vm module bypasses eval detection.`;
2065
+ const vmArg = node.arguments?.[0];
2066
+ let vmContent = '';
2067
+ if (vmArg?.type === 'Literal' && typeof vmArg.value === 'string') {
2068
+ vmContent = vmArg.value;
2069
+ } else if (vmArg?.type === 'Identifier' && ctx.stringVarValues?.has(vmArg.name)) {
2070
+ vmContent = ctx.stringVarValues.get(vmArg.name);
2071
+ }
2072
+ if (/\b(require|import|exec|execSync|spawn|child_process|process\.env)\b/.test(vmContent)) {
2073
+ vmSeverity = 'CRITICAL';
2074
+ ctx.hasDynamicExec = true;
2075
+ vmMsg = `vm.${vmMethod}() with dangerous API in code: "${vmContent.substring(0, 80)}" — vm module code execution bypass.`;
2076
+ }
2077
+ // NOTE: Do NOT set ctx.hasDynamicExec for generic vm.* calls — legitimately used by bundlers
1988
2078
  // (webpack, jest, etc.) and must not trigger compound detections (zlib_inflate_eval,
1989
2079
  // fetch_decrypt_exec) which were designed for eval/Function patterns.
1990
2080
  ctx.threats.push({
1991
2081
  type: 'vm_code_execution',
1992
- severity: 'HIGH',
1993
- message: `vm.${vmMethod}() — dynamic code execution via Node.js vm module bypasses eval detection.`,
2082
+ severity: vmSeverity,
2083
+ message: vmMsg,
1994
2084
  file: ctx.relFile
1995
2085
  });
1996
2086
  }
@@ -2053,6 +2143,19 @@ function handleCallExpression(node, ctx) {
2053
2143
  }
2054
2144
  }
2055
2145
 
2146
+ // Audit v3 bypass fix: process.on('uncaughtException'/'unhandledRejection', handler)
2147
+ // Error handler hijacking for silent credential exfiltration
2148
+ if (node.callee?.type === 'MemberExpression' &&
2149
+ node.callee.object?.type === 'Identifier' && node.callee.object.name === 'process' &&
2150
+ node.callee.property?.type === 'Identifier' && node.callee.property.name === 'on' &&
2151
+ node.arguments.length >= 2) {
2152
+ const eventArg = node.arguments[0];
2153
+ if (eventArg?.type === 'Literal' &&
2154
+ (eventArg.value === 'uncaughtException' || eventArg.value === 'unhandledRejection')) {
2155
+ ctx.hasUncaughtExceptionHandler = true;
2156
+ }
2157
+ }
2158
+
2056
2159
  // SANDWORM_MODE R8: dns.resolve detection moved to walk.ancestor() in ast.js (FIX 5)
2057
2160
  }
2058
2161
 
@@ -2064,9 +2167,11 @@ function handleImportExpression(node, ctx) {
2064
2167
  // Batch 2: strip node: prefix so import('node:child_process') normalizes
2065
2168
  const modName = src.value.startsWith('node:') ? src.value.slice(5) : src.value;
2066
2169
  if (dangerousModules.includes(modName)) {
2170
+ // Audit v3: dynamic import of code execution modules → CRITICAL (evasion technique)
2171
+ const CRITICAL_IMPORTS = ['child_process', 'net', 'dns', 'worker_threads'];
2067
2172
  ctx.threats.push({
2068
2173
  type: 'dynamic_import',
2069
- severity: 'HIGH',
2174
+ severity: CRITICAL_IMPORTS.includes(modName) ? 'CRITICAL' : 'HIGH',
2070
2175
  message: `Dynamic import() of dangerous module "${src.value}".`,
2071
2176
  file: ctx.relFile
2072
2177
  });
@@ -2815,6 +2920,29 @@ function handlePostWalk(ctx) {
2815
2920
  });
2816
2921
  }
2817
2922
 
2923
+ // Audit v3 bypass fix: uncaughtException + env access + network = silent exfiltration
2924
+ // Pattern: process.on('uncaughtException', handler) that reads env vars and sends to network.
2925
+ // Never legitimate — error handlers don't need to send credentials to external servers.
2926
+ if (ctx.hasUncaughtExceptionHandler && hasSensitiveEnvInFile && ctx.hasNetworkCallInFile) {
2927
+ ctx.threats.push({
2928
+ type: 'uncaught_exception_exfil',
2929
+ severity: 'CRITICAL',
2930
+ message: 'process.on("uncaughtException") + sensitive env access + network — silent credential exfiltration via error handler hijacking.',
2931
+ file: ctx.relFile
2932
+ });
2933
+ }
2934
+ // Also: uncaughtException + env enumeration (Object.entries/keys(process.env)) + network
2935
+ if (ctx.hasUncaughtExceptionHandler && ctx.hasEnvEnumeration && ctx.hasNetworkCallInFile) {
2936
+ if (!ctx.threats.some(t => t.type === 'uncaught_exception_exfil' && t.file === ctx.relFile)) {
2937
+ ctx.threats.push({
2938
+ type: 'uncaught_exception_exfil',
2939
+ severity: 'CRITICAL',
2940
+ message: 'process.on("uncaughtException") + bulk env enumeration + network — silent credential exfiltration via error handler hijacking.',
2941
+ file: ctx.relFile
2942
+ });
2943
+ }
2944
+ }
2945
+
2818
2946
  // GlassWorm: Unicode variation selector decoder = .codePointAt + variation selector constants
2819
2947
  // CRITICAL if combined with eval/exec (GlassWorm always uses dynamic execution),
2820
2948
  // MEDIUM otherwise (.codePointAt + 0xFE00 is legitimate Unicode processing in fonts/text libs)
@@ -178,7 +178,9 @@ function analyzeFile(content, filePath, basePath) {
178
178
  hasVariationSelectorConst: false,
179
179
  // GlassWorm: blockchain C2 resolution (Solana import + C2 method + dynamic exec)
180
180
  hasSolanaImport: false,
181
- hasSolanaC2Method: false
181
+ hasSolanaC2Method: false,
182
+ // Audit v3: uncaughtException/unhandledRejection handler for error hijacking detection
183
+ hasUncaughtExceptionHandler: false
182
184
  };
183
185
 
184
186
  // Compute fetchOnlySafeDomains: check if ALL URLs in file point to known registries