muaddib-scanner 1.6.13 → 1.6.15

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/README.fr.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="MUADDIBLOGO.png" alt="MUAD'DIB Logo" width="200">
2
+ <img src="assets/logo2removebg.png" alt="MUAD'DIB Logo" width="200">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">MUAD'DIB</h1>
@@ -387,6 +387,7 @@ Détecte les patterns malveillants dans les fichiers YAML `.github/workflows/`,
387
387
  | Reverse shell | T1059.004 | Pattern |
388
388
  | Dead man's switch | T1485 | Pattern |
389
389
  | Code obfusqué | T1027 | Heuristiques |
390
+ | Analyse entropie Shannon | T1027 | Calcul d'entropie |
390
391
  | Typosquatting (npm + PyPI) | T1195.002 | Levenshtein |
391
392
  | Supply chain compromise | T1195.002 | IOC matching |
392
393
  | Package PyPI malveillant | T1195.002 | IOC matching |
@@ -503,6 +504,7 @@ MUAD'DIB Scanner
503
504
  +-- Pattern Matching (shell, scripts)
504
505
  +-- Typosquat Detection (npm + PyPI, Levenshtein)
505
506
  +-- Python Scanner (requirements.txt, setup.py, pyproject.toml)
507
+ +-- Analyse Entropie Shannon
506
508
  +-- GitHub Actions Scanner
507
509
  +-- Paranoid Mode (ultra-strict)
508
510
  +-- Docker Sandbox (behavioral analysis, network capture)
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="MUADDIBLOGO.png" alt="MUAD'DIB Logo" width="200">
2
+ <img src="assets/logo2removebg.png" alt="MUAD'DIB Logo" width="200">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">MUAD'DIB</h1>
@@ -387,6 +387,7 @@ Detects malicious patterns in `.github/workflows/` YAML files, including Shai-Hu
387
387
  | Reverse shell | T1059.004 | Pattern |
388
388
  | Dead man's switch | T1485 | Pattern |
389
389
  | Obfuscated code | T1027 | Heuristics |
390
+ | Shannon entropy analysis | T1027 | Entropy calculation |
390
391
  | Typosquatting (npm + PyPI) | T1195.002 | Levenshtein |
391
392
  | Supply chain compromise | T1195.002 | IOC matching |
392
393
  | PyPI malicious package | T1195.002 | IOC matching |
@@ -503,6 +504,7 @@ MUAD'DIB Scanner
503
504
  +-- Pattern Matching (shell, scripts)
504
505
  +-- Typosquat Detection (npm + PyPI, Levenshtein)
505
506
  +-- Python Scanner (requirements.txt, setup.py, pyproject.toml)
507
+ +-- Shannon Entropy Analysis
506
508
  +-- GitHub Actions Scanner
507
509
  +-- Paranoid Mode (ultra-strict)
508
510
  +-- Docker Sandbox (behavioral analysis, network capture)
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "1.6.13",
3
+ "version": "1.6.15",
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
@@ -16,6 +16,7 @@ const path = require('path');
16
16
  const { scanGitHubActions } = require('./scanner/github-actions.js');
17
17
  const { detectPythonProject, normalizePythonName } = require('./scanner/python.js');
18
18
  const { loadCachedIOCs } = require('./ioc/updater.js');
19
+ const { scanEntropy } = require('./scanner/entropy.js');
19
20
  const { setExtraExcludes, getExtraExcludes } = require('./utils.js');
20
21
 
21
22
  // ============================================
@@ -213,7 +214,8 @@ async function run(targetPath, options = {}) {
213
214
  typosquatThreats,
214
215
  ghActionsThreats,
215
216
  pythonThreats,
216
- pypiTyposquatThreats
217
+ pypiTyposquatThreats,
218
+ entropyThreats
217
219
  ] = await Promise.all([
218
220
  scanPackageJson(targetPath),
219
221
  scanShellScripts(targetPath),
@@ -225,7 +227,8 @@ async function run(targetPath, options = {}) {
225
227
  scanTyposquatting(targetPath),
226
228
  Promise.resolve(scanGitHubActions(targetPath)),
227
229
  Promise.resolve(matchPythonIOCs(pythonDeps, targetPath)),
228
- Promise.resolve(checkPyPITyposquatting(pythonDeps, targetPath))
230
+ Promise.resolve(checkPyPITyposquatting(pythonDeps, targetPath)),
231
+ Promise.resolve(scanEntropy(targetPath))
229
232
  ]);
230
233
 
231
234
  const threats = [
@@ -239,7 +242,8 @@ async function run(targetPath, options = {}) {
239
242
  ...typosquatThreats,
240
243
  ...ghActionsThreats,
241
244
  ...pythonThreats,
242
- ...pypiTyposquatThreats
245
+ ...pypiTyposquatThreats,
246
+ ...entropyThreats
243
247
  ];
244
248
 
245
249
  // Paranoid mode
@@ -147,6 +147,11 @@ const PLAYBOOKS = {
147
147
  'Package lance des processus inconnus lors de l\'installation. Verifier les commandes executees.',
148
148
  sandbox_timeout:
149
149
  'CRITIQUE: Le container sandbox a depasse le timeout. Possible boucle infinie ou consommation de ressources.',
150
+
151
+ high_entropy_string:
152
+ 'Chaine a haute entropie detectee. Verifier si c\'est du base64, hex, ou un payload chiffre. Analyser le contexte d\'utilisation.',
153
+ high_entropy_file:
154
+ 'Fichier avec entropie elevee. Peut etre du code obfusque ou des donnees encodees. Analyser manuellement dans un environnement isole.',
150
155
  };
151
156
 
152
157
  function getPlaybook(threatType) {
@@ -461,6 +461,26 @@ const RULES = {
461
461
  references: ['https://attack.mitre.org/techniques/T1499/'],
462
462
  mitre: 'T1499'
463
463
  },
464
+
465
+ // Entropy detections
466
+ high_entropy_string: {
467
+ id: 'MUADDIB-ENTROPY-001',
468
+ name: 'High Entropy String',
469
+ severity: 'MEDIUM',
470
+ confidence: 'medium',
471
+ description: 'Chaine a haute entropie detectee (base64, hex, payload chiffre). Souvent signe d\'obfuscation ou de donnees encodees.',
472
+ references: ['https://attack.mitre.org/techniques/T1027/'],
473
+ mitre: 'T1027'
474
+ },
475
+ high_entropy_file: {
476
+ id: 'MUADDIB-ENTROPY-002',
477
+ name: 'High Entropy File',
478
+ severity: 'MEDIUM',
479
+ confidence: 'low',
480
+ description: 'Fichier JS avec entropie globale elevee. Peut contenir du code obfusque, des payloads encodes ou des donnees compressees.',
481
+ references: ['https://attack.mitre.org/techniques/T1027/'],
482
+ mitre: 'T1027'
483
+ },
464
484
  };
465
485
 
466
486
  function getRule(type) {
@@ -0,0 +1,121 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { findFiles } = require('../utils.js');
4
+
5
+ const ENTROPY_EXCLUDED_DIRS = ['.git', '.muaddib-cache'];
6
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
7
+
8
+ // Minimum string length to analyze (short strings naturally have low entropy)
9
+ const MIN_STRING_LENGTH = 50;
10
+
11
+ // Thresholds
12
+ const FILE_ENTROPY_THRESHOLD = 5.5;
13
+ const STRING_ENTROPY_MEDIUM = 5.5;
14
+ const STRING_ENTROPY_HIGH = 6.5;
15
+
16
+ /**
17
+ * Calculate Shannon entropy of a string.
18
+ * Returns a value between 0 (completely uniform) and log2(alphabet_size).
19
+ * For byte data, max is 8 bits.
20
+ * @param {string} str - Input string
21
+ * @returns {number} Entropy in bits (0-8)
22
+ */
23
+ function calculateShannonEntropy(str) {
24
+ if (!str || str.length === 0) return 0;
25
+
26
+ const freq = {};
27
+ for (let i = 0; i < str.length; i++) {
28
+ const ch = str[i];
29
+ freq[ch] = (freq[ch] || 0) + 1;
30
+ }
31
+
32
+ const len = str.length;
33
+ let entropy = 0;
34
+ for (const ch in freq) {
35
+ const p = freq[ch] / len;
36
+ if (p > 0) {
37
+ entropy -= p * Math.log2(p);
38
+ }
39
+ }
40
+
41
+ return entropy;
42
+ }
43
+
44
+ /**
45
+ * Extract string literals from JS source code via regex.
46
+ * Matches single-quoted, double-quoted, and backtick strings.
47
+ * @param {string} content - JS source code
48
+ * @returns {string[]} Array of string contents (without quotes)
49
+ */
50
+ function extractStringLiterals(content) {
51
+ const strings = [];
52
+ // Match "...", '...', `...` — non-greedy, no newlines for single/double
53
+ const regex = /(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|`([^`\\]*(?:\\.[^`\\]*)*)`)/g;
54
+ let match;
55
+ while ((match = regex.exec(content)) !== null) {
56
+ const str = match[1] || match[2] || match[3];
57
+ if (str) strings.push(str);
58
+ }
59
+ return strings;
60
+ }
61
+
62
+ /**
63
+ * Scan JavaScript files for high-entropy content (base64, hex, encrypted payloads).
64
+ * Follows the same pattern as detectObfuscation().
65
+ * @param {string} targetPath - Directory to scan
66
+ * @returns {Array} threats
67
+ */
68
+ function scanEntropy(targetPath) {
69
+ const threats = [];
70
+ const files = findFiles(targetPath, { extensions: ['.js'], excludedDirs: ENTROPY_EXCLUDED_DIRS });
71
+
72
+ for (const file of files) {
73
+ // Size guard
74
+ try {
75
+ const stat = fs.statSync(file);
76
+ if (stat.size > MAX_FILE_SIZE) continue;
77
+ } catch {
78
+ continue;
79
+ }
80
+
81
+ let content;
82
+ try {
83
+ content = fs.readFileSync(file, 'utf8');
84
+ } catch {
85
+ continue;
86
+ }
87
+
88
+ const relativePath = path.relative(targetPath, file);
89
+
90
+ // File-level entropy check
91
+ const fileEntropy = calculateShannonEntropy(content);
92
+ if (fileEntropy > FILE_ENTROPY_THRESHOLD) {
93
+ threats.push({
94
+ type: 'high_entropy_file',
95
+ severity: 'MEDIUM',
96
+ message: `High entropy file (${fileEntropy.toFixed(2)} bits) — possibly obfuscated or encoded content`,
97
+ file: relativePath
98
+ });
99
+ }
100
+
101
+ // String-level entropy check
102
+ const strings = extractStringLiterals(content);
103
+ for (const str of strings) {
104
+ if (str.length < MIN_STRING_LENGTH) continue;
105
+ const strEntropy = calculateShannonEntropy(str);
106
+ if (strEntropy > STRING_ENTROPY_MEDIUM) {
107
+ const severity = strEntropy > STRING_ENTROPY_HIGH ? 'HIGH' : 'MEDIUM';
108
+ threats.push({
109
+ type: 'high_entropy_string',
110
+ severity,
111
+ message: `High entropy string (${strEntropy.toFixed(2)} bits, ${str.length} chars) — possible base64/hex/encrypted payload`,
112
+ file: relativePath
113
+ });
114
+ }
115
+ }
116
+ }
117
+
118
+ return threats;
119
+ }
120
+
121
+ module.exports = { scanEntropy, calculateShannonEntropy };