# Audit complet — Projet Anonymisation de documents médicaux **Date** : 28 mai 2026 **Audit réalisé par** : Qwen Code **Répertoire analysé** : `/home/dom/ai/anonymisation/` --- ## Sommaire 1. [Synthèse globale](#1-synthèse-globale) 2. [Risques critiques](#2-risques-critiques) 3. [Risques importants](#3-risques-importants) 4. [Appréciations positives](#4-appréciations-positives) 5. [Recommandations prioritaires](#5-recommandations-priorisées) 6. [Métriques](#6-métriques) 7. [Rapport détaillé par domaine](#7-rapport-détaillé-par-domaine) --- ## 1. Synthèse globale | Domaine | Appréciation | Risque | |---|---|---| | Architecture | ⚠️ Moyen | **Élevé** | | Core (`anonymizer_core_refactored_onnx.py`) | ⚠️ Fonctionnel mais fragile | **Élevé** | | Qualité du code | ⚠️ Moyen | **Moyen** | | Tests | ✅ Bon | **Moyen** | | Documentation | ✅ Très bon | **Faible** | | Sécurité / Conformité | ✅ Bon sur le papier, ⚠️ dans le code | **Moyen** | | Build / CI/CD | ⚠️ Moyen | **Moyen** | | Gestion du code mort | ❌ Problématique | **Moyen** | --- ## 2. Risques critiques ### 2.1 Fuites PII silencieuses (`except Exception: pass`) Le core de 4 770 lignes contient **~47 clauses `except Exception`**, dont ~20 sont des silences purs (`pass`). Les plus dangereux : | Localisation | Ligne(s) | Problème | |---|---|---| | `extract_text_with_fallback_ocr` | ~1118-1156 | Chaque passe d'extraction PDF (PyMuPDF, pdfplumber, pdfminer, docTR) capture l'exception sans log. Si PyMuPDF échoue silencieusement, on ne sait jamais pourquoi. | | `redact_pdf_vector` → `apply_redactions()` | ~3938 | Si la rédaction PDF échoue, le PDF de sortie peut être **non anonymisé** sans aucun avertissement. | | `_rasterize_page` (police DejaVu) | ~3991 | Fallback de police silencieux. | | `process_pdf` (VLM et NER) | ~4137, 4202 | Dégradation gracieuse acceptable, mais aucun log même en debug. | | Rédaction vectorielle dans `process_pdf` | ~4655 | Tout le bloc de rédaction est dans un `try/except: pass`. Le PDF peut ne pas être généré. | **Impact** : Un document contenant des données de santé personnelles (PHI) pourrait être délivré non anonymisé. Dans le contexte médical, c'est un risque réglementaire majeur (RGPD, hébergement HDS). **Recommandation** : Remplacer systématiquement `except Exception: pass` par `except Exception as e: log.warning("...", exc_info=e)` sur les chemins critiques. Minimum : logguer l'erreur. ### 2.2 Chemin absolu hardcodé dans le `.spec` ```python # anonymisation_onefile.spec app_dir = 'C:\\Users\\dom\\ai\\anonymisation' ``` Le build PyInstaller ne fonctionne que sur la machine de `dom`. Tout rebuild sur une autre machine échouera ou produira un binaire cassé. **Recommandation** : Utiliser `Path(__file__).parent` ou une variable d'environnement. ### 2.3 Regex recompilées à chaque ligne Des regex sont compilées inline dans `_mask_line_by_content`, appelée pour **chaque ligne de chaque page** : ```python _re_ville_date = re.compile(r"...", re.MULTILINE) _re_lieu = re.compile(r"(...)") _re_ville_res = re.compile(r"(...)") _stop_rx = re.compile(_MEDICAL_STOP_WORDS, re.IGNORECASE) ``` Sur un document de 50 pages / 2 000 lignes → **2 000 recompilations inutiles**. Dégradation estimée : **3-5x** sur gros documents. **Recommandation** : Compiler ces regex au niveau module (une seule fois) en variables globales. --- ## 3. Risques importants ### 3.1 Deux build systems parallèles incohérents | Système | Point d'entrée | Fonctionnalités | |---|---|---| | PyInstaller (`.spec`) | `launcher.py` | Splash, single-instance, téléchargement modèles | | Nuitka (`build_windows.bat`) | `Pseudonymisation_Gui_V5.py` | Direct GUI, sans setup | Les deux produisent des expériences utilisateur différentes et ciblent des points d'entrée différents. ### 3.2 Core double | Fichier | Lignes | Statut | |---|---|---| | `anonymizer_core_refactored.py` | 388 | Version incomplète, sans NER ONNX | | `anonymizer_core_refactored_onnx.py` | 4 770 | Version active | Un développeur pourrait importer le mauvais fichier par erreur. ### 3.3 ~2 000 lignes de code mort | Fichier | Lignes | Statut | |---|---|---| | `pseudonymisation_pipeline_gui_v3.py` | 439 | GUI V3 abandonnée | | `Pseudonymisation_Gui_Models_V4.py` | 390 | GUI V4 abandonnée | | `pseudonymisation_pipeline_robuste.py` | 627 | RobustEngine non utilisé dans le pipeline principal | | `Pseudonymisation_Pipeline_Robuste_Patch.py` | 167 | Patch probablement obsolète | | `anonymizer_core_refactored.py` | 388 | Core incomplet | ### 3.4 `_search_whole_word` — complexité N² `page.get_text("words")` est appelé **une fois par token** à chercher dans PyMuPDF : ```python for w in page.get_text("words"): wt = w[4].strip(".,;:!?()[]{}\"'«»-–—/\\") if wt.lower() == p_lower: rects.append(fitz.Rect(...)) ``` Pour 500 noms × 30 pages = **15 000 appels** à `get_text("words")`. ### 3.5 Injection via regex utilisateur Les `regex_overrides` du YAML ne sont pas validés. Un pattern comme `(.*)` avec `DOTALL` pourrait capturer tout le document. Aucune sandboxing n'est appliquée. ### 3.6 Données sensibles en mémoire Le fichier garde toutes les PII en mémoire (`anon.audit`) avec les valeurs originales non masquées. Pas de `del` ou de nettoyage explicite après usage. En cas de crash ou de dump mémoire, les données non anonymisées sont exposées. ### 3.7 Données de test exposées Les répertoires `test_*/` et `corpus_validation/` contiennent des fichiers `.audit.jsonl` et `.pseudonymise.txt` qui sont des **sorties réelles d'anonymisation** (potentiellement avec des données sensibles résiduelles). Ils ne devraient pas être versionnés. --- ## 4. Appréciations positives ### 4.1 Documentation riche et structurante - `cadrage-projet-anonymisation.md` — document de cadrage complet avec priorités, gates de release, gouvernance - `AIPD-anonymisation.md` — analyse d'impact sur la protection des données - `conformite-rgpd-ia-act.md` — conformité RGPD et IA Act - `annotation_guide.md` — guide d'annotation - `protocole-validation-humaine.md` — protocole de validation humaine - `spec-regles-administration.md` — spécifications des règles d'administration ### 4.2 Corpus de test solide - **27 documents réels** annotés manuellement dans `tests/ground_truth/` - **4 couches de tests** : unitaires, regression synthétique, corpus réel, validation humaine - **13 tests unitaires** pytest dans `tests/unit/` - **Score de référence : 97.0/100 [Grade A]** - Baseline enregistrée : 0 fuite audit, 0 fuite regex, 0 fuite INSEE haute ### 4.3 Architecture de configuration saine - Séparation `dictionnaires.default.yml` / `dictionnaires.yml` (overlay runtime) - 5 profils utilisateur dans `profiles.default.yml` (standard_local, chuxx_strict, partage_recherche, dossier_audit, demo) - Règles admin avec cycle de vie (draft/candidate/active) dans `admin_rules.default.yml` - Schéma JSON de validation dans `schemas/admin_rules.schema.json` ### 4.4 Pipeline d'anonymisation bien conçu - 5 passes d'extraction avec fallback (pdfplumber → pdfminer → PyMuPDF → docTR OCR → tesseract) - Propagation globale des PII sur toutes les pages - Rescan de sécurité post-anonymisation - Gazetteers Aho-Corasick pour FINESS, villes, noms de famille - NER multi-modèles : EDS-Pseudo (F1=0.97), CamemBERT-bio ONNX, GLiNER, VLM Ollama ### 4.5 Évaluation structurée - **5 axes** : LEAK_AUDIT, LEAK_REGEX, LEAK_INSEE, FP_DENSITY, FP_MEDICAL - Scoring pondéré avec notation A+ → F - Comparaison automatique avec baseline - Export JSON des résultats ### 4.6 Absence de dépendances circulaires Le graphe d'import est propre et acyclique. Les managers (`eds_pseudo_manager`, `gliner_manager`, `camembert_ner_manager`, `vlm_manager`, `ner_manager_onnx`) sont des feuilles du graphe (n'importent rien de local). ### 4.7 Build Windows mature - Signature Authenticode optionnelle - Inno Setup pour l'installateur - PyInstaller (onefile/onedir) + Nuitka - Workflows GitHub Actions pour le build automatique --- ## 5. Recommandations priorisées ### Priorité 1 — Sécurité (à faire immédiatement) | # | Action | Effort | Impact | |---|---|---|---| | 1.1 | Remplacer `except Exception: pass` par `except Exception as e: log.warning(...)` sur les chemins critiques (rédaction PDF, rescan) | 2h | 🔴 Élimine le risque de fuite silencieuse | | 1.2 | Corriger le chemin absolu dans `.spec` (utiliser `Path(__file__).parent`) | 15min | 🔴 Build reproductible | | 1.3 | Ajouter un mécanisme de wipe des PII en mémoire après la rédaction PDF (`del anon.audit`) | 30min | 🟡 Conformité RGPD — dump mémoire | ### Priorité 2 — Nettoyage du code mort | # | Action | Effort | Impact | |---|---|---|---| | 2.1 | Supprimer ou archiver les 3 GUI mortes (V3, V4, Robuste) | 30min | 🟡 Réduit la confusion | | 2.2 | Supprimer `anonymizer_core_refactored.py` ou le renommer `anonymizer_core_refactored_legacy.py` | 15min | 🟡 Évite l'import accidentel | | 2.3 | Déplacer les `test_*/` de la racine vers `tests/data/` | 30min | 🟡 Repository propre | | 2.4 | Supprimer `ano.zip`, `*.log` de la racine | 15min | 🟡 Hygiène | | 2.5 | Nettoyer les répertoires vides (`test_doctr_fix/`) | 10min | 🟡 Hygiène | ### Priorité 3 — Performance | # | Action | Effort | Impact | |---|---|---|---| | 3.1 | Compiler les regex de `_mask_line_by_content` au niveau module (une seule fois) | 1h | 🟢 3-5x plus rapide sur gros documents | | 3.2 | Factoriser `_search_whole_word` pour appeler `get_text("words")` une seule fois par page | 2h | 🟢 Réduction significative du temps de redaction | | 3.3 | Extraire `_collect_rects_for_hits()` commune à vector/raster | 3h | 🟡 Réduit la duplication (~400 lignes → ~250) | ### Priorité 4 — Qualité et maintenance | # | Action | Effort | Impact | |---|---|---|---| | 4.1 | Ajouter `pytest.ini` ou `pyproject.toml` avec config pytest | 30min | 🟡 Tests exécutables proprement | | 4.2 | Ajouter un workflow GitHub Actions pour les tests (pas juste le build) | 2h | 🟡 Non-régression automatique | | 4.3 | Ajouter `ruff` ou `flake8` dans la CI | 1h | 🟡 Qualité syntaxique | | 4.4 | Regrouper les ~40 magic numbers dans un bloc de constantes | 2h | 🟡 Configurable sans lire le code | | 4.5 | Unifier le nommage (tout en `snake_case`) | 4h | 🟡 Cohérence du projet | | 4.6 | Factoriser les 3 fonctions Aho-Corasick en une classe générique | 3h | 🟡 ~150 lignes de duplication éliminées | ### Priorité 5 — Alignement Linux/Windows | # | Action | Effort | Impact | |---|---|---|---| | 5.1 | Faire pointer `install.sh` vers `launcher.py` au lieu de la GUI directement | 30min | 🟡 Expérience Linux identique à Windows | | 5.2 | Tester le pipeline complet sur Linux (pas juste Windows) | 4h | 🟡 Portabilité | --- ## 6. Métriques | Métrique | Valeur | |---|---| | Lignes de code Python (hors venv) | ~14 270 | | Fichiers Python à la racine | 33 | | Fichiers Python morts estimés | ~6 (~2 000 lignes) | | Tests unitaires | 13 fichiers | | Documents ground truth | 27 | | Score qualité baseline | 97.0/100 [Grade A] | | `except Exception: pass` dans le core | ~20 | | Magic numbers dans le core | ~25 | | Versions de GUI coexistantes | 4 (1 active) | | Dépendances circulaires | 0 | | Fichiers `tools/` (scripts divers) | ~42 | | Workflows GitHub Actions | 2 (build uniquement) | --- ## 7. Rapport détaillé par domaine ### 7.1 Core (`anonymizer_core_refactored_onnx.py`) **Fonctions trop longues :** | Fonction | Lignes estimées | Complexité | |---|---|---| | `process_pdf` | ~200 | Très élevée : orchestration, regex, NER, rescan, nettoyage, whitelist, PDF | | `anonymise_document_regex` | ~180 | Très élevée : 10+ phases, logique NER-first, noms, tables | | `_extract_trackare_identity` | ~250 | Extrêmement élevée : 20+ patterns regex, nested functions | | `_mask_ville_gazetteers` | ~150 | Élevée : Aho-Corasick + énumérations + contexte géo + point fixe | | `redact_pdf_raster` | ~170 | Élevée : search, OCR, images, barcode, parallélisation | **Code dupliqué :** - `redact_pdf_vector` et `redact_pdf_raster` — structure quasi-identique (~200 lignes chacune), même pattern `by_page`, même déduplication, même fallback `_search_whole_word` - Regex multiline répétées (phase 0a à 0h-bis) — 8+ blocs identiques de pattern scanning - `_mask_finess_establishments`, `_mask_finess_addresses`, `_mask_ville_gazetteers` — 3 fonctions Aho-Corasick avec la même structure **Types et annotations :** Aucune fonction n'a de type hints sur les paramètres ou le retour, à l'exception des dataclasses. Le type `cfg: Dict[str, Any]` est utilisé partout — un `TypedDict` ou dataclass rendrait le code auto-documenté. **Imports inline :** Des `import re as _re`, `import numpy as np`, `from pyzbar.pyzbar import decode`, `from PIL import ImageFont` sont exécutés à l'intérieur de fonctions, parfois dans des boucles. ### 7.2 Architecture **Graphe de dépendances :** ``` launcher.py (point d'entrée Windows) | +-> anonymizer_core_refactored_onnx.py (CORE PRINCIPAL, 4 770 lignes) | +-> config_defaults.py | +-> admin_rules.py -> config_defaults.py | +-> detectors/hospital_filter.py | +-> ner_manager_onnx.py | +-> camembert_ner_manager.py | +-> eds_pseudo_manager.py | +-> gliner_manager.py | +-> vlm_manager.py | +-> Pseudonymisation_Gui_V5.py (GUI ACTIVE, 1 804 lignes) +-> anonymizer_core_refactored_onnx.py (déjà chargé) +-> ner_manager_onnx.py +-> eds_pseudo_manager.py +-> vlm_manager.py +-> config_defaults.py server.py (API FastAPI, point d'entrée microservice) +-> anonymizer_core_refactored_onnx.py +-> config_defaults.py +-> tous les managers (try/except import) ``` **Incohérences de nommage :** | Convention | Fichiers concernés | |---|---| | `snake_case.py` | Majorité : `launcher.py`, `server.py`, `config_defaults.py`, etc. | | `PascalCase.py` | `Pseudonymisation_Gui_V5.py`, `Pseudonymisation_Gui_Models_V4.py`, `Pseudonymisation_Pipeline_Robuste_Patch.py` | Le répertoire `Pseudonymiseur/` (majuscule, nom français) coexiste avec `ano/` (abréviation anglaise). ### 7.3 Tests **Points forts :** - Architecture de test à 4 couches bien documentée - 27 documents réels annotés manuellement — corpus sérieux - Manifest de regression synthétique avec critères `must_contain` / `must_not_contain` - Baseline de qualité enregistrée : score global 97.0/100 **Points faibles :** - Pas de configuration pytest formelle (pas de `pytest.ini` ou `pyproject.toml`) - 3 fichiers de test flottent à la racine du projet au lieu d'être dans `tests/` - 42 fichiers dans `tools/` mêlant tests, analyses et utilitaires — pas de séparation claire - Pas de mesure de couverture de code (`pytest-cov` non configuré) - Pas de workflow CI pour les tests automatiques ### 7.4 Configuration **Architecture saine :** | Fichier | Rôle | |---|---| | `dictionnaires.default.yml` | Template versionné — whitelist, blacklist, regex_overrides, phrases préservées | | `dictionnaires.yml` | Surcharge locale — actuellement vide (`{}`) | | `profiles.default.yml` | 5 profils : standard_local, chuxx_strict, partage_recherche, dossier_audit, demo | | `profiles.yml` | Surcharge locale — 2 profils créés depuis la GUI | | `admin_rules.default.yml` | Règles administrables avec cycle de vie (draft/candidate/active) | | `admin_rules.yml` | Surcharge locale — vide (`rules: []`) | **Point d'attention :** `admin_rules` n'est pas encore branché au pipeline principal — le fichier est un "contrat cible" pour un futur moteur. ### 7.5 CI/CD **Workflows existants :** | Workflow | Déclencheur | Environnement | Méthode | |---|---|---|---| | `build-windows.yml` | Tag `v*` ou manuel | `windows-latest`, Python 3.12 | Nuitka (standalone folder) | | `build-portable.yml` | Tag `v*` ou manuel | `windows-latest`, Python 3.12 | Python embarqué embeddable zip | **Ce qui manque :** - **Pas de workflow de test** (pas de `pytest` sur PR/push) - **Pas de linting** (pas de ruff, flake8, mypy) - **Pas de vérification de qualité** (pas de `evaluate_quality.py` dans la CI) - **Pas de build Linux** (uniquement Windows) - **Pas de vérification de sécurité** (dependabot, Trivy, etc.) ### 7.6 Régression **Suite de regression :** - 29 fichiers baseline dans `regression_tests/baseline/` - Script `check_regression.py` avec 7 types de fuites connues et 5 types de faux positifs identifiés **Problème :** Le script utilise un **chemin absolu en dur** vers les sorties : ``` /home/dom/Téléchargements/II-1 Ctrl_T2A_2025_CHUXX_DocJustificatifs (1)/anonymise_audit_30 ``` Ce chemin n'est pas portable. ### 7.7 `detectors/` Un seul fichier : `detectors/hospital_filter.py` La méthode `should_filter()` retourne **toujours `False``. Les coordonnées hospitalières ont été validées comme devant être masquées car elles identifient indirectement le patient (contrôle humain du 2026-03-12). Le filtre est donc essentiellement inactivé, sauf pour les épisodes Trackare via `filter_detections()`. Ce répertoire semble être un emplacement prévu pour de futurs detecteurs mais qui n'a pas été étoffé. --- ## 8. Résumé exécutif Le projet dispose d'une **bonne base conceptuelle** : pipeline d'anonymisation bien pensé, documentation de qualité professionnelle, corpus de test sérieux avec score de 97/100. L'architecture de configuration (default/overlay) et le graphe de dépendances (acyclique) sont propres. Cependant, l'**évolution organique** a accumulé : - Des **risques de sécurité** silencieux (`except: pass` sur les chemins critiques de rédaction PDF) - ~**2 000 lignes de code mort** (GUI V3/V4, core incomplet, patch obsolète) - Des **incohérences** de nommage et de build (PyInstaller vs Nuitka, chemins absolus) - Des **problèmes de performance** évitables (regex recompilées, appels N² à PyMuPDF) **Les 3 actions prioritaires** qui apportent le plus de valeur immédiatement : 1. **Sécuriser les chemins critiques** : logguer les exceptions au lieu de les ignorer sur la rédaction PDF et le rescan 2. **Nettoyer le code mort** : supprimer les GUI abandonnées et le core incomplet 3. **Rendre le build reproductible** : corriger le chemin absolu dans le `.spec` Ces 3 actions combinées représentent moins de 3h de travail et éliminent les risques les plus sérieux.