muaddib-scanner 2.7.7 → 2.7.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 +4 -4
- package/package.json +1 -1
- package/src/index.js +6 -3
- package/src/scoring.js +28 -1
- package/src/shared/download.js +5 -1
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** (134 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
|
+
### 134 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
|
+
**2166 tests** across 49 files. **134 rules** (129 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
|
+
- **2166 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)
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -555,18 +555,21 @@ async function run(targetPath, options = {}) {
|
|
|
555
555
|
}
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
-
// Read package name for
|
|
558
|
+
// Read package name and dependencies for FP reduction heuristics
|
|
559
559
|
let packageName = null;
|
|
560
|
+
let packageDeps = null;
|
|
560
561
|
try {
|
|
561
562
|
const pkgPath = path.join(targetPath, 'package.json');
|
|
562
563
|
if (fs.existsSync(pkgPath)) {
|
|
563
|
-
|
|
564
|
+
const pkgData = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
565
|
+
packageName = pkgData.name || null;
|
|
566
|
+
packageDeps = pkgData.dependencies || null;
|
|
564
567
|
}
|
|
565
568
|
} catch { /* graceful fallback */ }
|
|
566
569
|
|
|
567
570
|
// FP reduction: legitimate frameworks produce high volumes of certain threat types.
|
|
568
571
|
// A malware package typically has 1-3 occurrences, not dozens.
|
|
569
|
-
applyFPReductions(deduped, reachableFiles, packageName);
|
|
572
|
+
applyFPReductions(deduped, reachableFiles, packageName, packageDeps);
|
|
570
573
|
|
|
571
574
|
// Intent coherence analysis: detect source→sink pairs within files
|
|
572
575
|
// Pass targetPath for destination-aware SDK pattern detection
|
package/src/scoring.js
CHANGED
|
@@ -197,7 +197,12 @@ const FRAMEWORK_PROTO_RE = new RegExp(
|
|
|
197
197
|
'^(' + FRAMEWORK_PROTOTYPES.join('|') + ')\\.prototype\\.'
|
|
198
198
|
);
|
|
199
199
|
|
|
200
|
-
function applyFPReductions(threats, reachableFiles, packageName) {
|
|
200
|
+
function applyFPReductions(threats, reachableFiles, packageName, packageDeps) {
|
|
201
|
+
// Initialize reductions audit trail on each threat
|
|
202
|
+
for (const t of threats) {
|
|
203
|
+
t.reductions = [];
|
|
204
|
+
}
|
|
205
|
+
|
|
201
206
|
// Count occurrences of each threat type (package-level, across all files)
|
|
202
207
|
const typeCounts = {};
|
|
203
208
|
for (const t of threats) {
|
|
@@ -224,6 +229,7 @@ function applyFPReductions(threats, reachableFiles, packageName) {
|
|
|
224
229
|
if ((t.type === 'dynamic_require' || t.type === 'dynamic_import') && t.severity === 'HIGH') {
|
|
225
230
|
const f = t.file || '(unknown)';
|
|
226
231
|
if (perFilePluginCount[f] > 4) {
|
|
232
|
+
t.reductions.push({ rule: 'plugin_loader_per_file', from: 'HIGH', to: 'LOW' });
|
|
227
233
|
t.severity = 'LOW';
|
|
228
234
|
}
|
|
229
235
|
}
|
|
@@ -245,6 +251,7 @@ function applyFPReductions(threats, reachableFiles, packageName) {
|
|
|
245
251
|
if (typeRatio < 0.4 ||
|
|
246
252
|
(t.type === 'suspicious_dataflow' && typeCounts[t.type] > rule.maxCount) ||
|
|
247
253
|
(t.type === 'vm_code_execution' && typeCounts[t.type] > rule.maxCount)) {
|
|
254
|
+
t.reductions.push({ rule: 'count_threshold', from: t.severity, to: rule.to });
|
|
248
255
|
t.severity = rule.to;
|
|
249
256
|
}
|
|
250
257
|
}
|
|
@@ -253,6 +260,7 @@ function applyFPReductions(threats, reachableFiles, packageName) {
|
|
|
253
260
|
// Malware poisons cache repeatedly; a single access is framework behavior
|
|
254
261
|
if (t.type === 'require_cache_poison' && t.severity === 'CRITICAL' &&
|
|
255
262
|
typeCounts.require_cache_poison === 1) {
|
|
263
|
+
t.reductions.push({ rule: 'cache_poison_single', from: 'CRITICAL', to: 'HIGH' });
|
|
256
264
|
t.severity = 'HIGH';
|
|
257
265
|
}
|
|
258
266
|
|
|
@@ -261,6 +269,7 @@ function applyFPReductions(threats, reachableFiles, packageName) {
|
|
|
261
269
|
// Browser/native APIs (globalThis.fetch, XMLHttpRequest) stay HIGH
|
|
262
270
|
if (t.type === 'prototype_hook' && t.severity === 'HIGH' &&
|
|
263
271
|
FRAMEWORK_PROTO_RE.test(t.message)) {
|
|
272
|
+
t.reductions.push({ rule: 'framework_prototype', from: 'HIGH', to: 'MEDIUM' });
|
|
264
273
|
t.severity = 'MEDIUM';
|
|
265
274
|
}
|
|
266
275
|
|
|
@@ -271,6 +280,7 @@ function applyFPReductions(threats, reachableFiles, packageName) {
|
|
|
271
280
|
typeCounts.prototype_hook > 20) {
|
|
272
281
|
const HTTP_PROTO_RE = /\b(Request|Response|IncomingMessage|ClientRequest|ServerResponse|fetch)\b/i;
|
|
273
282
|
if (HTTP_PROTO_RE.test(t.message)) {
|
|
283
|
+
t.reductions.push({ rule: 'http_client_whitelist', from: t.severity, to: 'MEDIUM' });
|
|
274
284
|
t.severity = 'MEDIUM';
|
|
275
285
|
}
|
|
276
286
|
}
|
|
@@ -283,14 +293,18 @@ function applyFPReductions(threats, reachableFiles, packageName) {
|
|
|
283
293
|
if (t.file && !DIST_EXEMPT_TYPES.has(t.type) && DIST_FILE_RE.test(t.file)) {
|
|
284
294
|
if (DIST_BUNDLER_ARTIFACT_TYPES.has(t.type)) {
|
|
285
295
|
// Two-notch downgrade for bundler artifacts
|
|
296
|
+
const fromSev = t.severity;
|
|
286
297
|
if (t.severity === 'CRITICAL') t.severity = 'MEDIUM';
|
|
287
298
|
else if (t.severity === 'HIGH') t.severity = 'LOW';
|
|
288
299
|
else if (t.severity === 'MEDIUM') t.severity = 'LOW';
|
|
300
|
+
if (t.severity !== fromSev) t.reductions.push({ rule: 'dist_file', from: fromSev, to: t.severity });
|
|
289
301
|
} else {
|
|
290
302
|
// One-notch downgrade for other non-exempt types
|
|
303
|
+
const fromSev = t.severity;
|
|
291
304
|
if (t.severity === 'CRITICAL') t.severity = 'HIGH';
|
|
292
305
|
else if (t.severity === 'HIGH') t.severity = 'MEDIUM';
|
|
293
306
|
else if (t.severity === 'MEDIUM') t.severity = 'LOW';
|
|
307
|
+
if (t.severity !== fromSev) t.reductions.push({ rule: 'dist_file', from: fromSev, to: t.severity });
|
|
294
308
|
}
|
|
295
309
|
}
|
|
296
310
|
|
|
@@ -300,10 +314,23 @@ function applyFPReductions(threats, reachableFiles, packageName) {
|
|
|
300
314
|
!isPackageLevelThreat(t)) {
|
|
301
315
|
const normalizedFile = t.file.replace(/\\/g, '/');
|
|
302
316
|
if (!reachableFiles.has(normalizedFile)) {
|
|
317
|
+
t.reductions.push({ rule: 'unreachable', from: t.severity, to: 'LOW' });
|
|
303
318
|
t.severity = 'LOW';
|
|
304
319
|
t.unreachable = true;
|
|
305
320
|
}
|
|
306
321
|
}
|
|
322
|
+
|
|
323
|
+
// C2: MCP server awareness — legitimate MCP servers write to MCP config files.
|
|
324
|
+
// Downgrade mcp_config_injection to MEDIUM when @modelcontextprotocol/sdk is in dependencies.
|
|
325
|
+
// Only dependencies (not devDependencies) — a real MCP server must ship the SDK.
|
|
326
|
+
// High-confidence compound types stay untouched (lifecycle_shell_pipe, fetch_decrypt_exec, etc.)
|
|
327
|
+
if (t.type === 'mcp_config_injection' && t.severity === 'CRITICAL' &&
|
|
328
|
+
packageDeps && typeof packageDeps === 'object' &&
|
|
329
|
+
packageDeps['@modelcontextprotocol/sdk']) {
|
|
330
|
+
t.reductions.push({ rule: 'mcp_sdk', from: 'CRITICAL', to: 'MEDIUM' });
|
|
331
|
+
t.severity = 'MEDIUM';
|
|
332
|
+
t.mcpSdkDowngrade = true;
|
|
333
|
+
}
|
|
307
334
|
}
|
|
308
335
|
}
|
|
309
336
|
|
package/src/shared/download.js
CHANGED
|
@@ -23,7 +23,11 @@ const PRIVATE_IP_PATTERNS = [
|
|
|
23
23
|
/^::1$/,
|
|
24
24
|
/^::ffff:127\./,
|
|
25
25
|
/^fc00:/,
|
|
26
|
-
/^fe80
|
|
26
|
+
/^fe80:/,
|
|
27
|
+
/^ff/i, // IPv6 multicast (RFC 4291 ff00::/8)
|
|
28
|
+
/^2001:0?db8:/i, // IPv6 documentation (RFC 3849 2001:db8::/32)
|
|
29
|
+
/^100:/, // IPv6 discard (RFC 6666 100::/64)
|
|
30
|
+
/^64:ff9b:/i // IPv6 NAT64 well-known (RFC 6052 64:ff9b::/96)
|
|
27
31
|
];
|
|
28
32
|
|
|
29
33
|
/**
|