muaddib-scanner 2.2.26 → 2.2.28

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.
@@ -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;
@@ -565,6 +675,7 @@ function handleCallExpression(node, ctx) {
565
675
 
566
676
  if (callName === 'eval') {
567
677
  ctx.hasEvalInFile = true;
678
+ ctx.hasDynamicExec = true;
568
679
  // Detect staged eval decode
569
680
  if (node.arguments.length === 1 && hasDecodeArg(node.arguments[0])) {
570
681
  ctx.threats.push({
@@ -585,6 +696,7 @@ function handleCallExpression(node, ctx) {
585
696
  });
586
697
  }
587
698
  } else if (callName === 'Function') {
699
+ ctx.hasDynamicExec = true;
588
700
  // Detect staged Function decode
589
701
  if (node.arguments.length >= 1 && hasDecodeArg(node.arguments[node.arguments.length - 1])) {
590
702
  ctx.threats.push({
@@ -679,12 +791,83 @@ function handleCallExpression(node, ctx) {
679
791
  });
680
792
  }
681
793
  if (propName === '_compile') {
794
+ ctx.hasDynamicExec = true;
682
795
  ctx.threats.push({
683
796
  type: 'module_compile',
684
797
  severity: 'CRITICAL',
685
798
  message: 'module._compile() detected — executes arbitrary code from string in module context (flatmap-stream pattern).',
686
799
  file: ctx.relFile
687
800
  });
801
+ // SANDWORM_MODE: Module._compile with non-literal argument = dynamic code execution
802
+ if (node.arguments.length >= 1 && !hasOnlyStringLiteralArgs(node)) {
803
+ ctx.threats.push({
804
+ type: 'module_compile_dynamic',
805
+ severity: 'CRITICAL',
806
+ message: 'In-memory code execution via Module._compile(). Common malware evasion technique.',
807
+ file: ctx.relFile
808
+ });
809
+ }
810
+ // Module._compile counts as temp file exec for write-execute-delete pattern
811
+ ctx.hasTempFileExec = ctx.hasTempFileExec || ctx.hasTmpdirInContent;
812
+ }
813
+
814
+ // SANDWORM_MODE: Track writeFileSync/writeFile to temp paths
815
+ if (propName === 'writeFileSync' || propName === 'writeFile') {
816
+ const arg = node.arguments && node.arguments[0];
817
+ if (arg) {
818
+ const strVal = extractStringValue(arg);
819
+ if (strVal && (/\/dev\/shm\b/.test(strVal) || /\btmp\b/i.test(strVal) || /\btemp\b/i.test(strVal))) {
820
+ ctx.hasTempFileWrite = true;
821
+ }
822
+ // Variable reference to tmpdir/temp path
823
+ if (!strVal && (arg.type === 'Identifier' || arg.type === 'CallExpression' || arg.type === 'MemberExpression')) {
824
+ // Dynamic path — check if file content involves tmpdir patterns
825
+ ctx.hasTempFileWrite = ctx.hasTempFileWrite || ctx.hasTmpdirInContent;
826
+ }
827
+ }
828
+ }
829
+
830
+ // SANDWORM_MODE: Track unlinkSync/rmSync (file deletion)
831
+ if (propName === 'unlinkSync' || propName === 'unlink' || propName === 'rmSync') {
832
+ ctx.hasFileDelete = true;
833
+ }
834
+ }
835
+
836
+ // SANDWORM_MODE: Track require() of temp path (execution of temp file)
837
+ if (callName === 'require' && node.arguments.length > 0) {
838
+ const arg = node.arguments[0];
839
+ const strVal = extractStringValue(arg);
840
+ if (strVal && (/\/dev\/shm\b/.test(strVal) || /\btmp\b/i.test(strVal) || /\btemp\b/i.test(strVal))) {
841
+ ctx.hasTempFileExec = true;
842
+ } else if (!strVal && ctx.hasTmpdirInContent) {
843
+ // Variable argument in a file that references tmpdir paths
844
+ ctx.hasTempFileExec = true;
845
+ }
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: Detect dns.resolve/resolve4/resolveTxt calls (flag for co-occurrence)
864
+ if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
865
+ const dnsPropName = node.callee.property.name;
866
+ if (['resolve', 'resolve4', 'resolveTxt', 'resolveCname'].includes(dnsPropName)) {
867
+ // Set hasDnsLoop if file has dns require + base64 encoding (co-occurrence checked in postWalk)
868
+ if (ctx.hasDnsRequire && ctx.hasBase64Encode) {
869
+ ctx.hasDnsLoop = true;
870
+ }
688
871
  }
689
872
  }
690
873
  }
@@ -892,11 +1075,65 @@ function handleMemberExpression(node, ctx) {
892
1075
  file: ctx.relFile
893
1076
  });
894
1077
  }
1078
+ // SANDWORM_MODE R9: Count LLM API key accesses
1079
+ if (LLM_API_KEY_VARS.includes(envVar)) {
1080
+ ctx.llmApiKeyCount++;
1081
+ }
895
1082
  }
896
1083
  }
897
1084
  }
898
1085
 
899
1086
  function handlePostWalk(ctx) {
1087
+ // SANDWORM_MODE: zlib inflate + base64 decode + eval/Function/Module._compile = obfuscated payload
1088
+ if (ctx.hasZlibInflate && ctx.hasBase64Decode && ctx.hasDynamicExec) {
1089
+ ctx.threats.push({
1090
+ type: 'zlib_inflate_eval',
1091
+ severity: 'CRITICAL',
1092
+ message: 'Obfuscated payload: zlib inflate + base64 decode + dynamic execution. No legitimate package uses this pattern.',
1093
+ file: ctx.relFile
1094
+ });
1095
+ }
1096
+
1097
+ // SANDWORM_MODE: write + execute + delete = anti-forensics staging
1098
+ if (ctx.hasTempFileWrite && ctx.hasTempFileExec && ctx.hasFileDelete) {
1099
+ ctx.threats.push({
1100
+ type: 'write_execute_delete',
1101
+ severity: 'HIGH',
1102
+ message: 'Anti-forensics: write, execute, then delete. Typical malware staging pattern.',
1103
+ file: ctx.relFile
1104
+ });
1105
+ }
1106
+
1107
+ // SANDWORM_MODE R7: env harvesting = Object.entries/keys/values(process.env) + sensitive pattern in file
1108
+ if (ctx.hasEnvEnumeration && ctx.hasEnvHarvestPattern) {
1109
+ ctx.threats.push({
1110
+ type: 'env_harvesting_dynamic',
1111
+ severity: 'HIGH',
1112
+ message: 'Dynamic environment variable harvesting with sensitive pattern matching. Credential theft technique.',
1113
+ file: ctx.relFile
1114
+ });
1115
+ }
1116
+
1117
+ // SANDWORM_MODE R8: DNS exfiltration = dns require + base64 encode + dns call (loop implied by co-occurrence)
1118
+ if (ctx.hasDnsLoop) {
1119
+ ctx.threats.push({
1120
+ type: 'dns_chunk_exfiltration',
1121
+ severity: 'HIGH',
1122
+ message: 'DNS exfiltration: data encoded in DNS queries. Covert channel for firewall bypass.',
1123
+ file: ctx.relFile
1124
+ });
1125
+ }
1126
+
1127
+ // SANDWORM_MODE R9: LLM API key harvesting (3+ different providers = harvesting)
1128
+ if (ctx.llmApiKeyCount >= 3) {
1129
+ ctx.threats.push({
1130
+ type: 'llm_api_key_harvesting',
1131
+ severity: 'MEDIUM',
1132
+ message: `LLM API key harvesting: accessing ${ctx.llmApiKeyCount} AI provider keys. Monetization vector.`,
1133
+ file: ctx.relFile
1134
+ });
1135
+ }
1136
+
900
1137
  // JS reverse shell pattern
901
1138
  if (ctx.hasJsReverseShell) {
902
1139
  ctx.threats.push({
@@ -75,7 +75,25 @@ function analyzeFile(content, filePath, basePath) {
75
75
  /\.pipe\b/.test(content) &&
76
76
  (/\bspawn\b/.test(content) || /\bstdin\b/.test(content) || /\bstdout\b/.test(content)),
77
77
  hasBinaryFileLiteral: /\.(png|jpg|jpeg|gif|bmp|ico|wasm)\b/i.test(content),
78
- hasEvalInFile: false
78
+ hasEvalInFile: false,
79
+ // SANDWORM_MODE: zlib inflate + base64 + eval co-occurrence
80
+ hasZlibInflate: /\brequire\s*\(\s*['"]zlib['"]\s*\)/.test(content) || /\bzlib\s*\.\s*inflate/.test(content),
81
+ hasBase64Decode: /Buffer\.from\s*\([^)]*,\s*['"]base64['"]/.test(content),
82
+ hasDynamicExec: false, // set in handleCallExpression for eval/Function/Module._compile
83
+ // SANDWORM_MODE: write + execute + delete anti-forensics
84
+ hasTempFileWrite: false,
85
+ hasTempFileExec: false,
86
+ hasFileDelete: false,
87
+ hasTmpdirInContent: /\btmpdir\b|\/dev\/shm\b|\/tmp\b/i.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
+ // SANDWORM_MODE P2: DNS exfiltration co-occurrence
92
+ hasDnsRequire: /\brequire\s*\(\s*['"]dns['"]\s*\)/.test(content) || /\bdns\s*\.\s*resolve/.test(content),
93
+ hasBase64Encode: /\.toString\s*\(\s*['"]base64(url)?['"]\s*\)/.test(content),
94
+ hasDnsLoop: false, // set when dns call inside loop context detected
95
+ // SANDWORM_MODE P2: LLM API key harvesting
96
+ llmApiKeyCount: 0
79
97
  };
80
98
 
81
99
  walk.simple(ast, {