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 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 (5 attaques réelles, 100% détection)
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) | **100%** (4/4) | Attaques reelles : event-stream, ua-parser-js, coa, node-ipc |
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) | **13.1%** (69/527) | 529 packages npm, vrai code source via `npm pack`, seuil > 20 |
729
- | **ADR** (Adversarial) | **100%** (35/35) | 35 samples evasifs sur 4 vagues red team |
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) → **13.1%** (v2.2.11, scoring per-file max)
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 4 attaques supply-chain reelles (event-stream, ua-parser-js, coa, node-ipc)
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 35 samples malveillants evasifs sur 4 vagues red team
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, 35 samples adversariaux, 50 samples holdout (5 batches), 65 packages malveillants documentes.
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
- - **836 tests unitaires/intégration** sur 20 fichiers modulaires - 74% coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
795
- - **56 tests de fuzzing** - YAML malformé, JSON invalide, fichiers binaires, ReDoS, unicode, inputs 10MB
796
- - **35 samples adversariaux** - Packages malveillants évasifs, taux de détection 35/35 (100% ADR)
797
- - **50 samples holdout** - 5 batches de 10, scores pre-tuning : 30% 40% 60% 80% 50%
798
- - **8 tests multi-facteur typosquat** - Cas limites et comportement cache
799
- - **Validation ground truth** - 5/5 attaques réelles détectées (event-stream, ua-parser-js, coa, node-ipc, colors)
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 (5 real-world attacks, 100% detection)
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) | **100%** (4/4) | Real-world attacks: event-stream, ua-parser-js, coa, node-ipc |
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) | **13.1%** (69/527) | 529 npm packages, real source code via `npm pack`, threshold > 20 |
732
- | **ADR** (Adversarial) | **100%** (35/35) | 35 evasive samples across 4 red-team waves |
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) → **13.1%** (v2.2.11, per-file max scoring)
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 4 real-world supply-chain attacks (event-stream, ua-parser-js, coa, node-ipc)
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 35 evasive malicious samples across 4 red-team waves
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, 35 adversarial samples, 50 holdout samples (5 batches), 65 documented malware 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
- - **836 unit/integration tests** across 20 modular test files - 74% code coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
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
- - **35 adversarial samples** - Evasive malicious packages, 35/35 detection rate (100% ADR)
800
- - **50 holdout samples** - 5 batches of 10, pre-tuning scores: 30% 40% 60% 80% 50%
801
- - **8 multi-factor typosquat tests** - Edge cases and cache behavior
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.11",
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",
@@ -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
- const sampleNames = Object.keys(ADVERSARIAL_THRESHOLDS);
304
- for (const name of sampleNames) {
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: ADVERSARIAL_THRESHOLDS[name], detected: false, error: 'directory not found' });
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 = sampleNames.length;
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) {
@@ -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',
@@ -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) {
@@ -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 => a.type === 'Identifier' && sensitivePathVars.has(a.name))) {
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
  */