fix: Propagation globale sélective v2 - Normalisation dates + Multi-pass

- Normalisation agressive des dates : génère 4 variations (/, ., -, espaces)
- Remplacement multi-pass : avec/sans contexte 'Né(e) le'
- Amélioration force_term : case-insensitive + word boundaries
- Outil de validation post-anonymisation
- Tests : 162 CRO, 0 fuite dates, 0 fuite CHCB (100% succès)
- Temps: 0.1s/doc

Résout les 36 CRO avec fuites identifiées dans l'audit initial.
This commit is contained in:
2026-03-02 12:22:58 +01:00
parent 871221ea56
commit f92da4d54e
251 changed files with 4676 additions and 23 deletions

View File

@@ -2043,7 +2043,29 @@ def process_pdf(
if h.kind in {"TEL", "EMAIL", "ADRESSE", "CODE_POSTAL", "EPISODE", "RPPS", "VILLE", "ETAB",
"VLM_SERVICE", "VLM_ETAB", "DATE_NAISSANCE", "NIR", "IPP",
"force_term", "force_regex"}:
_global_pii.setdefault(h.kind, set()).add(h.original.strip())
# Traitement spécial pour DATE_NAISSANCE : extraire la date pure et générer toutes les variations
if h.kind == "DATE_NAISSANCE":
# Extraire la date pure (DD/MM/YYYY ou DD/MM/YY)
date_match = re.search(r'(\d{1,2})[/.\-\s]+(\d{1,2})[/.\-\s]+(\d{2,4})', h.original)
if date_match:
day, month, year = date_match.groups()
# Normaliser les composants (ajouter zéro si nécessaire)
day = day.zfill(2)
month = month.zfill(2)
# Générer toutes les variations de séparateurs
date_variations = [
f"{day}/{month}/{year}",
f"{day}.{month}.{year}",
f"{day}-{month}-{year}",
f"{day} {month} {year}",
]
for var in date_variations:
_global_pii.setdefault(h.kind, set()).add(var)
else:
# Fallback : ajouter tel quel si pas de match
_global_pii.setdefault(h.kind, set()).add(h.original.strip())
else:
_global_pii.setdefault(h.kind, set()).add(h.original.strip())
# Propager UNIQUEMENT les PII critiques (évite les 951 FP des autres types)
for kind, values in _global_pii.items():
@@ -2076,23 +2098,40 @@ def process_pdf(
continue
try:
# Traitement spécial pour DATE_NAISSANCE_GLOBAL : gérer les variations de format
# Traitement spécial pour DATE_NAISSANCE_GLOBAL : gérer les variations de format et contexte
if h.kind == "DATE_NAISSANCE_GLOBAL":
# Extraire la date pure (DD/MM/YYYY ou DD/MM/YY)
date_match = re.search(r'\d{1,2}[/.\-]\d{1,2}[/.\-]\d{2,4}', token)
# Extraire les composants de la date (DD/MM/YYYY ou variations)
date_match = re.search(r'(\d{1,2})[/.\-\s]+(\d{1,2})[/.\-\s]+(\d{2,4})', token)
if date_match:
date_str = date_match.group(0)
# Normaliser les séparateurs pour le pattern
date_pattern = re.escape(date_str).replace(r'\/', r'[\s/.\-]').replace(r'\.', r'[\s/.\-]').replace(r'\-', r'[\s/.\-]')
# Remplacer avec ou sans contexte "Né(e) le"
day, month, year = date_match.groups()
# Pattern flexible qui accepte tous les séparateurs
# [\s/.\-]+ accepte : espace, slash, point, tiret (un ou plusieurs)
date_pattern = rf'{day}[\s/.\-]+{month}[\s/.\-]+{year}'
# Multi-pass replacement pour couvrir tous les cas
# Pass 1 : Avec contexte "Né(e) le" (case-insensitive)
final_text = re.sub(
rf'(?:Né(?:e)?\s+le\s+)?{date_pattern}',
rf'Né(?:e)?\s+le\s+{date_pattern}',
h.placeholder,
final_text,
flags=re.IGNORECASE
)
# Pass 2 : Sans contexte (date seule)
final_text = re.sub(
rf'\b{date_pattern}\b',
h.placeholder,
final_text,
flags=re.IGNORECASE
)
continue
# Traitement spécial pour force_term : remplacement case-insensitive avec word boundaries
if h.kind == "force_term_GLOBAL":
# Échapper les caractères spéciaux mais garder la flexibilité
pat = re.escape(token)
final_text = re.sub(rf'\b{pat}\b', h.placeholder, final_text, flags=re.IGNORECASE)
continue
# Traitement standard pour les autres types
pat = re.escape(token)
# Noms composés : tolérer les sauts de ligne/espaces autour du tiret