muaddib-scanner 2.2.11 → 2.2.13
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 +14 -17
- package/README.md +12 -15
- package/package.json +2 -1
- package/src/commands/evaluate.js +56 -6
- package/src/response/playbooks.js +10 -0
- package/src/rules/index.js +26 -0
- package/src/scanner/ast.js +24 -0
- package/src/scanner/dataflow.js +25 -2
- package/src/scanner/deobfuscate.js +48 -0
package/README.fr.md
CHANGED
|
@@ -686,7 +686,7 @@ MUAD'DIB 2.2.11 Scanner
|
|
|
686
686
|
| +-- Canary Tokens / Honey Tokens (sandbox)
|
|
687
687
|
|
|
|
688
688
|
+-- Validation & Observabilité (v2.1)
|
|
689
|
-
| +-- Ground Truth Dataset (
|
|
689
|
+
| +-- Ground Truth Dataset (51 attaques réelles, 91.8% TPR)
|
|
690
690
|
| +-- Logging Temps de Détection (first_seen, métriques lead time)
|
|
691
691
|
| +-- Suivi Taux FP (stats quotidiennes, taux faux positifs)
|
|
692
692
|
| +-- Décomposition Score (scoring explicable par règle)
|
|
@@ -723,11 +723,10 @@ Output (CLI, JSON, HTML, SARIF, Webhook, Threat Feed)
|
|
|
723
723
|
|
|
724
724
|
| Metrique | Resultat | Details |
|
|
725
725
|
|----------|----------|---------|
|
|
726
|
-
| **TPR** (Ground Truth) | **
|
|
726
|
+
| **TPR** (Ground Truth) | **91.8%** (45/49) | 51 attaques reelles (49 actives). 4 hors scope : browser-only (3) + risque FP (1) |
|
|
727
727
|
| **FPR** (Packages standard) | **6.2%** (18/290) | Packages avec <10 fichiers JS — librairies et outils typiques |
|
|
728
|
-
| **FPR** (Benign, global) |
|
|
729
|
-
| **ADR** (Adversarial) | **100%** (
|
|
730
|
-
| **Holdouts** (pre-tuning) | 40/40 pass | Tous les holdouts passent apres corrections |
|
|
728
|
+
| **FPR** (Benign, global) | **~13%** (69/527) | 529 packages npm, vrai code source via `npm pack`, seuil > 20 |
|
|
729
|
+
| **ADR** (Adversarial + Holdout) | **100%** (75/75) | 35 adversariaux + 40 holdouts evasifs sur 5 vagues red team |
|
|
731
730
|
|
|
732
731
|
**FPR par taille de package** — Le FPR correle lineairement avec la taille du package. Le scoring per-file max (v2.2.11) reduit significativement les FP sur les packages moyens/gros :
|
|
733
732
|
|
|
@@ -738,7 +737,7 @@ Output (CLI, JSON, HTML, SARIF, Webhook, Threat Feed)
|
|
|
738
737
|
| Gros (50-100 fichiers JS) | 40 | 10 | 25.0% |
|
|
739
738
|
| Tres gros (100+ fichiers JS) | 62 | 25 | 40.3% |
|
|
740
739
|
|
|
741
|
-
**Progression FPR** : 0% (invalide, dirs vides, v2.2.0-v2.2.6) → 38% (premiere vraie mesure, v2.2.7) → 19.4% (v2.2.8) → 17.5% (v2.2.9) →
|
|
740
|
+
**Progression FPR** : 0% (invalide, dirs vides, v2.2.0-v2.2.6) → 38% (premiere vraie mesure, v2.2.7) → 19.4% (v2.2.8) → 17.5% (v2.2.9) → **~13%** (v2.2.11, scoring per-file max)
|
|
742
741
|
|
|
743
742
|
**Progression holdout** (scores pre-tuning, regles gelees) :
|
|
744
743
|
|
|
@@ -750,12 +749,12 @@ Output (CLI, JSON, HTML, SARIF, Webhook, Threat Feed)
|
|
|
750
749
|
| v4 | **80%** (8/10) | Efficacite desobfuscation |
|
|
751
750
|
| v5 | 50% (5/10) | Dataflow inter-module (nouveau scanner) |
|
|
752
751
|
|
|
753
|
-
- **TPR** (True Positive Rate) : taux de detection sur
|
|
752
|
+
- **TPR** (True Positive Rate) : taux de detection sur 49 attaques supply-chain reelles (event-stream, ua-parser-js, coa, flatmap-stream, eslint-scope, solana-web3js, et 43 autres). 4 misses : browser-only (lottie-player, polyfill-io, trojanized-jquery) ou risque FP (websocket-rat) — voir [Threat Model](docs/threat-model.md).
|
|
754
753
|
- **FPR** (False Positive Rate) : packages avec score > 20 sur 529 packages npm reels (code source scanne, pas des dirs vides). Le 6.2% sur les packages standard (<10 fichiers JS, 290 packages) est la metrique la plus representative pour un usage typique — la plupart des packages npm sont petits.
|
|
755
|
-
- **ADR** (Adversarial Detection Rate) : taux de detection sur
|
|
754
|
+
- **ADR** (Adversarial Detection Rate) : taux de detection sur 75 samples malveillants evasifs — 35 adversariaux (4 vagues red team) + 40 holdouts (5 batches de 10, testant obfuscation, dataflow inter-module, etc.)
|
|
756
755
|
- **Holdout** (pre-tuning) : taux de detection sur 10 samples jamais vus avec regles gelees (mesure de generalisation)
|
|
757
756
|
|
|
758
|
-
Datasets : 529 npm + 132 PyPI packages benins,
|
|
757
|
+
Datasets : 529 npm + 132 PyPI packages benins, 75 samples adversariaux/holdout, 51 attaques ground-truth (65 packages malveillants documentes).
|
|
759
758
|
|
|
760
759
|
Voir [Evaluation Methodology](docs/EVALUATION_METHODOLOGY.md) pour le protocole experimental complet.
|
|
761
760
|
|
|
@@ -791,14 +790,12 @@ npm test
|
|
|
791
790
|
|
|
792
791
|
### Tests
|
|
793
792
|
|
|
794
|
-
- **
|
|
795
|
-
- **56 tests de fuzzing** - YAML
|
|
796
|
-
- **
|
|
797
|
-
- **
|
|
798
|
-
- **
|
|
799
|
-
- **
|
|
800
|
-
- **Validation faux positifs** - 6.2% FPR sur packages standard (18/290), 13.1% global (69/527) sur vrai code source npm via `npm pack`
|
|
801
|
-
- **Audit ESLint sécurité** - `eslint-plugin-security` avec 14 règles activées
|
|
793
|
+
- **807 tests unitaires/integration** sur 20 fichiers modulaires - 74% coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
|
|
794
|
+
- **56 tests de fuzzing** - YAML malforme, JSON invalide, fichiers binaires, ReDoS, unicode, inputs 10MB
|
|
795
|
+
- **75 samples adversariaux/holdout** - 35 adversariaux + 40 holdouts, 75/75 taux de detection (100% ADR)
|
|
796
|
+
- **Validation ground truth** - 51 attaques reelles (45/49 detectees = 91.8% TPR). 4 hors scope : browser-only (3) + risque FP (1)
|
|
797
|
+
- **Validation faux positifs** - 6.2% FPR sur packages standard (18/290), ~13% global (69/527) sur vrai code source npm via `npm pack`
|
|
798
|
+
- **Audit ESLint securite** - `eslint-plugin-security` avec 14 regles activees
|
|
802
799
|
|
|
803
800
|
---
|
|
804
801
|
|
package/README.md
CHANGED
|
@@ -689,7 +689,7 @@ MUAD'DIB 2.2.11 Scanner
|
|
|
689
689
|
| +-- Canary Tokens / Honey Tokens (sandbox)
|
|
690
690
|
|
|
|
691
691
|
+-- Validation & Observability (v2.1)
|
|
692
|
-
| +-- Ground Truth Dataset (
|
|
692
|
+
| +-- Ground Truth Dataset (51 real-world attacks, 91.8% TPR)
|
|
693
693
|
| +-- Detection Time Logging (first_seen tracking, lead time metrics)
|
|
694
694
|
| +-- FP Rate Tracking (daily stats, false positive rate)
|
|
695
695
|
| +-- Score Breakdown (explainable per-rule scoring)
|
|
@@ -726,11 +726,10 @@ Output (CLI, JSON, HTML, SARIF, Webhook, Threat Feed)
|
|
|
726
726
|
|
|
727
727
|
| Metric | Result | Details |
|
|
728
728
|
|--------|--------|---------|
|
|
729
|
-
| **TPR** (Ground Truth) | **
|
|
729
|
+
| **TPR** (Ground Truth) | **91.8%** (45/49) | 51 real-world attacks (49 active). 4 out-of-scope: browser-only (3) + FP-risky (1) |
|
|
730
730
|
| **FPR** (Standard packages) | **6.2%** (18/290) | Packages with <10 JS files — typical libraries and tools |
|
|
731
|
-
| **FPR** (Benign, global) |
|
|
732
|
-
| **ADR** (Adversarial) | **100%** (
|
|
733
|
-
| **Holdouts** (pre-tuning) | 40/40 pass | All holdout samples pass after corrections |
|
|
731
|
+
| **FPR** (Benign, global) | **~13%** (69/527) | 529 npm packages, real source code via `npm pack`, threshold > 20 |
|
|
732
|
+
| **ADR** (Adversarial + Holdout) | **100%** (75/75) | 35 adversarial + 40 holdout evasive samples across 5 red-team waves |
|
|
734
733
|
|
|
735
734
|
**FPR by package size** — FPR correlates linearly with package size. Per-file max scoring (v2.2.11) significantly reduces FP on medium/large packages:
|
|
736
735
|
|
|
@@ -741,7 +740,7 @@ Output (CLI, JSON, HTML, SARIF, Webhook, Threat Feed)
|
|
|
741
740
|
| Large (50-100 JS files) | 40 | 10 | 25.0% |
|
|
742
741
|
| Very large (100+ JS files) | 62 | 25 | 40.3% |
|
|
743
742
|
|
|
744
|
-
**FPR progression**: 0% (invalid, empty dirs, v2.2.0-v2.2.6) → 38% (first real measurement, v2.2.7) → 19.4% (v2.2.8) → 17.5% (v2.2.9) →
|
|
743
|
+
**FPR progression**: 0% (invalid, empty dirs, v2.2.0-v2.2.6) → 38% (first real measurement, v2.2.7) → 19.4% (v2.2.8) → 17.5% (v2.2.9) → **~13%** (v2.2.11, per-file max scoring)
|
|
745
744
|
|
|
746
745
|
**Holdout progression** (pre-tuning scores, rules frozen):
|
|
747
746
|
|
|
@@ -753,12 +752,12 @@ Output (CLI, JSON, HTML, SARIF, Webhook, Threat Feed)
|
|
|
753
752
|
| v4 | **80%** (8/10) | Deobfuscation effectiveness |
|
|
754
753
|
| v5 | 50% (5/10) | Inter-module dataflow (new scanner) |
|
|
755
754
|
|
|
756
|
-
- **TPR** (True Positive Rate): detection rate on
|
|
755
|
+
- **TPR** (True Positive Rate): detection rate on 49 real-world supply-chain attacks (event-stream, ua-parser-js, coa, flatmap-stream, eslint-scope, solana-web3js, and 43 more). 4 misses are browser-only (lottie-player, polyfill-io, trojanized-jquery) or risky to fix (websocket-rat) — see [Threat Model](docs/threat-model.md).
|
|
757
756
|
- **FPR** (False Positive Rate): packages scoring > 20 out of 529 real npm packages (source code scanned, not empty dirs). The 6.2% on standard packages (<10 JS files, 290 packages) is the most representative metric for typical use — most npm packages are small.
|
|
758
|
-
- **ADR** (Adversarial Detection Rate): detection rate on
|
|
757
|
+
- **ADR** (Adversarial Detection Rate): detection rate on 75 evasive malicious samples — 35 adversarial (4 red-team waves) + 40 holdout (5 batches of 10, testing obfuscation, inter-module dataflow, etc.)
|
|
759
758
|
- **Holdout** (pre-tuning): detection rate on 10 unseen samples with rules frozen (measures generalization)
|
|
760
759
|
|
|
761
|
-
Datasets: 529 npm + 132 PyPI benign packages,
|
|
760
|
+
Datasets: 529 npm + 132 PyPI benign packages, 75 adversarial/holdout samples, 51 ground-truth attacks (65 documented malware packages).
|
|
762
761
|
|
|
763
762
|
See [Evaluation Methodology](docs/EVALUATION_METHODOLOGY.md) for the full experimental protocol.
|
|
764
763
|
|
|
@@ -794,13 +793,11 @@ npm test
|
|
|
794
793
|
|
|
795
794
|
### Testing
|
|
796
795
|
|
|
797
|
-
- **
|
|
796
|
+
- **807 unit/integration tests** across 20 modular test files - 74% code coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
|
|
798
797
|
- **56 fuzz tests** - Malformed YAML, invalid JSON, binary files, ReDoS, unicode, 10MB inputs
|
|
799
|
-
- **
|
|
800
|
-
- **
|
|
801
|
-
- **
|
|
802
|
-
- **Ground truth validation** - 5/5 real-world attacks detected (event-stream, ua-parser-js, coa, node-ipc, colors)
|
|
803
|
-
- **False positive validation** - 6.2% FPR on standard packages (18/290), 13.1% global (69/527) on real npm source code via `npm pack`
|
|
798
|
+
- **75 adversarial/holdout samples** - 35 adversarial + 40 holdout, 75/75 detection rate (100% ADR)
|
|
799
|
+
- **Ground truth validation** - 51 real-world attacks (45/49 detected = 91.8% TPR). 4 out-of-scope: browser-only (3) + FP-risky (1)
|
|
800
|
+
- **False positive validation** - 6.2% FPR on standard packages (18/290), ~13% global (69/527) on real npm source code via `npm pack`
|
|
804
801
|
- **ESLint security audit** - `eslint-plugin-security` with 14 rules enabled
|
|
805
802
|
|
|
806
803
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muaddib-scanner",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.13",
|
|
4
4
|
"description": "Supply-chain threat detection & response for npm & PyPI/Python",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "node tests/run-tests.js",
|
|
11
|
+
"test:integration": "node tests/run-tests-integration.js",
|
|
11
12
|
"scan": "node bin/muaddib.js scan .",
|
|
12
13
|
"update": "node bin/muaddib.js update",
|
|
13
14
|
"lint": "eslint src bin --ext .js",
|
package/src/commands/evaluate.js
CHANGED
|
@@ -22,6 +22,12 @@ const BENIGN_DIR = path.join(ROOT, 'datasets', 'benign');
|
|
|
22
22
|
const ADVERSARIAL_DIR = path.join(ROOT, 'datasets', 'adversarial');
|
|
23
23
|
const METRICS_DIR = path.join(ROOT, 'metrics');
|
|
24
24
|
const CACHE_DIR = path.join(ROOT, '.muaddib-cache', 'benign-tarballs');
|
|
25
|
+
const HOLDOUT_DIRS = [
|
|
26
|
+
path.join(ROOT, 'datasets', 'holdout-v2'),
|
|
27
|
+
path.join(ROOT, 'datasets', 'holdout-v3'),
|
|
28
|
+
path.join(ROOT, 'datasets', 'holdout-v4'),
|
|
29
|
+
path.join(ROOT, 'datasets', 'holdout-v5'),
|
|
30
|
+
];
|
|
25
31
|
|
|
26
32
|
const GT_THRESHOLD = 3;
|
|
27
33
|
const BENIGN_THRESHOLD = 20;
|
|
@@ -68,6 +74,33 @@ const ADVERSARIAL_THRESHOLDS = {
|
|
|
68
74
|
'browser-api-hook': 20
|
|
69
75
|
};
|
|
70
76
|
|
|
77
|
+
const HOLDOUT_THRESHOLDS = {
|
|
78
|
+
// holdout-v2 (10 samples)
|
|
79
|
+
'conditional-os-payload': 25, 'env-var-reconstruction': 25,
|
|
80
|
+
'github-workflow-inject': 20, 'homedir-ssh-key-steal': 25,
|
|
81
|
+
'npm-cache-poison': 20, 'npm-lifecycle-preinstall-curl': 25,
|
|
82
|
+
'process-env-proxy-getter': 20, 'readable-stream-hijack': 20,
|
|
83
|
+
'setTimeout-chain': 25, 'wasm-loader': 20,
|
|
84
|
+
// holdout-v3 (10 samples)
|
|
85
|
+
'dns-txt-payload': 25, 'electron-rce': 30,
|
|
86
|
+
'env-file-parse-exfil': 20, 'git-credential-steal': 20,
|
|
87
|
+
'npm-hook-hijack': 25, 'postinstall-reverse-shell': 35,
|
|
88
|
+
'require-cache-poison': 20, 'steganography-payload': 15,
|
|
89
|
+
'symlink-escape': 25, 'timezone-trigger': 30,
|
|
90
|
+
// holdout-v4 (10 samples — deobfuscation)
|
|
91
|
+
'atob-eval': 20, 'base64-require': 35,
|
|
92
|
+
'charcode-fetch': 25, 'charcode-spread-homedir': 30,
|
|
93
|
+
'concat-env-steal': 20, 'double-decode-exfil': 40,
|
|
94
|
+
'hex-array-exec': 20, 'mixed-obfuscation-stealer': 30,
|
|
95
|
+
'nested-base64-concat': 25, 'template-literal-hide': 40,
|
|
96
|
+
// holdout-v5 (10 samples — inter-module dataflow)
|
|
97
|
+
'callback-exfil': 3, 'class-method-exfil': 20,
|
|
98
|
+
'conditional-split': 25, 'event-emitter-flow': 3,
|
|
99
|
+
'mixed-inline-split': 20, 'named-export-steal': 20,
|
|
100
|
+
'reexport-chain': 20, 'split-env-exfil': 20,
|
|
101
|
+
'split-npmrc-steal': 20, 'three-hop-chain': 20
|
|
102
|
+
};
|
|
103
|
+
|
|
71
104
|
/**
|
|
72
105
|
* Scan a directory silently and return the result
|
|
73
106
|
*/
|
|
@@ -300,23 +333,39 @@ async function evaluateAdversarial() {
|
|
|
300
333
|
const details = [];
|
|
301
334
|
let detected = 0;
|
|
302
335
|
|
|
303
|
-
|
|
304
|
-
for (const name of
|
|
336
|
+
// --- Adversarial samples (35) ---
|
|
337
|
+
for (const [name, threshold] of Object.entries(ADVERSARIAL_THRESHOLDS)) {
|
|
305
338
|
const sampleDir = path.join(ADVERSARIAL_DIR, name);
|
|
306
339
|
if (!fs.existsSync(sampleDir)) {
|
|
307
|
-
details.push({ name, score: 0, threshold
|
|
340
|
+
details.push({ name, score: 0, threshold, detected: false, error: 'directory not found', source: 'adversarial' });
|
|
308
341
|
continue;
|
|
309
342
|
}
|
|
343
|
+
const result = await silentScan(sampleDir);
|
|
344
|
+
const score = result.summary.riskScore;
|
|
345
|
+
const isDetected = score >= threshold;
|
|
346
|
+
if (isDetected) detected++;
|
|
347
|
+
details.push({ name, score, threshold, detected: isDetected, source: 'adversarial' });
|
|
348
|
+
}
|
|
310
349
|
|
|
350
|
+
// --- Holdout samples (40) ---
|
|
351
|
+
for (const [name, threshold] of Object.entries(HOLDOUT_THRESHOLDS)) {
|
|
352
|
+
let sampleDir = null;
|
|
353
|
+
for (const hDir of HOLDOUT_DIRS) {
|
|
354
|
+
const candidate = path.join(hDir, name);
|
|
355
|
+
if (fs.existsSync(candidate)) { sampleDir = candidate; break; }
|
|
356
|
+
}
|
|
357
|
+
if (!sampleDir) {
|
|
358
|
+
details.push({ name, score: 0, threshold, detected: false, error: 'directory not found', source: 'holdout' });
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
311
361
|
const result = await silentScan(sampleDir);
|
|
312
362
|
const score = result.summary.riskScore;
|
|
313
|
-
const threshold = ADVERSARIAL_THRESHOLDS[name];
|
|
314
363
|
const isDetected = score >= threshold;
|
|
315
364
|
if (isDetected) detected++;
|
|
316
|
-
details.push({ name, score, threshold, detected: isDetected });
|
|
365
|
+
details.push({ name, score, threshold, detected: isDetected, source: 'holdout' });
|
|
317
366
|
}
|
|
318
367
|
|
|
319
|
-
const total =
|
|
368
|
+
const total = Object.keys(ADVERSARIAL_THRESHOLDS).length + Object.keys(HOLDOUT_THRESHOLDS).length;
|
|
320
369
|
const adr = total > 0 ? detected / total : 0;
|
|
321
370
|
return { detected, total, adr, details };
|
|
322
371
|
}
|
|
@@ -425,6 +474,7 @@ module.exports = {
|
|
|
425
474
|
saveMetrics,
|
|
426
475
|
silentScan,
|
|
427
476
|
ADVERSARIAL_THRESHOLDS,
|
|
477
|
+
HOLDOUT_THRESHOLDS,
|
|
428
478
|
GT_THRESHOLD,
|
|
429
479
|
BENIGN_THRESHOLD
|
|
430
480
|
};
|
|
@@ -328,6 +328,16 @@ const PLAYBOOKS = {
|
|
|
328
328
|
'CRITIQUE: eval() ou Function() recoit un argument decode en base64 (atob/Buffer.from). ' +
|
|
329
329
|
'Technique de staged payload: le code malveillant est encode puis decode et execute dynamiquement. ' +
|
|
330
330
|
'Isoler la machine. Decoder le payload manuellement pour analyser le code execute. Supprimer le package.',
|
|
331
|
+
|
|
332
|
+
crypto_decipher:
|
|
333
|
+
'crypto.createDecipher/createDecipheriv detecte. Dechiffrement de payload embarque a runtime. ' +
|
|
334
|
+
'Pattern canonique de l\'attaque event-stream/flatmap-stream. Extraire et decoder le payload manuellement ' +
|
|
335
|
+
'pour analyser le code execute. Verifier la source des donnees chiffrees.',
|
|
336
|
+
|
|
337
|
+
module_compile:
|
|
338
|
+
'CRITIQUE: module._compile() detecte. Cette API Node.js interne execute du code arbitraire ' +
|
|
339
|
+
'a partir d\'une chaine dans le contexte d\'un module. Utilisee dans flatmap-stream pour executer ' +
|
|
340
|
+
'un payload dechiffre sans ecrire sur disque. Isoler immediatement. Analyser la source de la chaine compilee.',
|
|
331
341
|
};
|
|
332
342
|
|
|
333
343
|
function getPlaybook(threatType) {
|
package/src/rules/index.js
CHANGED
|
@@ -649,6 +649,32 @@ const RULES = {
|
|
|
649
649
|
mitre: 'T1565.001'
|
|
650
650
|
},
|
|
651
651
|
|
|
652
|
+
crypto_decipher: {
|
|
653
|
+
id: 'MUADDIB-AST-022',
|
|
654
|
+
name: 'Encrypted Payload Decryption',
|
|
655
|
+
severity: 'HIGH',
|
|
656
|
+
confidence: 'high',
|
|
657
|
+
description: 'crypto.createDecipher/createDecipheriv detecte. Dechiffrement runtime de payload embarque. Pattern canonique de flatmap-stream/event-stream.',
|
|
658
|
+
references: [
|
|
659
|
+
'https://snyk.io/blog/malicious-code-found-in-npm-package-event-stream/',
|
|
660
|
+
'https://attack.mitre.org/techniques/T1140/'
|
|
661
|
+
],
|
|
662
|
+
mitre: 'T1140'
|
|
663
|
+
},
|
|
664
|
+
|
|
665
|
+
module_compile: {
|
|
666
|
+
id: 'MUADDIB-AST-023',
|
|
667
|
+
name: 'Module Compile Execution',
|
|
668
|
+
severity: 'CRITICAL',
|
|
669
|
+
confidence: 'high',
|
|
670
|
+
description: 'module._compile() detecte. Execution de code arbitraire a partir d\'une chaine dans le contexte module. Technique cle de flatmap-stream.',
|
|
671
|
+
references: [
|
|
672
|
+
'https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident',
|
|
673
|
+
'https://attack.mitre.org/techniques/T1059/007/'
|
|
674
|
+
],
|
|
675
|
+
mitre: 'T1059'
|
|
676
|
+
},
|
|
677
|
+
|
|
652
678
|
ai_agent_abuse: {
|
|
653
679
|
id: 'MUADDIB-AST-013',
|
|
654
680
|
name: 'AI Agent Weaponization',
|
package/src/scanner/ast.js
CHANGED
|
@@ -719,6 +719,30 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
719
719
|
});
|
|
720
720
|
}
|
|
721
721
|
}
|
|
722
|
+
|
|
723
|
+
// Detect crypto.createDecipher/createDecipheriv — encrypted payload pattern (flatmap-stream)
|
|
724
|
+
// Also detect module._compile — in-memory code execution
|
|
725
|
+
if (node.callee.type === 'MemberExpression') {
|
|
726
|
+
const prop = node.callee.property;
|
|
727
|
+
const propName = prop.type === 'Identifier' ? prop.name :
|
|
728
|
+
(prop.type === 'Literal' ? prop.value : null);
|
|
729
|
+
if (propName === 'createDecipher' || propName === 'createDecipheriv') {
|
|
730
|
+
threats.push({
|
|
731
|
+
type: 'crypto_decipher',
|
|
732
|
+
severity: 'HIGH',
|
|
733
|
+
message: `${propName}() detected — runtime decryption of embedded payload (event-stream/flatmap-stream pattern).`,
|
|
734
|
+
file: path.relative(basePath, filePath)
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
if (propName === '_compile') {
|
|
738
|
+
threats.push({
|
|
739
|
+
type: 'module_compile',
|
|
740
|
+
severity: 'CRITICAL',
|
|
741
|
+
message: 'module._compile() detected — executes arbitrary code from string in module context (flatmap-stream pattern).',
|
|
742
|
+
file: path.relative(basePath, filePath)
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
722
746
|
},
|
|
723
747
|
|
|
724
748
|
ImportExpression(node) {
|
package/src/scanner/dataflow.js
CHANGED
|
@@ -95,7 +95,10 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
95
95
|
const prop = node.init.callee.property;
|
|
96
96
|
if (obj?.type === 'Identifier' && obj.name === 'path' &&
|
|
97
97
|
prop?.type === 'Identifier' && (prop.name === 'join' || prop.name === 'resolve')) {
|
|
98
|
-
if (node.init.arguments.some(a =>
|
|
98
|
+
if (node.init.arguments.some(a =>
|
|
99
|
+
(a.type === 'Identifier' && sensitivePathVars.has(a.name)) ||
|
|
100
|
+
(a.type === 'MemberExpression' && a.object?.type === 'Identifier' && sensitivePathVars.has(a.object.name))
|
|
101
|
+
)) {
|
|
99
102
|
sensitivePathVars.add(node.id.name);
|
|
100
103
|
}
|
|
101
104
|
}
|
|
@@ -241,6 +244,17 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
241
244
|
});
|
|
242
245
|
}
|
|
243
246
|
}
|
|
247
|
+
|
|
248
|
+
// Detect property access to secret key material
|
|
249
|
+
const propName = node.property?.type === 'Identifier' ? node.property.name :
|
|
250
|
+
(node.property?.type === 'Literal' ? node.property.value : null);
|
|
251
|
+
if (propName && ['secretKey', '_secretKey', 'privateKey', '_privateKey', 'mnemonic', 'seedPhrase'].includes(propName)) {
|
|
252
|
+
sources.push({
|
|
253
|
+
type: 'credential_read',
|
|
254
|
+
name: propName,
|
|
255
|
+
line: node.loc?.start?.line
|
|
256
|
+
});
|
|
257
|
+
}
|
|
244
258
|
}
|
|
245
259
|
});
|
|
246
260
|
|
|
@@ -300,7 +314,8 @@ const SENSITIVE_PATH_PATTERNS = [
|
|
|
300
314
|
'.ethereum', '.electrum', '.config/solana', '.exodus',
|
|
301
315
|
'.atomic', '.metamask', '.ledger-live', '.trezor',
|
|
302
316
|
'.bitcoin', '.monero', '.gnupg',
|
|
303
|
-
'_cacache', '.cache/yarn', '.cache/pip'
|
|
317
|
+
'_cacache', '.cache/yarn', '.cache/pip',
|
|
318
|
+
'discord', 'leveldb'
|
|
304
319
|
];
|
|
305
320
|
|
|
306
321
|
function isSensitivePath(val) {
|
|
@@ -327,6 +342,9 @@ function containsSensitiveLiteral(node) {
|
|
|
327
342
|
if (node.type === 'CallExpression' && node.arguments) {
|
|
328
343
|
return node.arguments.some(a => containsSensitiveLiteral(a));
|
|
329
344
|
}
|
|
345
|
+
if (node.type === 'ObjectExpression' && node.properties) {
|
|
346
|
+
return node.properties.some(p => p.value && containsSensitiveLiteral(p.value));
|
|
347
|
+
}
|
|
330
348
|
return false;
|
|
331
349
|
}
|
|
332
350
|
|
|
@@ -346,6 +364,11 @@ function isCredentialPath(arg, sensitivePathVars) {
|
|
|
346
364
|
if (arg.type === 'Identifier' && sensitivePathVars && sensitivePathVars.has(arg.name)) {
|
|
347
365
|
return true;
|
|
348
366
|
}
|
|
367
|
+
// Handle property access on tracked objects: _0x.a where _0x is tracked as sensitive
|
|
368
|
+
if (arg.type === 'MemberExpression' && arg.object?.type === 'Identifier' &&
|
|
369
|
+
sensitivePathVars && sensitivePathVars.has(arg.object.name)) {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
349
372
|
// Handle path.join(dir, '.npmrc') or path.join(sshDir, 'id_rsa') where sshDir is tracked
|
|
350
373
|
if (arg.type === 'CallExpression' && arg.callee.type === 'MemberExpression') {
|
|
351
374
|
const obj = arg.callee.object;
|
|
@@ -97,6 +97,26 @@ function deobfuscate(sourceCode) {
|
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// Buffer.from('...', 'hex').toString() → decoded string
|
|
101
|
+
if (isBufferHexToString(node)) {
|
|
102
|
+
const hexStr = extractBufferHexArg(node);
|
|
103
|
+
if (hexStr === null) return;
|
|
104
|
+
try {
|
|
105
|
+
const decoded = Buffer.from(hexStr, 'hex').toString();
|
|
106
|
+
if (!isPrintable(decoded)) return;
|
|
107
|
+
const before = sourceCode.slice(node.start, node.end);
|
|
108
|
+
const after = quoteString(decoded);
|
|
109
|
+
replacements.push({
|
|
110
|
+
start: node.start,
|
|
111
|
+
end: node.end,
|
|
112
|
+
value: after,
|
|
113
|
+
type: 'hex',
|
|
114
|
+
before
|
|
115
|
+
});
|
|
116
|
+
} catch { /* decode failure — skip */ }
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
// atob('...') → decoded string
|
|
101
121
|
if (isAtobCall(node)) {
|
|
102
122
|
const b64str = node.arguments[0]?.value;
|
|
@@ -430,6 +450,34 @@ function extractBufferBase64Arg(node) {
|
|
|
430
450
|
return inner.arguments[0].value;
|
|
431
451
|
}
|
|
432
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Check if node is Buffer.from('...', 'hex').toString()
|
|
455
|
+
*/
|
|
456
|
+
function isBufferHexToString(node) {
|
|
457
|
+
if (node.type !== 'CallExpression') return false;
|
|
458
|
+
const callee = node.callee;
|
|
459
|
+
if (callee.type !== 'MemberExpression') return false;
|
|
460
|
+
if (callee.property?.type !== 'Identifier' || callee.property.name !== 'toString') return false;
|
|
461
|
+
const inner = callee.object;
|
|
462
|
+
if (inner?.type !== 'CallExpression') return false;
|
|
463
|
+
const innerCallee = inner.callee;
|
|
464
|
+
if (innerCallee?.type !== 'MemberExpression') return false;
|
|
465
|
+
if (innerCallee.object?.type !== 'Identifier' || innerCallee.object.name !== 'Buffer') return false;
|
|
466
|
+
if (innerCallee.property?.type !== 'Identifier' || innerCallee.property.name !== 'from') return false;
|
|
467
|
+
if (inner.arguments.length < 2) return false;
|
|
468
|
+
if (inner.arguments[1]?.type !== 'Literal' || inner.arguments[1].value !== 'hex') return false;
|
|
469
|
+
if (inner.arguments[0]?.type !== 'Literal' || typeof inner.arguments[0].value !== 'string') return false;
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Extract the hex string argument from Buffer.from(str, 'hex').toString()
|
|
475
|
+
*/
|
|
476
|
+
function extractBufferHexArg(node) {
|
|
477
|
+
const inner = node.callee.object;
|
|
478
|
+
return inner.arguments[0].value;
|
|
479
|
+
}
|
|
480
|
+
|
|
433
481
|
/**
|
|
434
482
|
* Check if node is atob('...')
|
|
435
483
|
*/
|