muaddib-scanner 2.6.7 → 2.6.9
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.md +5 -5
- package/package.json +3 -5
- package/src/response/playbooks.js +9 -0
- package/src/rules/index.js +29 -0
- package/src/scanner/deobfuscate.js +2 -0
- package/src/scanner/module-graph.js +6 -5
- package/src/scanner/shell.js +5 -1
- package/src/scoring.js +4 -1
- package/src/shared/download.js +9 -2
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
npm and PyPI supply-chain attacks are exploding. Shai-Hulud compromised 25K+ repos in 2025. Existing tools detect threats but don't help you respond.
|
|
32
32
|
|
|
33
|
-
MUAD'DIB combines **14 parallel scanners** (
|
|
33
|
+
MUAD'DIB combines **14 parallel scanners** (133 detection rules), a **deobfuscation engine**, **inter-module dataflow analysis**, **per-file max scoring**, Docker sandbox with **monkey-patching preload** for time-bomb detection, **behavioral anomaly detection**, and **ground truth validation** to detect threats AND guide your response — even before they appear in any IOC database.
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
@@ -195,7 +195,7 @@ muaddib replay # Ground truth validation (46/49 TPR)
|
|
|
195
195
|
| GitHub Actions | Shai-Hulud backdoor detection |
|
|
196
196
|
| Hash Scanner | Known malicious file hashes |
|
|
197
197
|
|
|
198
|
-
###
|
|
198
|
+
### 133 detection rules
|
|
199
199
|
|
|
200
200
|
All rules are mapped to MITRE ATT&CK techniques. See [SECURITY.md](SECURITY.md#detection-rules-v262) for the complete rules reference.
|
|
201
201
|
|
|
@@ -286,7 +286,7 @@ repos:
|
|
|
286
286
|
| **FPR** (Benign) | **12.1%** (64/529) | 529 npm packages, real source via `npm pack` |
|
|
287
287
|
| **ADR** (Adversarial + Holdout) | **92.2%** (71/77) | 53 adversarial + 40 holdout (77 available on disk), global threshold=20 |
|
|
288
288
|
|
|
289
|
-
**
|
|
289
|
+
**2042 tests** across 49 files. **133 rules** (128 RULES + 5 PARANOID).
|
|
290
290
|
|
|
291
291
|
> **Methodology caveats:**
|
|
292
292
|
> - TPR measured on 49 Node.js attack samples (3 browser-only excluded from 51 total)
|
|
@@ -327,7 +327,7 @@ npm test
|
|
|
327
327
|
|
|
328
328
|
### Testing
|
|
329
329
|
|
|
330
|
-
- **
|
|
330
|
+
- **2042 tests** across 49 modular test files
|
|
331
331
|
- **56 fuzz tests** - Malformed inputs, ReDoS, unicode, binary
|
|
332
332
|
- **Datadog 17K benchmark** - 17,922 real malware samples
|
|
333
333
|
- **Ground truth validation** - 51 real-world attacks (93.9% TPR)
|
|
@@ -347,7 +347,7 @@ npm test
|
|
|
347
347
|
- [Evaluation Methodology](docs/EVALUATION_METHODOLOGY.md) - Experimental protocol, holdout scores
|
|
348
348
|
- [Threat Model](docs/threat-model.md) - What MUAD'DIB detects and doesn't detect
|
|
349
349
|
- [Adversarial Evaluation](ADVERSARIAL.md) - Red team samples and ADR results
|
|
350
|
-
- [Security Policy](SECURITY.md) - Detection rules reference (
|
|
350
|
+
- [Security Policy](SECURITY.md) - Detection rules reference (133 rules)
|
|
351
351
|
- [Security Audit](docs/SECURITY_AUDIT.md) - Bypass validation report
|
|
352
352
|
- [FP Analysis](docs/EVALUATION.md) - Historical false positive analysis
|
|
353
353
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muaddib-scanner",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.9",
|
|
4
4
|
"description": "Supply-chain threat detection & response for npm & PyPI/Python",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -48,10 +48,8 @@
|
|
|
48
48
|
"acorn": "8.16.0",
|
|
49
49
|
"acorn-walk": "8.3.5",
|
|
50
50
|
"adm-zip": "0.5.16",
|
|
51
|
-
"js-yaml": "4.1.1"
|
|
52
|
-
|
|
53
|
-
"overrides": {
|
|
54
|
-
"loadash": "0.0.0-security"
|
|
51
|
+
"js-yaml": "4.1.1",
|
|
52
|
+
"loadash": "^1.0.0"
|
|
55
53
|
},
|
|
56
54
|
"devDependencies": {
|
|
57
55
|
"@eslint/js": "10.0.1",
|
|
@@ -174,6 +174,15 @@ const PLAYBOOKS = {
|
|
|
174
174
|
wget_base64_decode:
|
|
175
175
|
'Telechargement + decodage base64 detecte. Verifier l\'URL de telechargement et decoder le contenu. Pattern de staging malveillant en deux etapes.',
|
|
176
176
|
|
|
177
|
+
curl_ifs_evasion:
|
|
178
|
+
'CRITIQUE: Evasion IFS detectee — curl$IFS ou curl${IFS} pipe vers shell. Technique d\'evasion pour contourner la detection de "curl|sh". Ne pas installer.',
|
|
179
|
+
|
|
180
|
+
eval_curl_subshell:
|
|
181
|
+
'CRITIQUE: eval $(curl ...) detecte. Telecharge et execute du code distant via command substitution. Ne pas installer.',
|
|
182
|
+
|
|
183
|
+
sh_c_curl_exec:
|
|
184
|
+
'sh -c wrapping autour de curl detecte. Technique d\'evasion pour masquer l\'execution de commandes distantes. Analyser le contenu telecharge.',
|
|
185
|
+
|
|
177
186
|
shai_hulud_backdoor:
|
|
178
187
|
'CRITIQUE: Backdoor Shai-Hulud dans GitHub Actions. Supprimer le workflow et auditer les runs precedents.',
|
|
179
188
|
|
package/src/rules/index.js
CHANGED
|
@@ -1369,6 +1369,35 @@ const RULES = {
|
|
|
1369
1369
|
],
|
|
1370
1370
|
mitre: 'T1557'
|
|
1371
1371
|
},
|
|
1372
|
+
// Shell IFS evasion rules (v2.6.9)
|
|
1373
|
+
curl_ifs_evasion: {
|
|
1374
|
+
id: 'MUADDIB-SHELL-016',
|
|
1375
|
+
name: 'Curl IFS Variable Evasion',
|
|
1376
|
+
severity: 'CRITICAL',
|
|
1377
|
+
confidence: 'high',
|
|
1378
|
+
description: 'Evasion IFS: curl$IFS ou curl${IFS} pipe vers shell. Technique d\'evasion pour contourner la detection de curl|sh en utilisant $IFS comme separateur.',
|
|
1379
|
+
references: ['https://attack.mitre.org/techniques/T1059/004/'],
|
|
1380
|
+
mitre: 'T1059.004'
|
|
1381
|
+
},
|
|
1382
|
+
eval_curl_subshell: {
|
|
1383
|
+
id: 'MUADDIB-SHELL-017',
|
|
1384
|
+
name: 'Eval Curl Command Substitution',
|
|
1385
|
+
severity: 'CRITICAL',
|
|
1386
|
+
confidence: 'high',
|
|
1387
|
+
description: 'eval $(curl ...) detecte. Telecharge et execute du code distant via command substitution.',
|
|
1388
|
+
references: ['https://attack.mitre.org/techniques/T1059/004/'],
|
|
1389
|
+
mitre: 'T1059.004'
|
|
1390
|
+
},
|
|
1391
|
+
sh_c_curl_exec: {
|
|
1392
|
+
id: 'MUADDIB-SHELL-018',
|
|
1393
|
+
name: 'Shell -c Curl Execution',
|
|
1394
|
+
severity: 'HIGH',
|
|
1395
|
+
confidence: 'high',
|
|
1396
|
+
description: 'sh -c wrapping autour de curl. Technique d\'evasion pour masquer l\'execution de commandes distantes.',
|
|
1397
|
+
references: ['https://attack.mitre.org/techniques/T1059/004/'],
|
|
1398
|
+
mitre: 'T1059.004'
|
|
1399
|
+
},
|
|
1400
|
+
|
|
1372
1401
|
// Intent Graph rules (v2.6.0)
|
|
1373
1402
|
intent_credential_exfil: {
|
|
1374
1403
|
id: 'MUADDIB-INTENT-001',
|
|
@@ -71,6 +71,8 @@ function deobfuscate(sourceCode) {
|
|
|
71
71
|
if (isStringFromCharCode(node)) {
|
|
72
72
|
const nums = extractNumericArgs(node);
|
|
73
73
|
if (nums === null) return;
|
|
74
|
+
// Validate charcode range [0, 0x10FFFF] to prevent invalid codepoints
|
|
75
|
+
if (nums.some(n => n < 0 || n > 0x10FFFF || !Number.isFinite(n))) return;
|
|
74
76
|
try {
|
|
75
77
|
const decoded = String.fromCharCode(...nums);
|
|
76
78
|
const before = sourceCode.slice(node.start, node.end);
|
|
@@ -167,15 +167,16 @@ function analyzeExports(filePath) {
|
|
|
167
167
|
|
|
168
168
|
// Track which local variables hold sensitive module references
|
|
169
169
|
// e.g. const fs = require('fs') → moduleVars['fs'] = 'fs'
|
|
170
|
-
|
|
170
|
+
// Object.create(null) prevents prototype pollution via __proto__/constructor keys
|
|
171
|
+
const moduleVars = Object.create(null);
|
|
171
172
|
// Track which local variables hold tainted values
|
|
172
173
|
// e.g. const data = fs.readFileSync(...) → taintedVars['data'] = { source, detail }
|
|
173
|
-
const taintedVars =
|
|
174
|
+
const taintedVars = Object.create(null);
|
|
174
175
|
|
|
175
176
|
// Track class declarations: class Foo { ... }
|
|
176
|
-
const classDefs =
|
|
177
|
+
const classDefs = Object.create(null);
|
|
177
178
|
// Track function declarations: function foo() { ... }
|
|
178
|
-
const funcDefs =
|
|
179
|
+
const funcDefs = Object.create(null);
|
|
179
180
|
walkAST(ast, (node) => {
|
|
180
181
|
if (node.type === 'ClassDeclaration' && node.id && node.id.name) {
|
|
181
182
|
classDefs[node.id.name] = node;
|
|
@@ -468,7 +469,7 @@ function describeSensitiveCall(mod, method, args) {
|
|
|
468
469
|
*/
|
|
469
470
|
function scanBodyForTaint(body, moduleVars, taintedVars) {
|
|
470
471
|
// Collect local tainted vars within this function scope too
|
|
471
|
-
const localTainted =
|
|
472
|
+
const localTainted = Object.assign(Object.create(null), taintedVars);
|
|
472
473
|
|
|
473
474
|
let found = null;
|
|
474
475
|
walkAST({ type: 'Program', body }, (node) => {
|
package/src/scanner/shell.js
CHANGED
|
@@ -20,7 +20,11 @@ const MALICIOUS_PATTERNS = [
|
|
|
20
20
|
{ pattern: /mkfifo.*\/dev\/tcp/m, name: 'fifo_reverse_shell', severity: 'CRITICAL' },
|
|
21
21
|
{ pattern: /mkfifo\s+\S+.*(?:\|\s*nc\s|nc\s+\S+.*>\s*\/tmp\/)/m, name: 'fifo_nc_reverse_shell', severity: 'CRITICAL' },
|
|
22
22
|
{ pattern: /base64\s+-d\b.*\|\s*(ba)?sh/m, name: 'base64_decode_exec', severity: 'CRITICAL' },
|
|
23
|
-
{ pattern: /wget\s+\S+.*&&.*base64\s+-d/m, name: 'wget_base64_decode', severity: 'HIGH' }
|
|
23
|
+
{ pattern: /wget\s+\S+.*&&.*base64\s+-d/m, name: 'wget_base64_decode', severity: 'HIGH' },
|
|
24
|
+
// IFS evasion patterns (v2.6.9)
|
|
25
|
+
{ pattern: /curl\$\{?IFS\}?.*\|.*sh/m, name: 'curl_ifs_evasion', severity: 'CRITICAL' },
|
|
26
|
+
{ pattern: /eval\s+.*\$\(curl/m, name: 'eval_curl_subshell', severity: 'CRITICAL' },
|
|
27
|
+
{ pattern: /sh\s+-c\s+['"].*curl/m, name: 'sh_c_curl_exec', severity: 'HIGH' }
|
|
24
28
|
];
|
|
25
29
|
|
|
26
30
|
const SHEBANG_RE = /^#!.*\b(?:ba)?sh\b/;
|
package/src/scoring.js
CHANGED
|
@@ -164,7 +164,10 @@ const DIST_BUNDLER_ARTIFACT_TYPES = new Set([
|
|
|
164
164
|
'js_obfuscation_pattern', 'vm_code_execution',
|
|
165
165
|
'module_compile', 'module_compile_dynamic',
|
|
166
166
|
// P7: env_access in dist/ is bundled SDK config reading, not credential theft
|
|
167
|
-
'env_access'
|
|
167
|
+
'env_access',
|
|
168
|
+
// P8: Proxy traps in dist/ are state management frameworks (MobX, Vue reactivity, Immer),
|
|
169
|
+
// not malicious data interception. Two-notch downgrade (CRITICAL→MEDIUM, HIGH→LOW).
|
|
170
|
+
'proxy_data_intercept'
|
|
168
171
|
]);
|
|
169
172
|
|
|
170
173
|
// Types exempt from reachability downgrade — IOC matches, lifecycle, and package-level types.
|
package/src/shared/download.js
CHANGED
|
@@ -116,8 +116,15 @@ async function safeDnsResolve(hostname) {
|
|
|
116
116
|
return hostname;
|
|
117
117
|
}
|
|
118
118
|
const dns = require('dns');
|
|
119
|
-
const
|
|
120
|
-
|
|
119
|
+
const [v4, v6] = await Promise.allSettled([
|
|
120
|
+
dns.promises.resolve4(hostname),
|
|
121
|
+
dns.promises.resolve6(hostname),
|
|
122
|
+
]);
|
|
123
|
+
const addresses = [
|
|
124
|
+
...(v4.status === 'fulfilled' ? v4.value : []),
|
|
125
|
+
...(v6.status === 'fulfilled' ? v6.value : []),
|
|
126
|
+
];
|
|
127
|
+
if (addresses.length === 0) {
|
|
121
128
|
throw new Error(`DNS resolution failed for ${hostname}`);
|
|
122
129
|
}
|
|
123
130
|
for (const addr of addresses) {
|