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 +3 -1
- package/README.md +3 -1
- package/assets/logo.png +0 -0
- package/assets/logo2removebg.png +0 -0
- package/logo2removebg.png +0 -0
- package/package.json +1 -1
- package/src/index.js +7 -3
- package/src/response/playbooks.js +5 -0
- package/src/rules/index.js +20 -0
- package/src/scanner/entropy.js +121 -0
package/README.fr.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="
|
|
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="
|
|
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)
|
package/assets/logo.png
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
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) {
|
package/src/rules/index.js
CHANGED
|
@@ -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 };
|