fix: Propagation globale sélective pour corriger fuites dates CRO
Problème: - 36 CRO avec fuites dates de naissance (Né(e) le DD/MM/YYYY) - Dates détectées page 0 mais pas propagées pages suivantes - Désactivation propagation globale avait éliminé 951 FP mais créé fuites Solution: - Propagation SÉLECTIVE: uniquement PII critiques (DATE_NAISSANCE, NIR, IPP, EMAIL, force_term) - PII non-critiques (TEL, ADRESSE, etc.) NON propagés (évite 951 FP) - Remplacement amélioré: gère variations format dates (/, ., -, espaces) - Gère contexte 'Né(e) le' avec case-insensitive Impact attendu: - Rappel: 100% (plus de fuites) - Précision: 85-87% (légère baisse vs 88.27%, mais acceptable) - FP réintroduits: ~10-20 (vs 951 avant) Fichiers: - anonymizer_core_refactored_onnx.py: propagation sélective + remplacement amélioré - tools/test_date_propagation.py: script test sur CRO - LEAK_FIX.md: documentation complète de la correction
This commit is contained in:
@@ -2032,19 +2032,32 @@ def process_pdf(
|
||||
# for token in _global_name_tokens:
|
||||
# anon.audit.append(PiiHit(page=-1, kind="NOM_GLOBAL", original=token, placeholder=PLACEHOLDERS["NOM"]))
|
||||
|
||||
# 4b) TEL, EMAIL, ADRESSE, CODE_POSTAL : propager les valeurs uniques sur toutes les pages
|
||||
# 4b) Propagation globale SÉLECTIVE : uniquement pour les PII critiques
|
||||
# Les PII critiques (DATE_NAISSANCE, NIR, IPP, EMAIL) sont propagés sur toutes les pages
|
||||
# pour éviter les fuites sur les documents multi-pages (ex: CRO)
|
||||
_CRITICAL_PII_TYPES = {"DATE_NAISSANCE", "NIR", "IPP", "EMAIL", "force_term", "force_regex"}
|
||||
|
||||
_global_pii: Dict[str, set] = {}
|
||||
for h in anon.audit:
|
||||
# Collecter TOUS les types pour analyse, mais ne propager que les critiques
|
||||
if h.kind in {"TEL", "EMAIL", "ADRESSE", "CODE_POSTAL", "EPISODE", "RPPS", "VILLE", "ETAB",
|
||||
"VLM_SERVICE", "VLM_ETAB", "DATE_NAISSANCE",
|
||||
"VLM_SERVICE", "VLM_ETAB", "DATE_NAISSANCE", "NIR", "IPP",
|
||||
"force_term", "force_regex"}:
|
||||
_global_pii.setdefault(h.kind, set()).add(h.original.strip())
|
||||
# DÉSACTIVÉ: Tous les types *_GLOBAL génèrent 951 FP avec 0 TP (100% faux positifs)
|
||||
# La propagation globale est trop agressive et ne détecte aucun vrai positif
|
||||
# for kind, values in _global_pii.items():
|
||||
# placeholder = PLACEHOLDERS.get(kind, PLACEHOLDERS["MASK"])
|
||||
# for val in values:
|
||||
# anon.audit.append(PiiHit(page=-1, kind=f"{kind}_GLOBAL", original=val, placeholder=placeholder))
|
||||
|
||||
# Propager UNIQUEMENT les PII critiques (évite les 951 FP des autres types)
|
||||
for kind, values in _global_pii.items():
|
||||
if kind not in _CRITICAL_PII_TYPES:
|
||||
continue # Skip non-critical PII (TEL, ADRESSE, etc.)
|
||||
|
||||
placeholder = PLACEHOLDERS.get(kind, PLACEHOLDERS["MASK"])
|
||||
for val in values:
|
||||
if not val or len(val) < 3: # Skip valeurs trop courtes
|
||||
continue
|
||||
anon.audit.append(PiiHit(page=-1, kind=f"{kind}_GLOBAL", original=val, placeholder=placeholder))
|
||||
|
||||
log.info("Propagation globale sélective : %d types critiques propagés",
|
||||
sum(1 for k in _global_pii.keys() if k in _CRITICAL_PII_TYPES))
|
||||
|
||||
# 4e) Appliquer les tokens globaux sur le texte pseudonymisé
|
||||
_GLOBAL_SKIP_KINDS = {"EDS_DATE_GLOBAL"}
|
||||
@@ -2061,12 +2074,35 @@ def process_pdf(
|
||||
# Garde trackare : NOM_GLOBAL très court (<=3) risque de masquer des codes diagnostics
|
||||
if anon.is_trackare and h.kind == "NOM_GLOBAL" and len(token) <= 3:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Traitement spécial pour DATE_NAISSANCE_GLOBAL : gérer les variations de format
|
||||
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)
|
||||
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"
|
||||
final_text = re.sub(
|
||||
rf'(?:Né(?:e)?\s+le\s+)?{date_pattern}',
|
||||
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
|
||||
if "-" in token:
|
||||
pat = pat.replace(r"\-", r"\-\s*")
|
||||
final_text = re.sub(rf"\b{pat}\b", h.placeholder, final_text)
|
||||
# Dates : tolérer variations de séparateurs
|
||||
if "/" in token or "." in token:
|
||||
pat = pat.replace(r"\.", r"[\s/.\-]").replace(r"\/", r"[\s/.\-]")
|
||||
|
||||
final_text = re.sub(rf"\b{pat}\b", h.placeholder, final_text, flags=re.IGNORECASE)
|
||||
except re.error:
|
||||
final_text = final_text.replace(token, h.placeholder)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user