muaddib-scanner 2.2.0 → 2.2.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 (28) hide show
  1. package/datasets/holdout-v2/conditional-os-payload/index.js +36 -0
  2. package/datasets/holdout-v2/conditional-os-payload/package.json +6 -0
  3. package/datasets/holdout-v2/env-var-reconstruction/index.js +21 -0
  4. package/datasets/holdout-v2/env-var-reconstruction/package.json +6 -0
  5. package/datasets/holdout-v2/github-workflow-inject/index.js +36 -0
  6. package/datasets/holdout-v2/github-workflow-inject/package.json +6 -0
  7. package/datasets/holdout-v2/homedir-ssh-key-steal/index.js +29 -0
  8. package/datasets/holdout-v2/homedir-ssh-key-steal/package.json +6 -0
  9. package/datasets/holdout-v2/npm-cache-poison/index.js +38 -0
  10. package/datasets/holdout-v2/npm-cache-poison/package.json +6 -0
  11. package/datasets/holdout-v2/npm-lifecycle-preinstall-curl/package.json +8 -0
  12. package/datasets/holdout-v2/process-env-proxy-getter/index.js +35 -0
  13. package/datasets/holdout-v2/process-env-proxy-getter/package.json +6 -0
  14. package/datasets/holdout-v2/readable-stream-hijack/index.js +44 -0
  15. package/datasets/holdout-v2/readable-stream-hijack/package.json +6 -0
  16. package/datasets/holdout-v2/setTimeout-chain/index.js +50 -0
  17. package/datasets/holdout-v2/setTimeout-chain/package.json +6 -0
  18. package/datasets/holdout-v2/wasm-loader/index.js +46 -0
  19. package/datasets/holdout-v2/wasm-loader/package.json +6 -0
  20. package/metrics/v2.1.5.json +752 -752
  21. package/metrics/v2.2.0.json +752 -752
  22. package/package.json +3 -3
  23. package/src/response/playbooks.js +15 -0
  24. package/src/rules/index.js +39 -1
  25. package/src/scanner/ast.js +93 -3
  26. package/src/scanner/dataflow.js +54 -4
  27. package/src/scanner/package.js +13 -0
  28. package/iocs.json.gz +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -43,7 +43,7 @@
43
43
  "node": ">=18.0.0"
44
44
  },
45
45
  "dependencies": {
46
- "@inquirer/prompts": "8.2.0",
46
+ "@inquirer/prompts": "8.2.1",
47
47
  "acorn": "8.15.0",
48
48
  "acorn-walk": "8.3.4",
49
49
  "adm-zip": "^0.5.16",
@@ -52,7 +52,7 @@
52
52
  "yargs": "18.0.0"
53
53
  },
54
54
  "devDependencies": {
55
- "@eslint/js": "9.39.2",
55
+ "@eslint/js": "10.0.1",
56
56
  "eslint": "10.0.0",
57
57
  "eslint-plugin-security": "^3.0.1",
58
58
  "globals": "17.3.0"
@@ -293,6 +293,21 @@ const PLAYBOOKS = {
293
293
  'CRITIQUE: Le package a tente de voler des credentials (honey tokens). Comportement malveillant confirme. ' +
294
294
  'NE PAS installer. Signaler immediatement sur npm/PyPI. ' +
295
295
  'Si deja installe: considerer la machine compromise, regenerer TOUS les secrets.',
296
+
297
+ env_charcode_reconstruction:
298
+ 'Obfuscation detectee: le nom de la variable d\'environnement est reconstruit dynamiquement via fromCharCode ' +
299
+ 'pour eviter la detection statique. Technique de vol de GITHUB_TOKEN, NPM_TOKEN, etc. ' +
300
+ 'Verifier quelles variables sont accedees et si elles sont exfiltrees.',
301
+
302
+ lifecycle_shell_pipe:
303
+ 'CRITIQUE: Le script lifecycle (preinstall/postinstall) pipe du code distant vers un shell (curl | sh). ' +
304
+ 'NE PAS installer. Ceci execute du code arbitraire a l\'installation. ' +
305
+ 'Si deja installe: considerer la machine compromise. Auditer les modifications systeme.',
306
+
307
+ credential_tampering:
308
+ 'CRITIQUE: Ecriture detectee dans un cache sensible (npm _cacache, yarn, pip). ' +
309
+ 'Possible cache poisoning: injection de code malveillant dans des packages caches. ' +
310
+ 'Nettoyer le cache: npm cache clean --force. Reinstaller les dependances depuis zero.',
296
311
  };
297
312
 
298
313
  function getPlaybook(threatType) {
@@ -500,7 +500,7 @@ const RULES = {
500
500
  workflow_write: {
501
501
  id: 'MUADDIB-AST-015',
502
502
  name: 'GitHub Actions Workflow Write',
503
- severity: 'HIGH',
503
+ severity: 'CRITICAL',
504
504
  confidence: 'high',
505
505
  description: 'fs.writeFileSync cree un fichier dans .github/workflows — injection de workflow GitHub Actions pour persistence. Technique Shai-Hulud 2.0.',
506
506
  references: [
@@ -562,6 +562,44 @@ const RULES = {
562
562
  mitre: 'T1059'
563
563
  },
564
564
 
565
+ env_charcode_reconstruction: {
566
+ id: 'MUADDIB-AST-018',
567
+ name: 'Environment Variable Key Reconstruction',
568
+ severity: 'HIGH',
569
+ confidence: 'high',
570
+ description: 'process.env accede avec une cle reconstruite dynamiquement via String.fromCharCode. Technique d\'obfuscation pour eviter la detection statique des noms de variables sensibles (GITHUB_TOKEN, etc.).',
571
+ references: [
572
+ 'https://attack.mitre.org/techniques/T1027/',
573
+ 'https://attack.mitre.org/techniques/T1552/001/'
574
+ ],
575
+ mitre: 'T1027'
576
+ },
577
+
578
+ lifecycle_shell_pipe: {
579
+ id: 'MUADDIB-PKG-010',
580
+ name: 'Lifecycle Script Pipes to Shell',
581
+ severity: 'CRITICAL',
582
+ confidence: 'high',
583
+ description: 'Script lifecycle (preinstall/install/postinstall) execute curl | sh ou wget | bash — telecharge et execute du code distant au moment de npm install.',
584
+ references: [
585
+ 'https://blog.phylum.io/shai-hulud-npm-worm',
586
+ 'https://socket.dev/blog/2025-supply-chain-report'
587
+ ],
588
+ mitre: 'T1195.002'
589
+ },
590
+
591
+ credential_tampering: {
592
+ id: 'MUADDIB-FLOW-003',
593
+ name: 'Credential/Cache Tampering',
594
+ severity: 'CRITICAL',
595
+ confidence: 'high',
596
+ description: 'Ecriture dans un chemin sensible (cache npm _cacache, cache yarn, credentials). Possible cache poisoning: injection de code malveillant dans des packages caches.',
597
+ references: [
598
+ 'https://attack.mitre.org/techniques/T1565/001/'
599
+ ],
600
+ mitre: 'T1565.001'
601
+ },
602
+
565
603
  ai_agent_abuse: {
566
604
  id: 'MUADDIB-AST-013',
567
605
  name: 'AI Agent Weaponization',
@@ -78,6 +78,13 @@ const HOOKABLE_NATIVES = [
78
78
  'WebSocket', 'EventSource'
79
79
  ];
80
80
 
81
+ // Node.js core module classes targeted for prototype hooking
82
+ const NODE_HOOKABLE_MODULES = ['http', 'https', 'net', 'tls', 'stream'];
83
+ const NODE_HOOKABLE_CLASSES = [
84
+ 'IncomingMessage', 'ServerResponse', 'ClientRequest',
85
+ 'OutgoingMessage', 'Socket', 'Server', 'Agent'
86
+ ];
87
+
81
88
  // Paths indicating sandbox/container environment detection (anti-analysis)
82
89
  const SANDBOX_INDICATORS = [
83
90
  '/.dockerenv',
@@ -162,6 +169,20 @@ function analyzeFile(content, filePath, basePath) {
162
169
  allowHashBang: true
163
170
  });
164
171
  } catch {
172
+ // AST parse failed — apply regex fallback for known dangerous patterns
173
+
174
+ // Workflow manipulation: reads + writes to .github/workflows
175
+ if (/\.github/.test(content) && /workflows/.test(content) &&
176
+ /writeFileSync|writeFile/.test(content) &&
177
+ /readdirSync|readFileSync/.test(content)) {
178
+ threats.push({
179
+ type: 'workflow_write',
180
+ severity: 'CRITICAL',
181
+ message: 'File reads and modifies .github/workflows — GitHub Actions injection (regex fallback).',
182
+ file: path.relative(basePath, filePath)
183
+ });
184
+ }
185
+
165
186
  if (content.length > 1000 && content.split('\n').length < 10) {
166
187
  threats.push({
167
188
  type: 'possible_obfuscation',
@@ -198,6 +219,9 @@ function analyzeFile(content, filePath, basePath) {
198
219
  return null;
199
220
  }
200
221
 
222
+ // Pre-scan for fromCharCode pattern (env var name obfuscation)
223
+ const hasFromCharCode = content.includes('fromCharCode');
224
+
201
225
  walk.simple(ast, {
202
226
  VariableDeclarator(node) {
203
227
  if (node.id?.type === 'Identifier') {
@@ -232,6 +256,10 @@ function analyzeFile(content, filePath, basePath) {
232
256
  if (/\.github[\\/\/]workflows/i.test(joinArgs) || /\.github[\\/\/]actions/i.test(joinArgs)) {
233
257
  workflowPathVars.add(node.id.name);
234
258
  }
259
+ // Propagate: path.join(workflowPathVar, ...) inherits tracking
260
+ else if (node.init.arguments.some(a => a.type === 'Identifier' && workflowPathVars.has(a.name))) {
261
+ workflowPathVars.add(node.id.name);
262
+ }
235
263
  }
236
264
  }
237
265
  }
@@ -416,8 +444,8 @@ function analyzeFile(content, filePath, basePath) {
416
444
  if (hasWorkflowVar || /\.github[\\/]workflows/i.test(checkPath) || /\.github[\\/]actions/i.test(checkPath)) {
417
445
  threats.push({
418
446
  type: 'workflow_write',
419
- severity: 'HIGH',
420
- message: `${writeMethod}() creates file in .github/workflows — GitHub Actions persistence technique.`,
447
+ severity: 'CRITICAL',
448
+ message: `${writeMethod}() writes to .github/workflows — GitHub Actions injection/persistence technique.`,
421
449
  file: path.relative(basePath, filePath)
422
450
  });
423
451
  }
@@ -433,7 +461,7 @@ function analyzeFile(content, filePath, basePath) {
433
461
  if (pathArg?.type === 'Identifier' && workflowPathVars.has(pathArg.name)) {
434
462
  threats.push({
435
463
  type: 'workflow_write',
436
- severity: 'HIGH',
464
+ severity: 'CRITICAL',
437
465
  message: `${mkdirMethod}() creates .github/workflows directory — GitHub Actions persistence technique.`,
438
466
  file: path.relative(basePath, filePath)
439
467
  });
@@ -441,6 +469,22 @@ function analyzeFile(content, filePath, basePath) {
441
469
  }
442
470
  }
443
471
 
472
+ // Detect fs.readdirSync on .github/workflows — workflow enumeration for injection
473
+ if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
474
+ const readDirMethod = node.callee.property.name;
475
+ if ((readDirMethod === 'readdirSync' || readDirMethod === 'readdir') && node.arguments.length > 0) {
476
+ const pathArg = node.arguments[0];
477
+ if (pathArg?.type === 'Identifier' && workflowPathVars.has(pathArg.name)) {
478
+ threats.push({
479
+ type: 'workflow_write',
480
+ severity: 'CRITICAL',
481
+ message: `${readDirMethod}() enumerates .github/workflows — workflow modification/injection technique.`,
482
+ file: path.relative(basePath, filePath)
483
+ });
484
+ }
485
+ }
486
+ }
487
+
444
488
  // Detect fs.chmodSync with executable permissions — binary dropper pattern
445
489
  if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
446
490
  const chmodMethod = node.callee.property.name;
@@ -505,6 +549,24 @@ function analyzeFile(content, filePath, basePath) {
505
549
  }
506
550
  }
507
551
 
552
+ // Detect Object.defineProperty(process.env, ...) — env interception via property descriptors
553
+ if (node.callee.type === 'MemberExpression' &&
554
+ node.callee.object?.type === 'Identifier' && node.callee.object.name === 'Object' &&
555
+ node.callee.property?.type === 'Identifier' && node.callee.property.name === 'defineProperty' &&
556
+ node.arguments.length >= 2) {
557
+ const target = node.arguments[0];
558
+ if (target.type === 'MemberExpression' &&
559
+ target.object?.name === 'process' &&
560
+ target.property?.name === 'env') {
561
+ threats.push({
562
+ type: 'env_proxy_intercept',
563
+ severity: 'CRITICAL',
564
+ message: 'Object.defineProperty(process.env) detected — intercepts environment variable access for credential theft.',
565
+ file: path.relative(basePath, filePath)
566
+ });
567
+ }
568
+ }
569
+
508
570
  if (callName === 'eval') {
509
571
  const isConstant = hasOnlyStringLiteralArgs(node);
510
572
  threats.push({
@@ -665,6 +727,25 @@ function analyzeFile(content, filePath, basePath) {
665
727
  file: path.relative(basePath, filePath)
666
728
  });
667
729
  }
730
+
731
+ // <module>.<Class>.prototype.<method> = ... (Node.js core module prototype hooking)
732
+ // e.g. http.IncomingMessage.prototype.emit = function(...)
733
+ if (left.object?.type === 'MemberExpression' &&
734
+ left.object.property?.type === 'Identifier' && left.object.property.name === 'prototype' &&
735
+ left.object.object?.type === 'MemberExpression' &&
736
+ left.object.object.object?.type === 'Identifier' &&
737
+ left.object.object.property?.type === 'Identifier') {
738
+ const moduleName = left.object.object.object.name;
739
+ const className = left.object.object.property.name;
740
+ if (NODE_HOOKABLE_MODULES.includes(moduleName) && NODE_HOOKABLE_CLASSES.includes(className)) {
741
+ threats.push({
742
+ type: 'prototype_hook',
743
+ severity: 'CRITICAL',
744
+ message: `${moduleName}.${className}.prototype.${left.property?.name || '?'} overridden — Node.js core module prototype hooking for traffic interception.`,
745
+ file: path.relative(basePath, filePath)
746
+ });
747
+ }
748
+ }
668
749
  }
669
750
  },
670
751
 
@@ -675,6 +756,15 @@ function analyzeFile(content, filePath, basePath) {
675
756
  ) {
676
757
  // Dynamic access: process.env[variable] — always flag as MEDIUM
677
758
  if (node.computed) {
759
+ // Escalate if env key was built via String.fromCharCode (obfuscation)
760
+ if (hasFromCharCode) {
761
+ threats.push({
762
+ type: 'env_charcode_reconstruction',
763
+ severity: 'HIGH',
764
+ message: 'process.env accessed with dynamically reconstructed key (String.fromCharCode obfuscation).',
765
+ file: path.relative(basePath, filePath)
766
+ });
767
+ }
678
768
  threats.push({
679
769
  type: 'env_access',
680
770
  severity: 'MEDIUM',
@@ -69,6 +69,17 @@ function analyzeFile(content, filePath, basePath) {
69
69
  if (containsSensitiveLiteral(node.init)) {
70
70
  sensitivePathVars.add(node.id.name);
71
71
  }
72
+ // Propagate sensitive vars through path.join/resolve
73
+ if (node.init?.type === 'CallExpression' && node.init.callee?.type === 'MemberExpression') {
74
+ const obj = node.init.callee.object;
75
+ const prop = node.init.callee.property;
76
+ if (obj?.type === 'Identifier' && obj.name === 'path' &&
77
+ prop?.type === 'Identifier' && (prop.name === 'join' || prop.name === 'resolve')) {
78
+ if (node.init.arguments.some(a => a.type === 'Identifier' && sensitivePathVars.has(a.name))) {
79
+ sensitivePathVars.add(node.id.name);
80
+ }
81
+ }
82
+ }
72
83
  }
73
84
  },
74
85
 
@@ -158,6 +169,21 @@ function analyzeFile(content, filePath, basePath) {
158
169
  }
159
170
  }
160
171
 
172
+ // Detect writeFileSync/writeFile on sensitive paths → cache poisoning / credential tampering
173
+ if (node.callee.type === 'MemberExpression') {
174
+ const prop = node.callee.property;
175
+ if (prop?.type === 'Identifier' && (prop.name === 'writeFileSync' || prop.name === 'writeFile')) {
176
+ const arg = node.arguments[0];
177
+ if (arg && isCredentialPath(arg, sensitivePathVars)) {
178
+ sinks.push({
179
+ type: 'file_tamper',
180
+ name: prop.name,
181
+ line: node.loc?.start?.line
182
+ });
183
+ }
184
+ }
185
+ }
186
+
161
187
  // Track eval calls for staged payload detection
162
188
  if (callName === 'eval') {
163
189
  sinks.push({
@@ -173,6 +199,15 @@ function analyzeFile(content, filePath, basePath) {
173
199
  node.object?.object?.name === 'process' &&
174
200
  node.object?.property?.name === 'env'
175
201
  ) {
202
+ // Dynamic bracket access: process.env[variable]
203
+ if (node.computed) {
204
+ sources.push({
205
+ type: 'env_read',
206
+ name: 'process.env[dynamic]',
207
+ line: node.loc?.start?.line
208
+ });
209
+ return;
210
+ }
176
211
  const envVar = node.property?.name || '';
177
212
  if (isSensitiveEnv(envVar)) {
178
213
  sources.push({
@@ -197,11 +232,15 @@ function analyzeFile(content, filePath, basePath) {
197
232
  });
198
233
  }
199
234
 
200
- if (sources.length > 0 && sinks.length > 0) {
235
+ // Separate exfiltration sinks from file tampering sinks
236
+ const exfilSinks = sinks.filter(s => s.type !== 'file_tamper');
237
+ const fileTamperSinks = sinks.filter(s => s.type === 'file_tamper');
238
+
239
+ if (sources.length > 0 && exfilSinks.length > 0) {
201
240
  // Determine severity by scope proximity: if source and sink are < 50 lines apart -> CRITICAL, else HIGH
202
241
  let severity = 'HIGH';
203
242
  for (const src of sources) {
204
- for (const sink of sinks) {
243
+ for (const sink of exfilSinks) {
205
244
  if (src.line && sink.line && Math.abs(src.line - sink.line) < 50) {
206
245
  severity = 'CRITICAL';
207
246
  break;
@@ -213,7 +252,17 @@ function analyzeFile(content, filePath, basePath) {
213
252
  threats.push({
214
253
  type: 'suspicious_dataflow',
215
254
  severity: severity,
216
- message: `Suspicious flow: credentials read (${sources.map(s => s.name).join(', ')}) + network send (${sinks.map(s => s.name).join(', ')})`,
255
+ message: `Suspicious flow: credentials read (${sources.map(s => s.name).join(', ')}) + network send (${exfilSinks.map(s => s.name).join(', ')})`,
256
+ file: path.relative(basePath, filePath)
257
+ });
258
+ }
259
+
260
+ // Detect cache poisoning: credential source + write to sensitive path
261
+ if (sources.length > 0 && fileTamperSinks.length > 0) {
262
+ threats.push({
263
+ type: 'credential_tampering',
264
+ severity: 'CRITICAL',
265
+ message: `Cache poisoning: sensitive data access (${sources.map(s => s.name).join(', ')}) + write to sensitive path (${fileTamperSinks.map(s => s.name).join(', ')})`,
217
266
  file: path.relative(basePath, filePath)
218
267
  });
219
268
  }
@@ -226,7 +275,8 @@ const SENSITIVE_PATH_PATTERNS = [
226
275
  '/etc/passwd', '/etc/shadow', '/etc/hosts',
227
276
  '.ethereum', '.electrum', '.config/solana', '.exodus',
228
277
  '.atomic', '.metamask', '.ledger-live', '.trezor',
229
- '.bitcoin', '.monero', '.gnupg'
278
+ '.bitcoin', '.monero', '.gnupg',
279
+ '_cacache', '.cache/yarn', '.cache/pip'
230
280
  ];
231
281
 
232
282
  function isSensitivePath(val) {
@@ -80,6 +80,19 @@ async function scanPackageJson(targetPath) {
80
80
  });
81
81
  }
82
82
  }
83
+
84
+ // Escalate: lifecycle script (preinstall/install/postinstall) + shell pipe → CRITICAL
85
+ if (['preinstall', 'install', 'postinstall'].includes(scriptName)) {
86
+ if (/curl\s.*\|\s*(sh|bash)\b/.test(scriptContent) ||
87
+ /wget\s.*\|\s*(sh|bash)\b/.test(scriptContent)) {
88
+ threats.push({
89
+ type: 'lifecycle_shell_pipe',
90
+ severity: 'CRITICAL',
91
+ message: `Critical: "${scriptName}" pipes remote code to shell — supply chain RCE.`,
92
+ file: 'package.json'
93
+ });
94
+ }
95
+ }
83
96
  }
84
97
  }
85
98
 
package/iocs.json.gz DELETED
Binary file