muaddib-scanner 2.2.27 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/muaddib.js CHANGED
@@ -33,6 +33,7 @@ let temporalFullMode = false;
33
33
  let breakdownMode = false;
34
34
  let noDeobfuscate = false;
35
35
  let noModuleGraph = false;
36
+ let noReachability = false;
36
37
  let feedLimit = null;
37
38
  let feedSeverity = null;
38
39
  let feedSince = null;
@@ -122,6 +123,8 @@ for (let i = 0; i < options.length; i++) {
122
123
  noDeobfuscate = true;
123
124
  } else if (options[i] === '--no-module-graph') {
124
125
  noModuleGraph = true;
126
+ } else if (options[i] === '--no-reachability') {
127
+ noReachability = true;
125
128
  } else if (options[i] === '--temporal') {
126
129
  temporalMode = true;
127
130
  } else if (options[i] === '--limit') {
@@ -415,6 +418,7 @@ const helpText = `
415
418
  --no-canary Disable honey token injection in sandbox
416
419
  --no-deobfuscate Disable deobfuscation pre-processing
417
420
  --no-module-graph Disable cross-file dataflow analysis
421
+ --no-reachability Disable entry-point reachability analysis
418
422
  --exclude [dir] Exclude directory from scan (repeatable)
419
423
  --limit [n] Limit feed entries (default: 50)
420
424
  --severity [level] Filter by severity (CRITICAL|HIGH|MEDIUM|LOW)
@@ -461,7 +465,8 @@ if (command === 'version' || command === '--version' || command === '-v') {
461
465
  entropyThreshold: entropyThreshold,
462
466
  breakdown: breakdownMode,
463
467
  noDeobfuscate: noDeobfuscate,
464
- noModuleGraph: noModuleGraph
468
+ noModuleGraph: noModuleGraph,
469
+ noReachability: noReachability
465
470
  }).then(exitCode => {
466
471
  process.exit(exitCode);
467
472
  }).catch(err => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.2.27",
3
+ "version": "2.3.0",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -19,6 +19,7 @@ const { scanEntropy } = require('./scanner/entropy.js');
19
19
  const { scanAIConfig } = require('./scanner/ai-config.js');
20
20
  const { deobfuscate } = require('./scanner/deobfuscate.js');
21
21
  const { buildModuleGraph, annotateTaintedExports, detectCrossFileFlows } = require('./scanner/module-graph.js');
22
+ const { computeReachableFiles } = require('./scanner/reachability.js');
22
23
  const { runTemporalAnalyses } = require('./temporal-runner.js');
23
24
  const { formatOutput } = require('./output-formatter.js');
24
25
  const { setExtraExcludes, getExtraExcludes, Spinner, listInstalledPackages, clearFileListCache } = require('./utils.js');
@@ -333,9 +334,22 @@ async function run(targetPath, options = {}) {
333
334
  }
334
335
  }
335
336
 
337
+ // Reachability analysis: determine which files are reachable from entry points
338
+ let reachableFiles = null;
339
+ if (!options.noReachability) {
340
+ try {
341
+ const reachability = computeReachableFiles(targetPath);
342
+ if (!reachability.skipped) {
343
+ reachableFiles = reachability.reachableFiles;
344
+ }
345
+ } catch {
346
+ // Graceful fallback — treat all files as reachable
347
+ }
348
+ }
349
+
336
350
  // FP reduction: legitimate frameworks produce high volumes of certain threat types.
337
351
  // A malware package typically has 1-3 occurrences, not dozens.
338
- applyFPReductions(deduped);
352
+ applyFPReductions(deduped, reachableFiles);
339
353
 
340
354
  // Enrich each threat with rules
341
355
  const enrichedThreats = deduped.map(t => {
@@ -354,6 +354,31 @@ const PLAYBOOKS = {
354
354
  'execution via require() ou Module._compile(), puis suppression via unlinkSync/rmSync. ' +
355
355
  'Ce pattern de staging est typique des malwares cherchant a eviter la detection post-mortem. ' +
356
356
  'Isoler la machine et inspecter les artefacts temporaires avant nettoyage.',
357
+
358
+ mcp_config_injection:
359
+ 'CRITIQUE: Ecriture dans les fichiers de configuration MCP d\'assistants IA (.claude/, .cursor/, .continue/, .vscode/). ' +
360
+ 'Technique SANDWORM_MODE: le malware empoisonne la configuration MCP pour ajouter des serveurs malveillants. ' +
361
+ 'Verifier immediatement les fichiers de config IA. Supprimer les entrees MCP non reconnues. Supprimer le package.',
362
+
363
+ git_hooks_injection:
364
+ 'Injection de hooks Git detectee. Le package ecrit dans .git/hooks/ (pre-commit, pre-push, etc.) ' +
365
+ 'ou modifie git config init.templateDir pour la persistence globale. ' +
366
+ 'Verifier .git/hooks/ et git config --global --list. Supprimer les hooks non reconnus.',
367
+
368
+ env_harvesting_dynamic:
369
+ 'Collecte dynamique de variables d\'environnement via Object.entries/keys/values(process.env) ' +
370
+ 'avec filtrage par patterns sensibles. Technique de vol de credentials a grande echelle. ' +
371
+ 'Verifier les tokens/secrets exposes. Revoquer immediatement les credentials compromis.',
372
+
373
+ dns_chunk_exfiltration:
374
+ 'Exfiltration via requetes DNS detectee. Les donnees sont encodees en base64 et envoyees comme sous-domaines DNS. ' +
375
+ 'Cette technique contourne les firewalls et proxies car le DNS est rarement filtre. ' +
376
+ 'Bloquer les requetes DNS sortantes du package. Verifier les donnees exfiltrees.',
377
+
378
+ llm_api_key_harvesting:
379
+ 'Acces a 3+ cles API de providers LLM (OpenAI, Anthropic, Google, etc.). Usage unique = legitime, ' +
380
+ 'acces multiples = collecte pour revente ou abus. Verifier si le package a une raison ' +
381
+ 'legitime d\'acceder a plusieurs providers. Revoquer les cles exposees si necessaire.',
357
382
  };
358
383
 
359
384
  function getPlaybook(threatType) {
@@ -713,6 +713,66 @@ const RULES = {
713
713
  mitre: 'T1070.004'
714
714
  },
715
715
 
716
+ mcp_config_injection: {
717
+ id: 'MUADDIB-AST-027',
718
+ name: 'MCP Config Injection',
719
+ severity: 'CRITICAL',
720
+ confidence: 'high',
721
+ description: 'Injection de configuration MCP: ecriture dans les fichiers de configuration d\'assistants IA (.claude/, .cursor/, .continue/, .vscode/, .windsurf/). Technique SANDWORM_MODE pour empoisonner la chaine d\'outils IA.',
722
+ references: [
723
+ 'https://attack.mitre.org/techniques/T1546/016/'
724
+ ],
725
+ mitre: 'T1546.016'
726
+ },
727
+
728
+ git_hooks_injection: {
729
+ id: 'MUADDIB-AST-028',
730
+ name: 'Git Hooks Injection',
731
+ severity: 'HIGH',
732
+ confidence: 'high',
733
+ description: 'Injection de hooks Git: ecriture dans .git/hooks/ ou modification de git config init.templateDir. Technique de persistence via hooks pre-commit, pre-push, post-checkout.',
734
+ references: [
735
+ 'https://attack.mitre.org/techniques/T1546/004/'
736
+ ],
737
+ mitre: 'T1546.004'
738
+ },
739
+
740
+ env_harvesting_dynamic: {
741
+ id: 'MUADDIB-AST-029',
742
+ name: 'Dynamic Environment Variable Harvesting',
743
+ severity: 'HIGH',
744
+ confidence: 'high',
745
+ description: 'Collecte dynamique de variables d\'environnement via Object.entries/keys/values(process.env) avec filtrage par patterns sensibles (TOKEN, SECRET, KEY, PASSWORD, AWS, SSH). Technique de vol de credentials.',
746
+ references: [
747
+ 'https://attack.mitre.org/techniques/T1552/001/'
748
+ ],
749
+ mitre: 'T1552.001'
750
+ },
751
+
752
+ dns_chunk_exfiltration: {
753
+ id: 'MUADDIB-AST-030',
754
+ name: 'DNS Chunk Exfiltration',
755
+ severity: 'HIGH',
756
+ confidence: 'high',
757
+ description: 'Exfiltration DNS: donnees encodees en base64 dans les requetes DNS. Canal covert pour contourner les firewalls. Pattern: dns.resolve + Buffer.from().toString("base64").',
758
+ references: [
759
+ 'https://attack.mitre.org/techniques/T1048/003/'
760
+ ],
761
+ mitre: 'T1048.003'
762
+ },
763
+
764
+ llm_api_key_harvesting: {
765
+ id: 'MUADDIB-AST-031',
766
+ name: 'LLM API Key Harvesting',
767
+ severity: 'MEDIUM',
768
+ confidence: 'medium',
769
+ description: 'Collecte de cles API LLM: acces a 3+ variables d\'environnement de providers IA (OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, etc.). Vecteur de monetisation.',
770
+ references: [
771
+ 'https://attack.mitre.org/techniques/T1552/001/'
772
+ ],
773
+ mitre: 'T1552.001'
774
+ },
775
+
716
776
  ai_agent_abuse: {
717
777
  id: 'MUADDIB-AST-013',
718
778
  name: 'AI Agent Weaponization',
@@ -83,6 +83,38 @@ const NODE_HOOKABLE_CLASSES = [
83
83
  'OutgoingMessage', 'Socket', 'Server', 'Agent'
84
84
  ];
85
85
 
86
+ // AI/MCP config paths targeted for config injection (SANDWORM_MODE)
87
+ const MCP_CONFIG_PATHS = [
88
+ '.claude/', 'claude_desktop_config',
89
+ '.cursor/', 'mcp.json',
90
+ '.continue/',
91
+ '.vscode/',
92
+ '.windsurf/', '.codeium/'
93
+ ];
94
+
95
+ // MCP content indicators in written data
96
+ const MCP_CONTENT_PATTERNS = ['mcpServers', '"mcp"', '"server"', '"command"', '"args"'];
97
+
98
+ // Git hooks names
99
+ const GIT_HOOKS = [
100
+ 'pre-commit', 'pre-push', 'post-checkout', 'post-merge',
101
+ 'pre-receive', 'post-receive', 'prepare-commit-msg', 'commit-msg',
102
+ 'pre-rebase', 'post-rewrite', 'pre-auto-gc'
103
+ ];
104
+
105
+ // LLM API key environment variable names (3+ = harvesting)
106
+ const LLM_API_KEY_VARS = [
107
+ 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GOOGLE_API_KEY',
108
+ 'GROQ_API_KEY', 'TOGETHER_API_KEY', 'FIREWORKS_API_KEY',
109
+ 'REPLICATE_API_KEY', 'MISTRAL_API_KEY', 'COHERE_API_KEY'
110
+ ];
111
+
112
+ // Env harvesting sensitive patterns (for Object.entries/keys/values filtering)
113
+ const ENV_HARVEST_PATTERNS = [
114
+ 'KEY', 'SECRET', 'TOKEN', 'PASSWORD', 'CREDENTIAL',
115
+ 'NPM', 'AWS', 'SSH', 'WEBHOOK'
116
+ ];
117
+
86
118
  // Paths indicating sandbox/container environment detection (anti-analysis)
87
119
  const SANDBOX_INDICATORS = [
88
120
  '/.dockerenv',
@@ -487,6 +519,84 @@ function handleCallExpression(node, ctx) {
487
519
  }
488
520
  }
489
521
 
522
+ // SANDWORM_MODE R5: MCP config injection — writeFileSync to AI config paths
523
+ if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
524
+ const mcpWriteMethod = node.callee.property.name;
525
+ if (['writeFileSync', 'writeFile'].includes(mcpWriteMethod) && node.arguments.length >= 2) {
526
+ const mcpPathArg = node.arguments[0];
527
+ const mcpPathStr = extractStringValue(mcpPathArg);
528
+ // Also check path.join() calls
529
+ let mcpJoinedPath = '';
530
+ if (mcpPathArg?.type === 'CallExpression' && mcpPathArg.arguments) {
531
+ mcpJoinedPath = mcpPathArg.arguments.map(a => extractStringValue(a) || '').join('/');
532
+ }
533
+ const mcpCheckPath = (mcpPathStr || mcpJoinedPath).toLowerCase();
534
+ const isMcpPath = MCP_CONFIG_PATHS.some(p => mcpCheckPath.includes(p.toLowerCase()));
535
+ if (isMcpPath) {
536
+ // Check content argument for MCP-related patterns
537
+ const contentArg = node.arguments[1];
538
+ const contentStr = extractStringValue(contentArg);
539
+ const hasContentPattern = contentStr
540
+ ? MCP_CONTENT_PATTERNS.some(p => contentStr.includes(p.replace(/"/g, '')))
541
+ : true; // dynamic content = suspicious by default for AI config paths
542
+ if (hasContentPattern) {
543
+ ctx.threats.push({
544
+ type: 'mcp_config_injection',
545
+ severity: 'CRITICAL',
546
+ message: `MCP config injection: ${mcpWriteMethod}() writes to AI assistant configuration (${mcpCheckPath}). SANDWORM_MODE technique for AI toolchain poisoning.`,
547
+ file: ctx.relFile
548
+ });
549
+ }
550
+ }
551
+ }
552
+ }
553
+
554
+ // SANDWORM_MODE R6: Git hooks injection — writeFileSync to .git/hooks/
555
+ if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
556
+ const gitWriteMethod = node.callee.property.name;
557
+ if (['writeFileSync', 'writeFile'].includes(gitWriteMethod) && node.arguments.length >= 1) {
558
+ const gitPathArg = node.arguments[0];
559
+ const gitPathStr = extractStringValue(gitPathArg);
560
+ let gitJoinedPath = '';
561
+ if (gitPathArg?.type === 'CallExpression' && gitPathArg.arguments) {
562
+ gitJoinedPath = gitPathArg.arguments.map(a => extractStringValue(a) || '').join('/');
563
+ }
564
+ const gitCheckPath = gitPathStr || gitJoinedPath;
565
+ if (/\.git[\\/]hooks[\\/]/i.test(gitCheckPath) ||
566
+ GIT_HOOKS.some(h => gitCheckPath.includes(h) && gitCheckPath.includes('.git'))) {
567
+ ctx.threats.push({
568
+ type: 'git_hooks_injection',
569
+ severity: 'HIGH',
570
+ message: `Git hook injection: ${gitWriteMethod}() writes to .git/hooks/. Persistence technique.`,
571
+ file: ctx.relFile
572
+ });
573
+ }
574
+ }
575
+ }
576
+
577
+ // SANDWORM_MODE R6: Git hooks injection via exec — git config --global init.templateDir or git config hooks
578
+ if ((execName || memberExec) && node.arguments.length > 0) {
579
+ const gitExecArg = node.arguments[0];
580
+ const gitExecStr = extractStringValue(gitExecArg);
581
+ if (gitExecStr) {
582
+ if (/git\s+config\s+.*init\.templateDir/i.test(gitExecStr)) {
583
+ ctx.threats.push({
584
+ type: 'git_hooks_injection',
585
+ severity: 'HIGH',
586
+ message: 'Git hook injection: modifying global git template directory via "git config init.templateDir". Persistence technique.',
587
+ file: ctx.relFile
588
+ });
589
+ } else if (/git\s+config\b/.test(gitExecStr) && /hooks/i.test(gitExecStr)) {
590
+ ctx.threats.push({
591
+ type: 'git_hooks_injection',
592
+ severity: 'HIGH',
593
+ message: 'Git hook injection: modifying git hooks configuration. Persistence technique.',
594
+ file: ctx.relFile
595
+ });
596
+ }
597
+ }
598
+ }
599
+
490
600
  // Detect fs.chmodSync with executable permissions (deferred to postWalk for compound check)
491
601
  if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
492
602
  const chmodMethod = node.callee.property.name;
@@ -698,7 +808,7 @@ function handleCallExpression(node, ctx) {
698
808
  });
699
809
  }
700
810
  // Module._compile counts as temp file exec for write-execute-delete pattern
701
- ctx.hasTempFileExec = ctx.hasTempFileExec || ctx.hasTmpdirInContent;
811
+ ctx.hasTempFileExec = ctx.hasTempFileExec || ctx.hasDevShmInContent;
702
812
  }
703
813
 
704
814
  // SANDWORM_MODE: Track writeFileSync/writeFile to temp paths
@@ -706,13 +816,13 @@ function handleCallExpression(node, ctx) {
706
816
  const arg = node.arguments && node.arguments[0];
707
817
  if (arg) {
708
818
  const strVal = extractStringValue(arg);
709
- if (strVal && (/\/dev\/shm\b/.test(strVal) || /\btmp\b/i.test(strVal) || /\btemp\b/i.test(strVal))) {
819
+ if (strVal && /\/dev\/shm\b/.test(strVal)) {
710
820
  ctx.hasTempFileWrite = true;
711
821
  }
712
- // Variable reference to tmpdir/temp path
822
+ // Variable reference to /dev/shm path
713
823
  if (!strVal && (arg.type === 'Identifier' || arg.type === 'CallExpression' || arg.type === 'MemberExpression')) {
714
- // Dynamic path — check if file content involves tmpdir patterns
715
- ctx.hasTempFileWrite = ctx.hasTempFileWrite || ctx.hasTmpdirInContent;
824
+ // Dynamic path — check if file content involves /dev/shm
825
+ ctx.hasTempFileWrite = ctx.hasTempFileWrite || ctx.hasDevShmInContent;
716
826
  }
717
827
  }
718
828
  }
@@ -727,13 +837,30 @@ function handleCallExpression(node, ctx) {
727
837
  if (callName === 'require' && node.arguments.length > 0) {
728
838
  const arg = node.arguments[0];
729
839
  const strVal = extractStringValue(arg);
730
- if (strVal && (/\/dev\/shm\b/.test(strVal) || /\btmp\b/i.test(strVal) || /\btemp\b/i.test(strVal))) {
840
+ if (strVal && /\/dev\/shm\b/.test(strVal)) {
731
841
  ctx.hasTempFileExec = true;
732
- } else if (!strVal && ctx.hasTmpdirInContent) {
733
- // Variable argument in a file that references tmpdir paths
842
+ } else if (!strVal && ctx.hasDevShmInContent) {
843
+ // Variable argument in a file that references /dev/shm
734
844
  ctx.hasTempFileExec = true;
735
845
  }
736
846
  }
847
+
848
+ // SANDWORM_MODE R7: Detect Object.entries/keys/values(process.env)
849
+ if (node.callee.type === 'MemberExpression' &&
850
+ node.callee.object?.type === 'Identifier' && node.callee.object.name === 'Object' &&
851
+ node.callee.property?.type === 'Identifier' &&
852
+ ['entries', 'keys', 'values'].includes(node.callee.property.name) &&
853
+ node.arguments.length >= 1) {
854
+ const enumArg = node.arguments[0];
855
+ // Check if argument is process.env
856
+ if (enumArg.type === 'MemberExpression' &&
857
+ enumArg.object?.type === 'Identifier' && enumArg.object.name === 'process' &&
858
+ enumArg.property?.type === 'Identifier' && enumArg.property.name === 'env') {
859
+ ctx.hasEnvEnumeration = true;
860
+ }
861
+ }
862
+
863
+ // SANDWORM_MODE R8: dns.resolve detection moved to walk.ancestor() in ast.js (FIX 5)
737
864
  }
738
865
 
739
866
  function handleImportExpression(node, ctx) {
@@ -939,6 +1066,10 @@ function handleMemberExpression(node, ctx) {
939
1066
  file: ctx.relFile
940
1067
  });
941
1068
  }
1069
+ // SANDWORM_MODE R9: Count LLM API key accesses
1070
+ if (LLM_API_KEY_VARS.includes(envVar)) {
1071
+ ctx.llmApiKeyCount++;
1072
+ }
942
1073
  }
943
1074
  }
944
1075
  }
@@ -946,9 +1077,11 @@ function handleMemberExpression(node, ctx) {
946
1077
  function handlePostWalk(ctx) {
947
1078
  // SANDWORM_MODE: zlib inflate + base64 decode + eval/Function/Module._compile = obfuscated payload
948
1079
  if (ctx.hasZlibInflate && ctx.hasBase64Decode && ctx.hasDynamicExec) {
1080
+ // FIX 4: dist/build files get LOW severity (bundlers legitimately use zlib+base64+eval)
1081
+ const isDistFile = /^(dist|build)[/\\]/i.test(ctx.relFile) || /\.bundle\.js$/i.test(ctx.relFile);
949
1082
  ctx.threats.push({
950
1083
  type: 'zlib_inflate_eval',
951
- severity: 'CRITICAL',
1084
+ severity: isDistFile ? 'LOW' : 'CRITICAL',
952
1085
  message: 'Obfuscated payload: zlib inflate + base64 decode + dynamic execution. No legitimate package uses this pattern.',
953
1086
  file: ctx.relFile
954
1087
  });
@@ -964,6 +1097,36 @@ function handlePostWalk(ctx) {
964
1097
  });
965
1098
  }
966
1099
 
1100
+ // SANDWORM_MODE R7: env harvesting = Object.entries/keys/values(process.env) + sensitive pattern in file
1101
+ if (ctx.hasEnvEnumeration && ctx.hasEnvHarvestPattern && ctx.hasNetworkCallInFile) {
1102
+ ctx.threats.push({
1103
+ type: 'env_harvesting_dynamic',
1104
+ severity: 'HIGH',
1105
+ message: 'Dynamic environment variable harvesting with sensitive pattern matching. Credential theft technique.',
1106
+ file: ctx.relFile
1107
+ });
1108
+ }
1109
+
1110
+ // SANDWORM_MODE R8: DNS exfiltration = dns require + base64 encode + dns call (loop implied by co-occurrence)
1111
+ if (ctx.hasDnsLoop) {
1112
+ ctx.threats.push({
1113
+ type: 'dns_chunk_exfiltration',
1114
+ severity: 'HIGH',
1115
+ message: 'DNS exfiltration: data encoded in DNS queries. Covert channel for firewall bypass.',
1116
+ file: ctx.relFile
1117
+ });
1118
+ }
1119
+
1120
+ // SANDWORM_MODE R9: LLM API key harvesting (3+ different providers = harvesting)
1121
+ if (ctx.llmApiKeyCount >= 3) {
1122
+ ctx.threats.push({
1123
+ type: 'llm_api_key_harvesting',
1124
+ severity: 'MEDIUM',
1125
+ message: `LLM API key harvesting: accessing ${ctx.llmApiKeyCount} AI provider keys. Monetization vector.`,
1126
+ file: ctx.relFile
1127
+ });
1128
+ }
1129
+
967
1130
  // JS reverse shell pattern
968
1131
  if (ctx.hasJsReverseShell) {
969
1132
  ctx.threats.push({
@@ -84,7 +84,17 @@ function analyzeFile(content, filePath, basePath) {
84
84
  hasTempFileWrite: false,
85
85
  hasTempFileExec: false,
86
86
  hasFileDelete: false,
87
- hasTmpdirInContent: /\btmpdir\b|\/dev\/shm\b|\/tmp\b/i.test(content)
87
+ hasDevShmInContent: /\/dev\/shm\b/.test(content),
88
+ // SANDWORM_MODE P2: env harvesting co-occurrence
89
+ hasEnvEnumeration: false, // Object.entries/keys/values(process.env)
90
+ hasEnvHarvestPattern: /\b(KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|NPM|AWS|SSH|WEBHOOK)\b/.test(content),
91
+ hasNetworkCallInFile: /\b(fetch|https?\.request|https?\.get|dns\.resolve)\b/.test(content),
92
+ // SANDWORM_MODE P2: DNS exfiltration co-occurrence
93
+ hasDnsRequire: /\brequire\s*\(\s*['"]dns['"]\s*\)/.test(content) || /\bdns\s*\.\s*resolve/.test(content),
94
+ hasBase64Encode: /\.toString\s*\(\s*['"]base64(url)?['"]\s*\)/.test(content),
95
+ hasDnsLoop: false, // set when dns call inside loop context detected
96
+ // SANDWORM_MODE P2: LLM API key harvesting
97
+ llmApiKeyCount: 0
88
98
  };
89
99
 
90
100
  walk.simple(ast, {
@@ -97,6 +107,35 @@ function analyzeFile(content, filePath, basePath) {
97
107
  MemberExpression(node) { handleMemberExpression(node, ctx); }
98
108
  });
99
109
 
110
+ // FIX 5: DNS chunk exfiltration — verify dns.resolve* is inside a loop body
111
+ if (ctx.hasDnsRequire && ctx.hasBase64Encode && !ctx.hasDnsLoop) {
112
+ walk.ancestor(ast, {
113
+ CallExpression(node, _state, ancestors) {
114
+ if (ctx.hasDnsLoop) return;
115
+ if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
116
+ const name = node.callee.property.name;
117
+ if (['resolve', 'resolve4', 'resolveTxt', 'resolveCname'].includes(name)) {
118
+ for (const anc of ancestors) {
119
+ if (['ForStatement', 'WhileStatement', 'ForOfStatement',
120
+ 'ForInStatement', 'DoWhileStatement'].includes(anc.type)) {
121
+ ctx.hasDnsLoop = true;
122
+ return;
123
+ }
124
+ // forEach/map callback = implicit loop
125
+ if (anc.type === 'CallExpression' && anc.callee?.type === 'MemberExpression') {
126
+ const m = anc.callee.property?.name;
127
+ if (['forEach', 'map', 'reduce', 'filter'].includes(m)) {
128
+ ctx.hasDnsLoop = true;
129
+ return;
130
+ }
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+ });
137
+ }
138
+
100
139
  handlePostWalk(ctx);
101
140
 
102
141
  return threats;