muaddib-scanner 2.4.6 → 2.4.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.4.6",
3
+ "version": "2.4.7",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -387,6 +387,21 @@ const PLAYBOOKS = {
387
387
  'Domaine C2 ou d\'exfiltration detecte dans le code source. Ces domaines (oastify.com, webhook.site, ngrok.io, etc.) ' +
388
388
  'sont utilises pour recevoir des donnees volees ou relayer des commandes. Verifier si le package a une raison ' +
389
389
  'legitime d\'utiliser ce domaine. Bloquer les connexions sortantes vers ce domaine.',
390
+
391
+ fetch_decrypt_exec:
392
+ 'CRITIQUE: Chaine steganographique detectee. Le code telecharge un fichier distant, le dechiffre via crypto, ' +
393
+ 'puis execute le resultat via eval/Function. Pattern buildrunner-dev: payload malveillant cache dans une image PNG. ' +
394
+ 'Isoler immediatement. Analyser le payload dechiffre. Supprimer le package.',
395
+
396
+ download_exec_binary:
397
+ 'CRITIQUE: Pattern download-execute detecte. Le code telecharge un binaire, le rend executable (chmod 755), ' +
398
+ 'puis l\'execute via execSync. Dropper deguise en compilation native addon (NeoShadow pattern). ' +
399
+ 'Bloquer les telechargements. Verifier les binaires ecrits sur disque. Supprimer le package.',
400
+
401
+ ide_persistence:
402
+ 'Persistence IDE detectee. Le code ecrit dans tasks.json ou la configuration VS Code avec execution automatique ' +
403
+ 'a l\'ouverture du dossier (runOn: folderOpen, reveal: silent). Pattern FAMOUS CHOLLIMA / StegaBin. ' +
404
+ 'Verifier ~/.config/Code/User/tasks.json et supprimer les taches inconnues.',
390
405
  };
391
406
 
392
407
  function getPlaybook(threatType) {
@@ -1103,6 +1103,44 @@ const RULES = {
1103
1103
  ],
1104
1104
  mitre: 'T1071.001'
1105
1105
  },
1106
+
1107
+ fetch_decrypt_exec: {
1108
+ id: 'MUADDIB-AST-033',
1109
+ name: 'Steganographic Payload Chain',
1110
+ severity: 'CRITICAL',
1111
+ confidence: 'high',
1112
+ description: 'Chaine steganographique: fetch distant + dechiffrement crypto + execution dynamique (eval/Function). Pattern buildrunner-dev: payload cache dans une image, dechiffre a runtime, puis execute.',
1113
+ references: [
1114
+ 'https://attack.mitre.org/techniques/T1027/003/',
1115
+ 'https://attack.mitre.org/techniques/T1140/'
1116
+ ],
1117
+ mitre: 'T1027.003'
1118
+ },
1119
+
1120
+ download_exec_binary: {
1121
+ id: 'MUADDIB-AST-034',
1122
+ name: 'Download-Execute Binary Pattern',
1123
+ severity: 'CRITICAL',
1124
+ confidence: 'high',
1125
+ description: 'Pattern download-execute: telechargement distant + chmod executable + execSync dans le meme fichier. Dropper binaire deguise en compilation native addon (NeoShadow pattern).',
1126
+ references: [
1127
+ 'https://attack.mitre.org/techniques/T1105/',
1128
+ 'https://attack.mitre.org/techniques/T1059/'
1129
+ ],
1130
+ mitre: 'T1105'
1131
+ },
1132
+
1133
+ ide_persistence: {
1134
+ id: 'MUADDIB-AST-035',
1135
+ name: 'IDE Task Persistence',
1136
+ severity: 'HIGH',
1137
+ confidence: 'high',
1138
+ description: 'Persistence IDE: ecriture dans tasks.json ou Code/User/ avec execution automatique a l\'ouverture du dossier (runOn: folderOpen). Pattern FAMOUS CHOLLIMA / StegaBin pour persistance VS Code.',
1139
+ references: [
1140
+ 'https://attack.mitre.org/techniques/T1546/'
1141
+ ],
1142
+ mitre: 'T1546'
1143
+ },
1106
1144
  };
1107
1145
 
1108
1146
  function getRule(type) {
@@ -154,6 +154,35 @@ function extractStringValue(node) {
154
154
  return null;
155
155
  }
156
156
 
157
+ /**
158
+ * Recursively resolve BinaryExpression with '+' operator to reconstruct
159
+ * concatenated strings like '.gi' + 't' → '.git' or 'ho' + 'oks' → 'hooks'.
160
+ * Returns null if any part is non-literal.
161
+ */
162
+ function resolveStringConcat(node) {
163
+ if (!node) return null;
164
+ if (node.type === 'Literal' && typeof node.value === 'string') return node.value;
165
+ if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
166
+ return node.quasis.map(q => q.value.raw).join('');
167
+ }
168
+ if (node.type === 'BinaryExpression' && node.operator === '+') {
169
+ const left = resolveStringConcat(node.left);
170
+ const right = resolveStringConcat(node.right);
171
+ if (left !== null && right !== null) return left + right;
172
+ }
173
+ return null;
174
+ }
175
+
176
+ /**
177
+ * Extract string value from a node, including BinaryExpression resolution.
178
+ * Falls back to extractStringValue if concat resolution fails.
179
+ */
180
+ function extractStringValueDeep(node) {
181
+ const concat = resolveStringConcat(node);
182
+ if (concat !== null) return concat;
183
+ return extractStringValue(node);
184
+ }
185
+
157
186
  /**
158
187
  * Returns true if all arguments of a call/new expression are string literals.
159
188
  * Used to distinguish safe patterns like eval('1+2') or Function('return this')
@@ -276,7 +305,7 @@ function handleVariableDeclarator(node, ctx) {
276
305
  const prop = node.init.callee.property;
277
306
  if (obj?.type === 'Identifier' && obj.name === 'path' &&
278
307
  prop?.type === 'Identifier' && (prop.name === 'join' || prop.name === 'resolve')) {
279
- const joinArgs = node.init.arguments.map(a => extractStringValue(a) || '').join('/');
308
+ const joinArgs = node.init.arguments.map(a => extractStringValueDeep(a) || '').join('/');
280
309
  if (/\.github[\\/\/]workflows/i.test(joinArgs) || /\.github[\\/\/]actions/i.test(joinArgs)) {
281
310
  ctx.workflowPathVars.add(node.id.name);
282
311
  }
@@ -284,6 +313,32 @@ function handleVariableDeclarator(node, ctx) {
284
313
  else if (node.init.arguments.some(a => a.type === 'Identifier' && ctx.workflowPathVars.has(a.name))) {
285
314
  ctx.workflowPathVars.add(node.id.name);
286
315
  }
316
+ // Track path.join that resolves to .git/hooks (concat fragments included)
317
+ if (/\.git[\\/\/]hooks/i.test(joinArgs) ||
318
+ (GIT_HOOKS.some(h => joinArgs.includes(h)) && joinArgs.includes('.git'))) {
319
+ ctx.gitHooksPathVars.set(node.id.name, joinArgs);
320
+ }
321
+ // Propagate: path.join(gitHooksPathVar, ...) inherits tracking
322
+ else if (node.init.arguments.some(a => a.type === 'Identifier' && ctx.gitHooksPathVars.has(a.name))) {
323
+ const parentPath = node.init.arguments.map(a => {
324
+ if (a.type === 'Identifier' && ctx.gitHooksPathVars.has(a.name)) return ctx.gitHooksPathVars.get(a.name);
325
+ return extractStringValueDeep(a) || '';
326
+ }).join('/');
327
+ ctx.gitHooksPathVars.set(node.id.name, parentPath);
328
+ }
329
+ // Track path.join that resolves to IDE config paths (.claude/, .cursor/, etc.)
330
+ const joinLower = joinArgs.toLowerCase();
331
+ if (MCP_CONFIG_PATHS.some(p => joinLower.includes(p.toLowerCase()))) {
332
+ ctx.ideConfigPathVars.set(node.id.name, joinArgs);
333
+ }
334
+ // Propagate: path.join(ideConfigPathVar, ...) inherits tracking
335
+ else if (node.init.arguments.some(a => a.type === 'Identifier' && ctx.ideConfigPathVars.has(a.name))) {
336
+ const parentPath = node.init.arguments.map(a => {
337
+ if (a.type === 'Identifier' && ctx.ideConfigPathVars.has(a.name)) return ctx.ideConfigPathVars.get(a.name);
338
+ return extractStringValueDeep(a) || '';
339
+ }).join('/');
340
+ ctx.ideConfigPathVars.set(node.id.name, parentPath);
341
+ }
287
342
  }
288
343
  }
289
344
  }
@@ -336,6 +391,11 @@ function handleCallExpression(node, ctx) {
336
391
  file: ctx.relFile
337
392
  });
338
393
  }
394
+ // Wave 4: detect require() of .node binary files (native addon camouflage)
395
+ const reqStr = extractStringValueDeep(arg);
396
+ if (reqStr && /\.node\s*$/.test(reqStr)) {
397
+ ctx.hasRequireNodeFile = true;
398
+ }
339
399
  }
340
400
 
341
401
  // Detect exec/execSync with dangerous shell commands (direct or via MemberExpression)
@@ -345,6 +405,8 @@ function handleCallExpression(node, ctx) {
345
405
  (node.callee.property.name === 'exec' || node.callee.property.name === 'execSync')
346
406
  ? node.callee.property.name : null;
347
407
  if ((execName || memberExec) && node.arguments.length > 0) {
408
+ // Wave 4: track any execSync/exec call for compound detection
409
+ ctx.hasExecSyncCall = true;
348
410
  const arg = node.arguments[0];
349
411
  let cmdStr = null;
350
412
  if (arg.type === 'Literal' && typeof arg.value === 'string') {
@@ -560,13 +622,18 @@ function handleCallExpression(node, ctx) {
560
622
  const mcpWriteMethod = node.callee.property.name;
561
623
  if (['writeFileSync', 'writeFile'].includes(mcpWriteMethod) && node.arguments.length >= 2) {
562
624
  const mcpPathArg = node.arguments[0];
563
- const mcpPathStr = extractStringValue(mcpPathArg);
564
- // Also check path.join() calls
625
+ const mcpPathStr = extractStringValueDeep(mcpPathArg);
626
+ // Also check path.join() calls — resolve concat fragments in each argument
565
627
  let mcpJoinedPath = '';
566
628
  if (mcpPathArg?.type === 'CallExpression' && mcpPathArg.arguments) {
567
- mcpJoinedPath = mcpPathArg.arguments.map(a => extractStringValue(a) || '').join('/');
629
+ mcpJoinedPath = mcpPathArg.arguments.map(a => extractStringValueDeep(a) || '').join('/');
630
+ }
631
+ // Also check if path arg is a variable tracked as MCP/IDE config path
632
+ let mcpVarPath = '';
633
+ if (mcpPathArg?.type === 'Identifier' && ctx.ideConfigPathVars.has(mcpPathArg.name)) {
634
+ mcpVarPath = ctx.ideConfigPathVars.get(mcpPathArg.name);
568
635
  }
569
- const mcpCheckPath = (mcpPathStr || mcpJoinedPath).toLowerCase();
636
+ const mcpCheckPath = (mcpPathStr || mcpJoinedPath || mcpVarPath).toLowerCase();
570
637
  const isMcpPath = MCP_CONFIG_PATHS.some(p => mcpCheckPath.includes(p.toLowerCase()));
571
638
  if (isMcpPath) {
572
639
  // Check content argument for MCP-related patterns
@@ -592,14 +659,19 @@ function handleCallExpression(node, ctx) {
592
659
  const gitWriteMethod = node.callee.property.name;
593
660
  if (['writeFileSync', 'writeFile'].includes(gitWriteMethod) && node.arguments.length >= 1) {
594
661
  const gitPathArg = node.arguments[0];
595
- const gitPathStr = extractStringValue(gitPathArg);
662
+ const gitPathStr = extractStringValueDeep(gitPathArg);
596
663
  let gitJoinedPath = '';
597
664
  if (gitPathArg?.type === 'CallExpression' && gitPathArg.arguments) {
598
- gitJoinedPath = gitPathArg.arguments.map(a => extractStringValue(a) || '').join('/');
665
+ gitJoinedPath = gitPathArg.arguments.map(a => extractStringValueDeep(a) || '').join('/');
599
666
  }
600
- const gitCheckPath = gitPathStr || gitJoinedPath;
667
+ // Also check if path arg is a variable tracked as git hooks path
668
+ let gitVarPath = '';
669
+ if (gitPathArg?.type === 'Identifier' && ctx.gitHooksPathVars.has(gitPathArg.name)) {
670
+ gitVarPath = ctx.gitHooksPathVars.get(gitPathArg.name);
671
+ }
672
+ const gitCheckPath = gitPathStr || gitJoinedPath || gitVarPath;
601
673
  if (/\.git[\\/]hooks[\\/]/i.test(gitCheckPath) ||
602
- GIT_HOOKS.some(h => gitCheckPath.includes(h) && gitCheckPath.includes('.git'))) {
674
+ GIT_HOOKS.some(h => gitCheckPath.includes(h) && (gitCheckPath.includes('.git') || gitCheckPath.includes('hooks')))) {
603
675
  ctx.threats.push({
604
676
  type: 'git_hooks_injection',
605
677
  severity: 'HIGH',
@@ -633,6 +705,43 @@ function handleCallExpression(node, ctx) {
633
705
  }
634
706
  }
635
707
 
708
+ // Wave 4: IDE persistence — writeFileSync to VS Code tasks.json or Code/User/ paths
709
+ if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
710
+ const ideWriteMethod = node.callee.property.name;
711
+ if (['writeFileSync', 'writeFile'].includes(ideWriteMethod) && node.arguments.length >= 1) {
712
+ const idePathArg = node.arguments[0];
713
+ const idePathStr = extractStringValueDeep(idePathArg);
714
+ let ideJoinedPath = '';
715
+ if (idePathArg?.type === 'CallExpression' && idePathArg.arguments) {
716
+ ideJoinedPath = idePathArg.arguments.map(a => extractStringValueDeep(a) || '').join('/');
717
+ }
718
+ let ideVarPath = '';
719
+ if (idePathArg?.type === 'Identifier' && ctx.ideConfigPathVars.has(idePathArg.name)) {
720
+ ideVarPath = ctx.ideConfigPathVars.get(idePathArg.name);
721
+ }
722
+ const ideCheckPath = (idePathStr || ideJoinedPath || ideVarPath).toLowerCase();
723
+ if ((ideCheckPath.includes('tasks.json') || ideCheckPath.includes('code/user/') || ideCheckPath.includes('.vscode/')) &&
724
+ !ctx.hasIdePersistenceWrite) {
725
+ // Check content for task runner persistence patterns (runOn, folderOpen)
726
+ const ideContentArg = node.arguments[1];
727
+ const ideContentStr = extractStringValue(ideContentArg);
728
+ const hasPersistencePattern = ideContentStr
729
+ ? /runOn|folderOpen|reveal.*silent/.test(ideContentStr)
730
+ : true; // dynamic content targeting IDE task paths = suspicious
731
+ if (hasPersistencePattern) {
732
+ ctx.hasIdePersistenceWrite = true;
733
+ ctx.threats.push({
734
+ type: 'ide_persistence',
735
+ severity: 'HIGH',
736
+ message: `IDE persistence: ${ideWriteMethod}() writes to IDE task configuration (${ideCheckPath}). Auto-execution on folder open.`,
737
+ file: ctx.relFile
738
+ });
739
+ }
740
+ }
741
+ }
742
+ }
743
+
744
+
636
745
  // Detect fs.chmodSync with executable permissions (deferred to postWalk for compound check)
637
746
  if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
638
747
  const chmodMethod = node.callee.property.name;
@@ -926,6 +1035,7 @@ function handleNewExpression(node, ctx) {
926
1035
  if (node.callee.type === 'Identifier' && node.callee.name === 'Function') {
927
1036
  // Skip string literal args — zero-risk globalThis polyfills used by every bundler
928
1037
  if (!hasOnlyStringLiteralArgs(node)) {
1038
+ ctx.hasDynamicExec = true;
929
1039
  ctx.threats.push({
930
1040
  type: 'dangerous_call_function',
931
1041
  severity: 'MEDIUM',
@@ -1222,6 +1332,47 @@ function handlePostWalk(ctx) {
1222
1332
  file: ctx.relFile
1223
1333
  });
1224
1334
  }
1335
+
1336
+ // Wave 4: Remote fetch + crypto decrypt + dynamic eval = steganographic payload chain
1337
+ if (ctx.hasRemoteFetch && ctx.hasCryptoDecipher && ctx.hasDynamicExec) {
1338
+ ctx.threats.push({
1339
+ type: 'fetch_decrypt_exec',
1340
+ severity: 'CRITICAL',
1341
+ message: 'Steganographic payload chain: remote fetch + crypto decryption + dynamic execution. No legitimate package uses this pattern.',
1342
+ file: ctx.relFile
1343
+ });
1344
+ }
1345
+
1346
+ // Wave 4: Download-execute-cleanup — https download + chmod executable + execSync + unlink
1347
+ if (ctx.hasRemoteFetch && ctx.hasChmodExecutable && ctx.hasExecSyncCall) {
1348
+ ctx.threats.push({
1349
+ type: 'download_exec_binary',
1350
+ severity: 'CRITICAL',
1351
+ message: 'Download-execute pattern: remote fetch + chmod executable + execSync in same file. Binary dropper camouflaged as native addon build.',
1352
+ file: ctx.relFile
1353
+ });
1354
+ }
1355
+
1356
+ // Wave 4: IDE persistence via content co-occurrence — tasks.json + runOn + writeFileSync
1357
+ if (!ctx.hasIdePersistenceWrite && ctx.hasTasksJsonInContent && ctx.hasRunOnInContent && ctx.hasWriteFileSyncInContent) {
1358
+ ctx.hasIdePersistenceWrite = true;
1359
+ ctx.threats.push({
1360
+ type: 'ide_persistence',
1361
+ severity: 'HIGH',
1362
+ message: 'IDE persistence: writes tasks.json with auto-execution trigger (runOn/folderOpen). VS Code task persistence technique.',
1363
+ file: ctx.relFile
1364
+ });
1365
+ }
1366
+
1367
+ // Wave 4: MCP content keywords in file with writeFileSync = MCP injection signal
1368
+ if (ctx.hasMcpContentKeywords && !ctx.threats.some(t => t.type === 'mcp_config_injection')) {
1369
+ ctx.threats.push({
1370
+ type: 'mcp_config_injection',
1371
+ severity: 'CRITICAL',
1372
+ message: 'MCP config injection: code contains MCP server configuration keywords (mcpServers/mcp.json/claude_desktop_config) with filesystem writes. AI toolchain poisoning.',
1373
+ file: ctx.relFile
1374
+ });
1375
+ }
1225
1376
  }
1226
1377
 
1227
1378
  module.exports = {
@@ -94,7 +94,24 @@ function analyzeFile(content, filePath, basePath) {
94
94
  hasBase64Encode: /\.toString\s*\(\s*['"]base64(url)?['"]\s*\)/.test(content),
95
95
  hasDnsLoop: false, // set when dns call inside loop context detected
96
96
  // SANDWORM_MODE P2: LLM API key harvesting
97
- llmApiKeyCount: 0
97
+ llmApiKeyCount: 0,
98
+ // Wave 4: path variable tracking for git hooks and IDE config injection
99
+ gitHooksPathVars: new Map(),
100
+ ideConfigPathVars: new Map(),
101
+ // Wave 4: compound detection — fetch + decrypt + eval chain
102
+ hasRemoteFetch: /\bhttps?\.(get|request)\b/.test(content) || /\bfetch\s*\(/.test(content),
103
+ hasCryptoDecipher: /\bcreateDecipher(iv)?\s*\(/.test(content),
104
+ // Wave 4: native addon camouflage signals
105
+ hasRequireNodeFile: false,
106
+ hasExecSyncCall: false,
107
+ // Wave 4: IDE persistence (VS Code tasks.json, Code/User/ paths)
108
+ hasIdePersistenceWrite: false,
109
+ hasTasksJsonInContent: /\btasks\.json\b/.test(content),
110
+ hasRunOnInContent: /\brunOn\b|\bfolderOpen\b/.test(content),
111
+ hasWriteFileSyncInContent: /\bwriteFileSync\b|\bwriteFile\s*\(/.test(content),
112
+ // Wave 4: MCP content keyword detection (must also have writeFileSync in same file)
113
+ hasMcpContentKeywords: (/\bmcpServers\b/.test(content) || /\bmcp\.json\b/.test(content) || /\bclaude_desktop_config\b/.test(content)) &&
114
+ /\bwriteFileSync\b|\bwriteFile\s*\(/.test(content)
98
115
  };
99
116
 
100
117
  walk.simple(ast, {