muaddib-scanner 2.2.8 → 2.2.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.fr.md CHANGED
@@ -30,7 +30,7 @@
30
30
 
31
31
  Les attaques supply-chain npm et PyPI explosent. Shai-Hulud a compromis 25K+ repos en 2025. Les outils existants détectent, mais n'aident pas à répondre.
32
32
 
33
- MUAD'DIB combine analyse statique + analyse dynamique (sandbox Docker) + **détection comportementale d'anomalies** (v2.0) + **validation ground truth** (v2.1) pour détecter les menaces ET guider votre réponse — même avant leur apparition dans une base d'IOC.
33
+ MUAD'DIB combine analyse statique + **moteur de désobfuscation** (v2.2.5) + **dataflow inter-module** (v2.2.6) + analyse dynamique (sandbox Docker) + **détection comportementale d'anomalies** (v2.0) + **validation ground truth** (v2.1) pour détecter les menaces ET guider votre réponse — même avant leur apparition dans une base d'IOC.
34
34
 
35
35
  ---
36
36
 
@@ -431,6 +431,10 @@ Détecte les patterns malveillants dans les fichiers YAML `.github/workflows/`,
431
431
  | Hooking prototype (fetch, XMLHttpRequest) | T1557 | AST |
432
432
  | Injection workflow (.github/workflows) | T1195.002 | AST |
433
433
  | Harvest crypto wallets | T1005 | Dataflow |
434
+ | Require cache poisoning | T1574.001 | AST |
435
+ | Staged eval decode (eval+atob/Buffer) | T1140 | AST |
436
+ | Désobfuscation (string concat, charcode, base64, hex) | T1140 | Pré-traitement AST |
437
+ | Dataflow cross-fichiers (exfiltration inter-module) | T1041 | Module graph |
434
438
 
435
439
  ---
436
440
 
@@ -642,7 +646,7 @@ Les alertes apparaissent dans Security > Code scanning alerts.
642
646
  ## Architecture
643
647
 
644
648
  ```
645
- MUAD'DIB 2.2 Scanner
649
+ MUAD'DIB 2.2.9 Scanner
646
650
  |
647
651
  +-- IOC Match (225 000+ packages, JSON DB)
648
652
  | +-- OSV.dev npm dump (200K+ entrées MAL-*)
@@ -654,7 +658,17 @@ MUAD'DIB 2.2 Scanner
654
658
  | +-- Snyk Known Malware
655
659
  | +-- Static IOCs (Socket, Phylum)
656
660
  |
657
- +-- 13 Scanners Parallèles (86 règles)
661
+ +-- Pré-traitement Désobfuscation (v2.2.5, --no-deobfuscate pour désactiver)
662
+ | +-- String concat folding, reconstruction CharCode
663
+ | +-- Décodage Base64, résolution Hex array
664
+ | +-- Propagation de constantes (Phase 2)
665
+ |
666
+ +-- Dataflow Inter-module (v2.2.6, --no-module-graph pour désactiver)
667
+ | +-- Graphe de dépendances, annotation exports teintés
668
+ | +-- Chaînes re-export 3 hops, analyse méthodes de classe
669
+ | +-- Détection credential read → network sink cross-fichiers
670
+ |
671
+ +-- 14 Scanners Parallèles (~95 règles)
658
672
  | +-- AST Parse (acorn) — eval/Function, credential CLI theft, binary droppers, prototype hooks
659
673
  | +-- Pattern Matching (shell, scripts)
660
674
  | +-- Typosquat Detection (npm + PyPI, Levenshtein)
@@ -678,6 +692,12 @@ MUAD'DIB 2.2 Scanner
678
692
  | +-- Décomposition Score (scoring explicable par règle)
679
693
  | +-- API Threat Feed (serveur HTTP, flux JSON pour SIEM)
680
694
  |
695
+ +-- Réduction FP Post-traitement (v2.2.8-v2.2.9)
696
+ | +-- Downgrade sévérité par comptage (dynamic_require, dataflow, etc.)
697
+ | +-- Cap scoring prototype_hook MEDIUM
698
+ | +-- Obfuscation dans dist/build → LOW
699
+ | +-- Filtrage env vars safe + préfixes
700
+ |
681
701
  +-- Paranoid Mode (ultra-strict)
682
702
  +-- Docker Sandbox (analyse comportementale, capture réseau, canary tokens, CI-aware)
683
703
  +-- Moniteur Zero-Day (interne : polling RSS npm + PyPI, alertes Discord, rapport quotidien)
@@ -696,14 +716,31 @@ Output (CLI, JSON, HTML, SARIF, Webhook, Threat Feed)
696
716
 
697
717
  ## Metriques d'evaluation
698
718
 
699
- | Version | TPR (Ground Truth) | FPR (Benign) | ADR (Adversarial) | Holdout (pre-tuning) | Date |
700
- |---------|-------------------|-------------|-------------------|---------------------|------|
701
- | 2.2.0 | 100.0% (4/4) | 0.0% (0/98) | 100.0% (35/35) | 30.0% (3/10) | 2026-02-20 |
719
+ | Metrique | Resultat | Details |
720
+ |----------|----------|---------|
721
+ | **TPR** (Ground Truth) | **100%** (4/4) | Attaques reelles : event-stream, ua-parser-js, coa, node-ipc |
722
+ | **FPR** (Benign) | **17.5%** (92/527) | 529 packages npm, vrai code source via `npm pack`, seuil > 20 |
723
+ | **ADR** (Adversarial) | **100%** (35/35) | 35 samples evasifs sur 4 vagues red team |
724
+ | **Holdouts** (pre-tuning) | 40/40 pass | Tous les holdouts passent apres corrections |
725
+
726
+ **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)
727
+
728
+ **Progression holdout** (scores pre-tuning, regles gelees) :
729
+
730
+ | Holdout | Score | Focus |
731
+ |---------|-------|-------|
732
+ | v1 | 30% (3/10) | Patterns generaux |
733
+ | v2 | 40% (4/10) | Env charcode, lifecycle, prototype |
734
+ | v3 | 60% (6/10) | Require cache, DNS TXT, reverse shell |
735
+ | v4 | **80%** (8/10) | Efficacite desobfuscation |
736
+ | v5 | 50% (5/10) | Dataflow inter-module (nouveau scanner) |
737
+
738
+ - **TPR** (True Positive Rate) : taux de detection sur 4 attaques supply-chain reelles (event-stream, ua-parser-js, coa, node-ipc)
739
+ - **FPR** (False Positive Rate) : packages avec score > 20 sur 529 packages npm reels (code source scanne, pas des dirs vides)
740
+ - **ADR** (Adversarial Detection Rate) : taux de detection sur 35 samples malveillants evasifs sur 4 vagues red team
741
+ - **Holdout** (pre-tuning) : taux de detection sur 10 samples jamais vus avec regles gelees (mesure de generalisation)
702
742
 
703
- - **TPR** (True Positive Rate) : taux de detection sur 5 attaques supply-chain reelles (event-stream, ua-parser-js, coa, node-ipc, colors)
704
- - **FPR** (False Positive Rate) : packages avec score > 20 sur 98 packages npm populaires
705
- - **ADR** (Adversarial Detection Rate) : taux de detection sur 35 samples malveillants evasifs (4 vagues red team + holdout promu)
706
- - **Holdout** (pre-tuning) : taux de detection sur 10 samples jamais vus avant correction des regles (mesure de generalisation)
743
+ Datasets : 529 npm + 132 PyPI packages benins, 35 samples adversariaux, 50 samples holdout (5 batches), 65 packages malveillants documentes.
707
744
 
708
745
  Voir [Evaluation Methodology](docs/EVALUATION_METHODOLOGY.md) pour le protocole experimental complet.
709
746
 
@@ -739,13 +776,13 @@ npm test
739
776
 
740
777
  ### Tests
741
778
 
742
- - **781 tests unitaires/intégration** sur 18 fichiers modulaires - 74% coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
779
+ - **822 tests unitaires/intégration** sur 20 fichiers modulaires - 74% coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
743
780
  - **56 tests de fuzzing** - YAML malformé, JSON invalide, fichiers binaires, ReDoS, unicode, inputs 10MB
744
781
  - **35 samples adversariaux** - Packages malveillants évasifs, taux de détection 35/35 (100% ADR)
782
+ - **50 samples holdout** - 5 batches de 10, scores pre-tuning : 30% → 40% → 60% → 80% → 50%
745
783
  - **8 tests multi-facteur typosquat** - Cas limites et comportement cache
746
784
  - **Validation ground truth** - 5/5 attaques réelles détectées (event-stream, ua-parser-js, coa, node-ipc, colors)
747
- - **Validation faux positifs** - 0/98 faux positifs sur packages npm populaires (0% FPR)
748
- - **Validation holdout** - 3/10 détection sur samples jamais vus avant correction (30% pre-tuning)
785
+ - **Validation faux positifs** - 17.5% FPR (92/527) sur vrai code source npm via `npm pack` (mesure honnête)
749
786
  - **Audit ESLint sécurité** - `eslint-plugin-security` avec 14 règles activées
750
787
 
751
788
  ---
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 static analysis + dynamic analysis (Docker sandbox) + **behavioral anomaly detection** (v2.0) + **ground truth validation** (v2.1) to detect threats AND guide your response — even before they appear in any IOC database.
33
+ MUAD'DIB combines static analysis + **deobfuscation engine** (v2.2.5) + **inter-module dataflow** (v2.2.6) + dynamic analysis (Docker sandbox) + **behavioral anomaly detection** (v2.0) + **ground truth validation** (v2.1) to detect threats AND guide your response — even before they appear in any IOC database.
34
34
 
35
35
  ---
36
36
 
@@ -432,6 +432,10 @@ Detects malicious patterns in `.github/workflows/` YAML files, including Shai-Hu
432
432
  | Prototype hooking (fetch, XMLHttpRequest) | T1557 | AST |
433
433
  | Workflow injection (.github/workflows) | T1195.002 | AST |
434
434
  | Crypto wallet harvesting | T1005 | Dataflow |
435
+ | Require cache poisoning | T1574.001 | AST |
436
+ | Staged eval decode (eval+atob/Buffer) | T1140 | AST |
437
+ | Deobfuscation (string concat, charcode, base64, hex) | T1140 | AST pre-processing |
438
+ | Cross-file dataflow (inter-module exfiltration) | T1041 | Module graph |
435
439
 
436
440
  ---
437
441
 
@@ -643,7 +647,7 @@ Alerts appear in Security > Code scanning alerts.
643
647
  ## Architecture
644
648
 
645
649
  ```
646
- MUAD'DIB 2.2 Scanner
650
+ MUAD'DIB 2.2.9 Scanner
647
651
  |
648
652
  +-- IOC Match (225,000+ packages, JSON DB)
649
653
  | +-- OSV.dev npm dump (200K+ MAL-* entries)
@@ -655,7 +659,17 @@ MUAD'DIB 2.2 Scanner
655
659
  | +-- Snyk Known Malware
656
660
  | +-- Static IOCs (Socket, Phylum)
657
661
  |
658
- +-- 13 Parallel Scanners (86 rules)
662
+ +-- Deobfuscation Pre-processing (v2.2.5, --no-deobfuscate to disable)
663
+ | +-- String concat folding, CharCode reconstruction
664
+ | +-- Base64 decode, Hex array resolution
665
+ | +-- Const propagation (Phase 2)
666
+ |
667
+ +-- Inter-module Dataflow (v2.2.6, --no-module-graph to disable)
668
+ | +-- Module dependency graph, tainted export annotation
669
+ | +-- 3-hop re-export chains, class method analysis
670
+ | +-- Cross-file credential read -> network sink detection
671
+ |
672
+ +-- 14 Parallel Scanners (~95 rules)
659
673
  | +-- AST Parse (acorn) — eval/Function, credential CLI theft, binary droppers, prototype hooks
660
674
  | +-- Pattern Matching (shell, scripts)
661
675
  | +-- Obfuscation Detection (skip .min.js, ignore hex/unicode alone)
@@ -681,6 +695,12 @@ MUAD'DIB 2.2 Scanner
681
695
  | +-- Score Breakdown (explainable per-rule scoring)
682
696
  | +-- Threat Feed API (HTTP server, JSON feed for SIEM)
683
697
  |
698
+ +-- FP Reduction Post-processing (v2.2.8-v2.2.9)
699
+ | +-- Count-based severity downgrade (dynamic_require, dataflow, etc.)
700
+ | +-- Framework prototype scoring cap
701
+ | +-- Obfuscation in dist/build → LOW
702
+ | +-- Safe env var + prefix filtering
703
+ |
684
704
  +-- Paranoid Mode (ultra-strict)
685
705
  +-- Docker Sandbox (behavioral analysis, network capture, canary tokens, CI-aware)
686
706
  +-- Zero-Day Monitor (internal: npm + PyPI RSS polling, Discord alerts, daily report)
@@ -699,14 +719,31 @@ Output (CLI, JSON, HTML, SARIF, Webhook, Threat Feed)
699
719
 
700
720
  ## Evaluation Metrics
701
721
 
702
- | Version | TPR (Ground Truth) | FPR (Benign) | ADR (Adversarial) | Holdout (pre-tuning) | Date |
703
- |---------|-------------------|-------------|-------------------|---------------------|------|
704
- | 2.2.0 | 100.0% (4/4) | 0.0% (0/98) | 100.0% (35/35) | 30.0% (3/10) | 2026-02-20 |
722
+ | Metric | Result | Details |
723
+ |--------|--------|---------|
724
+ | **TPR** (Ground Truth) | **100%** (4/4) | Real-world attacks: event-stream, ua-parser-js, coa, node-ipc |
725
+ | **FPR** (Benign) | **17.5%** (92/527) | 529 npm packages, real source code via `npm pack`, threshold > 20 |
726
+ | **ADR** (Adversarial) | **100%** (35/35) | 35 evasive samples across 4 red-team waves |
727
+ | **Holdouts** (pre-tuning) | 40/40 pass | All holdout samples pass after corrections |
728
+
729
+ **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)
730
+
731
+ **Holdout progression** (pre-tuning scores, rules frozen):
732
+
733
+ | Holdout | Score | Focus |
734
+ |---------|-------|-------|
735
+ | v1 | 30% (3/10) | General patterns |
736
+ | v2 | 40% (4/10) | Env charcode, lifecycle, prototype |
737
+ | v3 | 60% (6/10) | Require cache, DNS TXT, reverse shell |
738
+ | v4 | **80%** (8/10) | Deobfuscation effectiveness |
739
+ | v5 | 50% (5/10) | Inter-module dataflow (new scanner) |
740
+
741
+ - **TPR** (True Positive Rate): detection rate on 4 real-world supply-chain attacks (event-stream, ua-parser-js, coa, node-ipc)
742
+ - **FPR** (False Positive Rate): packages scoring > 20 out of 529 real npm packages (source code scanned, not empty dirs)
743
+ - **ADR** (Adversarial Detection Rate): detection rate on 35 evasive malicious samples across 4 red-team waves
744
+ - **Holdout** (pre-tuning): detection rate on 10 unseen samples with rules frozen (measures generalization)
705
745
 
706
- - **TPR** (True Positive Rate): detection rate on 5 real-world supply-chain attacks (event-stream, ua-parser-js, coa, node-ipc, colors)
707
- - **FPR** (False Positive Rate): packages scoring > 20 out of 98 popular npm packages
708
- - **ADR** (Adversarial Detection Rate): detection rate on 35 evasive malicious samples across 4 red-team waves + promoted holdout
709
- - **Holdout** (pre-tuning): detection rate on 10 unseen samples before any rule correction (measures generalization)
746
+ Datasets: 529 npm + 132 PyPI benign packages, 35 adversarial samples, 50 holdout samples (5 batches), 65 documented malware packages.
710
747
 
711
748
  See [Evaluation Methodology](docs/EVALUATION_METHODOLOGY.md) for the full experimental protocol.
712
749
 
@@ -742,13 +779,13 @@ npm test
742
779
 
743
780
  ### Testing
744
781
 
745
- - **781 unit/integration tests** across 18 modular test files - 74% code coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
782
+ - **822 unit/integration tests** across 20 modular test files - 74% code coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
746
783
  - **56 fuzz tests** - Malformed YAML, invalid JSON, binary files, ReDoS, unicode, 10MB inputs
747
784
  - **35 adversarial samples** - Evasive malicious packages, 35/35 detection rate (100% ADR)
785
+ - **50 holdout samples** - 5 batches of 10, pre-tuning scores: 30% → 40% → 60% → 80% → 50%
748
786
  - **8 multi-factor typosquat tests** - Edge cases and cache behavior
749
787
  - **Ground truth validation** - 5/5 real-world attacks detected (event-stream, ua-parser-js, coa, node-ipc, colors)
750
- - **False positive validation** - 0/98 false positives on popular npm packages (0% FPR)
751
- - **Holdout validation** - 3/10 detection on unseen samples before rule corrections (30% pre-tuning)
788
+ - **False positive validation** - 17.5% FPR (92/527) on real npm source code via `npm pack` (honest measurement)
752
789
  - **ESLint security audit** - `eslint-plugin-security` with 14 rules enabled
753
790
 
754
791
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.2.8",
3
+ "version": "2.2.9",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -73,7 +73,9 @@ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
73
73
  const FP_COUNT_THRESHOLDS = {
74
74
  dynamic_require: { maxCount: 10, from: 'HIGH', to: 'LOW' },
75
75
  dangerous_call_function: { maxCount: 5, from: 'MEDIUM', to: 'LOW' },
76
- require_cache_poison: { maxCount: 3, from: 'CRITICAL', to: 'LOW' }
76
+ require_cache_poison: { maxCount: 3, from: 'CRITICAL', to: 'LOW' },
77
+ suspicious_dataflow: { maxCount: 5, to: 'LOW' },
78
+ obfuscation_detected: { maxCount: 3, to: 'LOW' }
77
79
  };
78
80
 
79
81
  // Custom class prototypes that HTTP frameworks legitimately extend.
@@ -94,7 +96,7 @@ function applyFPReductions(threats) {
94
96
  // Count-based downgrade: if a threat type appears too many times,
95
97
  // it's a framework/plugin system, not malware
96
98
  const rule = FP_COUNT_THRESHOLDS[t.type];
97
- if (rule && typeCounts[t.type] > rule.maxCount && t.severity === rule.from) {
99
+ if (rule && typeCounts[t.type] > rule.maxCount && (!rule.from || t.severity === rule.from)) {
98
100
  t.severity = rule.to;
99
101
  }
100
102
 
@@ -638,10 +640,18 @@ async function run(targetPath, options = {}) {
638
640
  const mediumCount = deduped.filter(t => t.severity === 'MEDIUM').length;
639
641
  const lowCount = deduped.filter(t => t.severity === 'LOW').length;
640
642
 
643
+ // Cap MEDIUM prototype_hook contribution to 15 points max (5 × MEDIUM=3)
644
+ // Frameworks like Restify have 50+ prototype extensions that are not malicious
645
+ const mediumProtoHookCount = deduped.filter(t => t.type === 'prototype_hook' && t.severity === 'MEDIUM').length;
646
+ const PROTO_HOOK_MEDIUM_CAP = 15;
647
+ const protoHookPoints = Math.min(mediumProtoHookCount * SEVERITY_WEIGHTS.MEDIUM, PROTO_HOOK_MEDIUM_CAP);
648
+ const otherMediumCount = mediumCount - mediumProtoHookCount;
649
+
641
650
  let riskScore = 0;
642
651
  riskScore += criticalCount * SEVERITY_WEIGHTS.CRITICAL;
643
652
  riskScore += highCount * SEVERITY_WEIGHTS.HIGH;
644
- riskScore += mediumCount * SEVERITY_WEIGHTS.MEDIUM;
653
+ riskScore += otherMediumCount * SEVERITY_WEIGHTS.MEDIUM;
654
+ riskScore += protoHookPoints;
645
655
  riskScore += lowCount * SEVERITY_WEIGHTS.LOW;
646
656
  riskScore = Math.min(MAX_RISK_SCORE, riskScore);
647
657
 
@@ -31,9 +31,15 @@ const SENSITIVE_STRINGS = [
31
31
  // Env vars that are safe and should NOT be flagged (common config/runtime vars)
32
32
  const SAFE_ENV_VARS = [
33
33
  'NODE_ENV', 'PORT', 'HOST', 'HOSTNAME', 'PWD', 'HOME', 'PATH',
34
- 'LANG', 'TERM', 'CI', 'DEBUG', 'VERBOSE', 'LOG_LEVEL'
34
+ 'LANG', 'TERM', 'CI', 'DEBUG', 'VERBOSE', 'LOG_LEVEL',
35
+ 'SHELL', 'USER', 'LOGNAME', 'EDITOR', 'TZ',
36
+ 'NODE_DEBUG', 'NODE_PATH', 'NODE_OPTIONS',
37
+ 'DISPLAY', 'COLORTERM', 'FORCE_COLOR', 'NO_COLOR', 'TERM_PROGRAM'
35
38
  ];
36
39
 
40
+ // Env var prefixes that are safe (npm metadata, locale settings)
41
+ const SAFE_ENV_PREFIXES = ['npm_config_', 'npm_lifecycle_', 'npm_package_', 'lc_'];
42
+
37
43
  // Env var keywords to detect sensitive environment access (separate from SENSITIVE_STRINGS)
38
44
  const ENV_SENSITIVE_KEYWORDS = [
39
45
  'TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL', 'AUTH'
@@ -914,6 +920,11 @@ function analyzeFile(content, filePath, basePath) {
914
920
  if (SAFE_ENV_VARS.includes(envVar)) {
915
921
  return;
916
922
  }
923
+ // Skip safe prefixes (npm_config_*, npm_lifecycle_*, npm_package_*, LC_*)
924
+ const envLower = envVar.toLowerCase();
925
+ if (SAFE_ENV_PREFIXES.some(p => envLower.startsWith(p))) {
926
+ return;
927
+ }
917
928
  // Flag only vars containing sensitive keywords
918
929
  if (ENV_SENSITIVE_KEYWORDS.some(s => envVar.toUpperCase().includes(s))) {
919
930
  threats.push({
@@ -29,7 +29,12 @@ function detectObfuscation(targetPath) {
29
29
 
30
30
  const signals = [];
31
31
  let score = 0;
32
- const isMinified = path.basename(file).endsWith('.min.js');
32
+ const basename = path.basename(file);
33
+ const isMinified = basename.endsWith('.min.js');
34
+ const isBundled = basename.endsWith('.bundle.js');
35
+ const pathParts = relativePath.split(path.sep);
36
+ const isInDistOrBuild = pathParts.some(p => p === 'dist' || p === 'build');
37
+ const isPackageOutput = isMinified || isBundled || isInDistOrBuild;
33
38
 
34
39
  // 1. Ratio code sur une seule ligne (skip .min.js — minification, not obfuscation)
35
40
  if (!isMinified) {
@@ -86,7 +91,7 @@ function detectObfuscation(targetPath) {
86
91
  if (score >= 40) {
87
92
  threats.push({
88
93
  type: 'obfuscation_detected',
89
- severity: score >= 70 ? 'CRITICAL' : 'HIGH',
94
+ severity: isPackageOutput ? 'LOW' : (score >= 70 ? 'CRITICAL' : 'HIGH'),
90
95
  message: `Code obfusque (score: ${score}). Signaux: ${signals.join(', ')}`,
91
96
  file: relativePath
92
97
  });