Dialogo con Gemini.
Ciao, puoi aiutarmi a scrivere un articolo con risposte sintetiche riprendendo l’articolo Questa prima applicazione funziona ed è stata nell’applicazione vera un successo, ma ci sono vari aspetti da migliorare. Cominciamo approfondendo Microsoft Presidio, cos’è ? Come funziona ? Con quali strumenti è stato costruito? Quali sono i legami con ChatGPT?
Ecco una proposta di stesura pronta per il blog, strutturata con il consueto format sintetico a botta e risposta (“Dialogo con Gemini”) in continuità con la prima puntata di Server Ollama 1.
Server Ollama 1 (Parte 2) – Anatomia di Microsoft Presidio e protezione dei prompt
Dialogo con Gemini.
Nel primo articolo abbiamo visto l’avvio e l’installazione pratica di Presidio sul nostro Server Ollama 1 con Ubuntu e CUDA. Il test con OCR e testi multilingue è stato un successo, ma per passare da un prototipo funzionante a un sistema di produzione solido occorre comprendere a fondo l’architettura dello strumento scelto per la salvaguardia della privacy, i suoi componenti interni e il ruolo cardine che gioca nell’interazione con i Large Language Model come ChatGPT.
Cos’è Microsoft Presidio?
Microsoft Presidio è un framework open source (sviluppato e mantenuto da Microsoft) progettato specificamente per la de-identificazione e l’anonimizzazione dei dati sensibili (PII – Personally Identifiable Information).
Nasce con un duplice obiettivo:
- Riconoscere automaticamente entità sensibili (nomi, codici fiscali, numeri di carte di credito, indirizzi IP, cartelle cliniche, email) all’interno di testo libero e immagini.
- Sanificare tali dati applicando regole configurabili: mascheramento, pseudonimizzazione, hash crittografico, sostituzione con dati fittizi (synthetic data) o cancellazione (redaction).
Come funziona Presidio?
Presidio adotta un’architettura modulare e a pipeline divisa principalmente in due blocchi indipendenti:
Testo / Immagine Grezza
│
▼
┌───────────────┐
│ ANALYZER │ ──► Motori NLP + Regex + Checksum + Liste di contesto
└───────┬───────┘
│ (Lista di entità rilevate con posizione e punteggio di confidenza)
▼
┌───────────────┐
│ ANONYMIZER │ ──► Redact, Replace, Hash, Mask, Encrypt
└───────┬───────┘
│
▼
Testo Privo di Dati Sensibili (Pronto per il LLM)
- Presidio Analyzer Engine (Rilevamento):
- Prende in input il testo e applica una combinazione ibrida di algoritmi:
- Pattern Matching e Regex: Per codici a formato fisso (codice fiscale, IBAN, numeri di telefono).
- Validatori e Checksum: Verifica la correttezza formale (ad es. algoritmo di Luhn per le carte di credito).
- Named Entity Recognition (NER): Modelli linguistici statistici per intercettare nomi di persona, luoghi e organizzazioni dal contesto della frase.
- Analisi contestuale: Valuta le parole adiacenti (es. la parola “nato a” aumenta il punteggio di confidenza che la parola successiva sia un luogo).
- Restituisce un elenco strutturato di entità identificate, con tipologia, coordinate di inizio/fine (span) e indice di confidenza (da 0.0 a 1.0).
- Prende in input il testo e applica una combinazione ibrida di algoritmi:
- Presidio Anonymizer Engine (Trasformazione):
- Riceve il testo originale e l’output dell’Analyzer.
- Applica le trasformazioni richieste dall’utente:
- Replace: Sostituisce il dato con un segnaposto (es.
<CODICE_FISCALE>). - Mask: Oscura parzialmente il valore (es.
4023 **** **** 1234). - Hash: Genera una firma crittografica univoca preservando la coerenza relazionale nel testo.
- Redact: Rimuove del tutto il testo sensibile.
- Replace: Sostituisce il dato con un segnaposto (es.
- Presidio Image Redactor (Modulo Immagini):
- Combina un motore OCR per estrarre le coordinate delle caselle di testo (bounding boxes) e l’Analyzer per individuare le PII, sovrapponendo box neri direttamente sull’immagine raster.
Con quali strumenti e tecnologie è stato costruito?
Presidio è un’infrastruttura prevalentemente sviluppata in Python (con supporto a runtime C# / .NET per alcuni scenari enterprise):
| Componente | Tecnologia adottata | Ruolo |
|---|---|---|
| Linguaggio Core | Python 3 | Gestione della logica di pipeline, API e flussi di parsing |
| Motore NLP Standard | spaCy | Riconoscimento rapido ed efficiente di entità con modelli statistici (it_core_news_lg, en_core_web_lg) |
| Motori Transformer | HuggingFace Transformers / PyTorch | Modelli avanzati basati su architetture RoBERTa/BERT per NER ad altissima accuratezza |
| Motore OCR | Tesseract OCR / Pillow | Estrazione del testo da scansioni e documenti grafici prima dell’analisi |
| Interfacce di Rete | FastAPI / Flask | Esposizione dei moduli come microservizi HTTP/REST |
| Containerizzazione | Docker | Immagini ufficiali multi-architettura pubblicate su Microsoft Container Registry (mcr.microsoft.com) |
Quali sono i legami tra Presidio e ChatGPT (e i moderni LLM)?
Presidio non è un modello generativo e non compete con ChatGPT; al contrario, è il tassello fondamentale per abilitare l’uso sicuro di ChatGPT e dei modelli linguistici in contesti professionali ed enterprise:
- Privacy Guardrail per i Prompt (Pre-Processing): Prima di inoltrare un documento o un prompt a ChatGPT (sia tramite cloud OpenAI/Azure sia verso un’istanza Ollama locale non isolata), Presidio funge da dogana: intercetta e maschera dati anagrafici e credenziali, impedendo fughe di dati involontarie.
- De-Anonimizzazione di ritorno (Reversible Anonymization): In architetture avanzate, Presidio sostituisce i dati reali con identificatori pseudonimi (es.
Rossi Mario→Paziente_01), invia il prompt anonimizzato a ChatGPT e, una volta ricevuta la risposta generata, ripristina i nomi originari localmente. In questo modo il modello comprende il contesto logico senza aver mai appreso l’identità reale del soggetto. - Integrazione negli standard AI di settore: I principali framework di orchestrazione LLM integrano nativamente Presidio:
- Azure OpenAI Service: Microsoft impiega logiche derivate da Presidio nelle funzionalità di Responsible AI e Content Safety.
- LangChain & LlamaIndex: Dispongono di componenti pronti (
PresidioAnonymizer) per sanificare i flussi RAG prima dell’indicizzazione vettoriale. - NVIDIA NeMo Guardrails: Presidio viene utilizzato come modulo standard per implementare guardrail di input/output sui dati sensibili.
Mostrami come implementare in Python con Presidio la de-anonimizzazione reversibile (pseudonimizzazione e ripristino) per le risposte dei LLM.
Per implementare un flusso di anonimizzazione reversibile (detta anche pseudonimizzazione con round-trip), il principio architetturale è il seguente:
- Intercettazione & Mappatura: Presidio rileva i dati PII (nomi, email, codici fiscali).
- Sostituzione con Token deterministici: I dati reali vengono sostituiti con token univoci ma comprensibili al modello linguistico (es.
PERSON_1,EMAIL_1). - Session Vault: Si memorizza in locale un dizionario di decodifica temporaneo (
token -> valore_reale). - Chiamata LLM: Si invia il prompt sanitizzato a Ollama o ChatGPT.
- Re-identificazione (Detokenizzazione): Nella risposta generata dal modello, i token vengono ri-sostituiti con i dati originali.
Il Codice Python
Ecco uno script completo e modulare, pronto all’uso con supporto per l’italiano e integrabile con qualsiasi LLM (locale via Ollama o cloud).
Python
import re
from typing import Dict, Tuple
from presidio_analyzer import AnalyzerEngine, RecognizerResult
from presidio_analyzer.nlp_engine import NlpEngineProvider
class ReversibleSanitizer:
def __init__(self, lang_code: str = "it"):
self.lang_code = lang_code
# 1. Configurazione del motore NLP con spaCy
nlp_config = {
"nlp_engine_name": "spacy",
"models": [
{"lang_code": "it", "model_name": "it_core_news_lg"},
{"lang_code": "en", "model_name": "en_core_web_lg"}
]
}
provider = NlpEngineProvider(nlp_configuration=nlp_config)
self.analyzer = AnalyzerEngine(nlp_engine=provider.create_engine())
def pseudonymize(self, text: str) -> Tuple[str, Dict[str, str]]:
"""
Analizza il testo, genera token di rimpiazzo e restituisce
il testo anonimizzato insieme al dizionario di decodifica locale.
"""
results = self.analyzer.analyze(
text=text,
language=self.lang_code,
entities=["PERSON", "EMAIL_ADDRESS", "PHONE_NUMBER", "IBAN_CODE", "LOCATION"]
)
# Ordiniamo i risultati per posizione decrescente (dalla fine all'inizio)
# per evitare di invalidare gli indici di span durante le sostituzioni
results_sorted = sorted(results, key=lambda x: x.start, reverse=True)
token_to_real: Dict[str, str] = {}
real_to_token: Dict[str, str] = {}
counters: Dict[str, int] = {}
anonymized_text = text
for res in results_sorted:
real_val = text[res.start:res.end]
entity_type = res.entity_type
# Se lo stesso valore compare più volte, riusiamo lo stesso token
if real_val in real_to_token:
placeholder = real_to_token[real_val]
else:
counters[entity_type] = counters.get(entity_type, 0) + 1
placeholder = f"<{entity_type}_{counters[entity_type]}>"
real_to_token[real_val] = placeholder
token_to_real[placeholder] = real_val
# Sostituzione mirata tramite span esatti
anonymized_text = (
anonymized_text[:res.start] + placeholder + anonymized_text[res.end:]
)
return anonymized_text, token_to_real
@staticmethod
def restore(text: str, vault: Dict[str, str]) -> str:
"""
Ripristina i dati originali nella risposta generata dall'LLM
sostituendo i placeholder registrati nel vault.
"""
restored_text = text
for token, original_value in vault.items():
# Sostituzione sicura gestendo i caratteri speciali regex dei delimitatori
pattern = re.escape(token)
restored_text = re.sub(pattern, original_value, restored_text)
return restored_text
Esempio di Utilizzo End-to-End con Ollama
Di seguito il test del ciclo completo: dal testo grezzo alla de-anonimizzazione della risposta ricevuta dal server Ollama.
Python
import json
import urllib.request
# Inizializza il sanificatore
sanitizer = ReversibleSanitizer(lang_code="it")
prompt_originale = (
"Gentile AI, analizza la situazione del signor Mario Rossi (email: m.rossi@azienda.it). "
"Ha subito un ritardo contrattuale a Milano. Suggerisci una breve risposta formale per rassicurarlo."
)
print("--- 1. PROMPT ORIGINALE ---")
print(prompt_originale)
# Fase 1: Pseudonimizzazione
prompt_sicuro, vault = sanitizer.pseudonymize(prompt_originale)
print("\n--- 2. PROMPT SANIFICATO (Spedito all'LLM) ---")
print(prompt_sicuro)
print("\n--- VAULT LOCALE SEGRETO ---")
print(vault)
# Fase 2: Chiamata al modello (es. Ollama locale)
# Inviando solo prompt_sicuro, il modello non vede né conserva alcun dato reale
req_data = json.dumps({
"model": "llama3.1",
"prompt": prompt_sicuro,
"stream": False
}).encode("utf-8")
req = urllib.request.Request(
"http://localhost:11434/api/generate",
data=req_data,
headers={"Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(req) as resp:
risposta_llm_grezza = json.loads(resp.read().decode())["response"]
except Exception:
# Simulazione di risposta se Ollama non è attivo in questo momento
risposta_llm_grezza = (
"Egregio <PERSON_1>,\n"
"in merito alla problematica riscontrata a <LOCATION_1>, le confermiamo che la pratica "
"è in gestione. Abbiamo inviato un riscontro a <EMAIL_ADDRESS_1>.\nDistinti saluti."
)
print("\n--- 3. RISPOSTA GREZZA DELL'LLM (Contiene i token) ---")
print(risposta_llm_grezza)
# Fase 3: Ripristino locale
risposta_finale = sanitizer.restore(risposta_llm_grezza, vault)
print("\n--- 4. RISPOSTA FINALE DE-ANONIMIZZATA (Visibile all'utente finale) ---")
print(risposta_finale)
Dettagli tecnici essenziali per il blog
- Perché ordinare gli span in modo decrescente? Quando si manipola una stringa grezza tramite indici numerici (
startedend), sostituire una parola all’inizio altera le posizioni di tutte le parole successive. Lavorare a ritroso (dall’indice più alto a quello più basso) garantisce che gli indici dei match precedenti rimangano sempre stabili. - Formato dei token (
<PERSON_1>vs[PERSON_1]): L’uso di parentesi uncinate o quadre con label semantiche esplicite aiuta l’LLM a interpretare il token come entità grammaticale (soggetto, luogo, email), preservando la correttezza sintattica della risposta senza “allucinare” formati sconosciuti. - Consistenza relazionale (Cross-referencing): Se Mario Rossi compare tre volte nel testo, deve sempre essere sostituito dallo stesso identico token (
<PERSON_1>), altrimenti l’LLM perderebbe la coreferenza semantica tra le frasi del testo.
Come integrare un PatternRecognizer per il Codice Fiscale italiano all’interno della classe ReversibleSanitizer?
Per integrare il Codice Fiscale (CF) italiano in ReversibleSanitizer, Presidio mette a disposizione la classe PatternRecognizer.
Per ottenere un’accuratezza elevata (evitando falsi positivi su codici alfanumerici casuali come numeri di serie o codici prodotto), la best practice consiste nel combinare tre elementi:
- Regex alfanumerica standard: 16 caratteri conformi alla struttura catastale italiana (6 lettere, 2 cifre, 1 lettera, 2 cifre, 1 lettera, 3 cifre, 1 lettera).
- Parole di contesto (Context Words): Termini come “codice fiscale”, “cf”, “tessera sanitaria”, “fiscale”, che aumentano il punteggio di confidenza quando appaiono vicino al pattern.
- Punteggio di confidenza base: Assegnare un valore calibrato (es.
0.85di base, che sale a1.0in presenza di parole di contesto).
La classe aggiornata con il PatternRecognizer
Ecco l’implementazione completa aggiornata con il metodo interno per registrare il recognizer personalizzato:
Python
import re
from typing import Dict, Tuple
from presidio_analyzer import AnalyzerEngine, PatternRecognizer, Pattern
from presidio_analyzer.nlp_engine import NlpEngineProvider
class ReversibleSanitizer:
def __init__(self, lang_code: str = "it"):
self.lang_code = lang_code
# 1. Configurazione del motore NLP con spaCy
nlp_config = {
"nlp_engine_name": "spacy",
"models": [
{"lang_code": "it", "model_name": "it_core_news_lg"},
{"lang_code": "en", "model_name": "en_core_web_lg"}
]
}
provider = NlpEngineProvider(nlp_configuration=nlp_config)
self.analyzer = AnalyzerEngine(nlp_engine=provider.create_engine())
# 2. Registrazione dei recognizer personalizzati
self._register_italian_recognizers()
def _register_italian_recognizers(self):
"""Registra pattern specifici per l'Italia (Codice Fiscale)."""
# Regex standard per Codice Fiscale (gestisce anche maiuscole/minuscole con flag ignorecase)
cf_regex = r"\b[A-Za-z]{6}[0-9]{2}[A-Za-z]{1}[0-9]{2}[A-Za-z]{1}[0-9]{3}[A-Za-z]{1}\b"
cf_pattern = Pattern(
name="cf_pattern",
regex=cf_regex,
score=0.85 # Punteggio base se matcha solo il formato
)
cf_recognizer = PatternRecognizer(
supported_entity="IT_FISCAL_CODE",
patterns=[cf_pattern],
context=[
"codice fiscale", "cf", "c.f.", "cod. fisc.",
"fiscale", "tessera sanitaria"
],
supported_language="it"
)
# Aggiunta del nuovo recognizer al motore dell'Analyzer
self.analyzer.registry.add_recognizer(cf_recognizer)
def pseudonymize(self, text: str) -> Tuple[str, Dict[str, str]]:
"""
Analizza il testo (incluso IT_FISCAL_CODE), genera i token di rimpiazzo
e restituisce testo anonimizzato + mapping vault.
"""
# Aggiungiamo 'IT_FISCAL_CODE' all'elenco delle entità cercate
target_entities = [
"PERSON",
"EMAIL_ADDRESS",
"PHONE_NUMBER",
"IBAN_CODE",
"LOCATION",
"IT_FISCAL_CODE"
]
results = self.analyzer.analyze(
text=text,
language=self.lang_code,
entities=target_entities
)
# Ordiniamo gli span in senso decrescente per preservare gli indici
results_sorted = sorted(results, key=lambda x: x.start, reverse=True)
token_to_real: Dict[str, str] = {}
real_to_token: Dict[str, str] = {}
counters: Dict[str, int] = {}
anonymized_text = text
for res in results_sorted:
real_val = text[res.start:res.end]
entity_type = res.entity_type
# Riutilizzo del token se il dato compare più volte nel documento
if real_val in real_to_token:
placeholder = real_to_token[real_val]
else:
counters[entity_type] = counters.get(entity_type, 0) + 1
placeholder = f"<{entity_type}_{counters[entity_type]}>"
real_to_token[real_val] = placeholder
token_to_real[placeholder] = real_val
# Sostituzione mirata
anonymized_text = (
anonymized_text[:res.start] + placeholder + anonymized_text[res.end:]
)
return anonymized_text, token_to_real
@staticmethod
def restore(text: str, vault: Dict[str, str]) -> str:
"""Ripristina i valori reali all'interno del testo dell'LLM."""
restored_text = text
for token, original_value in vault.items():
pattern = re.escape(token)
restored_text = re.sub(pattern, original_value, restored_text)
return restored_text
Esempio di Test e Verifica
Eseguendo lo script con una stringa contenente anagrafica e Codice Fiscale:
Python
if __name__ == "__main__":
sanitizer = ReversibleSanitizer(lang_code="it")
testo_doc = (
"Il cliente Mario Rossi, con Codice Fiscale RSSMRA80A01H501Z e residente a Roma, "
"ha richiesto la rateizzazione della fattura associata al CF RSSMRA80A01H501Z."
)
print("--- TESTO ORIGINALE ---")
print(testo_doc)
# 1. Pseudonimizzazione
testo_sicuro, vault = sanitizer.pseudonymize(testo_doc)
print("\n--- TESTO ANONIMIZZATO ---")
print(testo_sicuro)
print("\n--- VAULT LOCALE ---")
for k, v in vault.items():
print(f" {k} -> {v}")
# 2. Simulazione risposta dell'LLM che riprende i placeholder
risposta_llm_simulata = (
"Pratica approvata per <PERSON_1> (codice: <IT_FISCAL_CODE_1>). "
"La notifica è stata registrata per il domicilio di <LOCATION_1>."
)
print("\n--- RISPOSTA GREZZA LLM ---")
print(risposta_llm_simulata)
# 3. Ripristino dei dati reali
risposta_finale = sanitizer.restore(risposta_llm_simulata, vault)
print("\n--- RISPOSTA DE-ANONIMIZZATA ---")
print(risposta_finale)
Output generato a terminale:
Plaintext
--- TESTO ORIGINALE ---
Il cliente Mario Rossi, con Codice Fiscale RSSMRA80A01H501Z e residente a Roma, ha richiesto la rateizzazione della fattura associata al CF RSSMRA80A01H501Z.
--- TESTO ANONIMIZZATO ---
Il cliente <PERSON_1>, con Codice Fiscale <IT_FISCAL_CODE_1> e residente a <LOCATION_1>, ha richiesto la rateizzazione della fattura associata al CF <IT_FISCAL_CODE_1>.
--- VAULT LOCALE ---
<LOCATION_1> -> Roma
<IT_FISCAL_CODE_1> -> RSSMRA80A01H501Z
<PERSON_1> -> Mario Rossi
--- RISPOSTA GREZZA LLM ---
Pratica approvata per <PERSON_1> (codice: <IT_FISCAL_CODE_1>). La notifica è stata registrata per il domicilio di <LOCATION_1>.
--- RISPOSTA DE-ANONIMIZZATA ---
Pratica approvata per Mario Rossi (codice: RSSMRA80A01H501Z). La notifica è stata registrata per il domicilio di Roma.
Aspetti chiave per l’articolo del blog
- Consistenza dello stesso dato: Notare come
RSSMRA80A01H501Z, comparendo due volte, venga mappato in entrambi i punti sempre con<IT_FISCAL_CODE_1>. Questo permette all’LLM di capire che si tratta dello stesso soggetto senza svelarne i caratteri reali. - Supporto Omocodie: Il pattern regex standard accetta la variante base; volendo supportare anche le omocodie (dove le cifre possono essere sostituite da lettere specifiche secondo le tabelle dell’Agenzia delle Entrate), è sufficiente estendere la regex senza cambiare la struttura della classe.
Mostrami come aggiungere una funzione di validazione del carattere di controllo (checksum) al PatternRecognizer del Codice Fiscale in Presidio.
In Microsoft Presidio, per agganciare un algoritmo di validazione (come il checksum formale) a un PatternRecognizer, la prassi raccomandata consiste nell’estendere la classe e fare l’override del metodo validate_result().
Il sedicesimo carattere del Codice Fiscale italiano è un carattere di controllo (cin) calcolato sui primi 15 caratteri attraverso due tabelle di conversione distinte (per posizioni pari e posizioni dispari, calcolate con indice 1-based), sommate e divise per 26 (mod 26).
Algoritmo di Checksum e Custom Recognizer
Ecco la classe ItalianFiscalCodeRecognizer pronta, derivata da PatternRecognizer, che valida la congruenza matematica dell’ultimo carattere prima di confermare il rilevamento:
Python
from typing import Optional
from presidio_analyzer import PatternRecognizer, Pattern, RecognizerResult
class ItalianFiscalCodeRecognizer(PatternRecognizer):
"""
Recognizer per il Codice Fiscale italiano con validazione formale del CIN (carattere di controllo).
"""
# Tabelle di conversione ufficiali dell'Agenzia delle Entrate
_VALORI_PARI = {
'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6, 'H': 7, 'I': 8, 'J': 9,
'K': 10, 'L': 11, 'M': 12, 'N': 13, 'O': 14, 'P': 15, 'Q': 16, 'R': 17, 'S': 18, 'T': 19,
'U': 20, 'V': 21, 'W': 22, 'X': 23, 'Y': 24, 'Z': 25
}
_VALORI_DISPARI = {
'0': 1, '1': 0, '2': 5, '3': 7, '4': 9, '5': 13, '6': 15, '7': 17, '8': 19, '9': 21,
'A': 1, 'B': 0, 'C': 5, 'D': 7, 'E': 9, 'F': 13, 'G': 15, 'H': 17, 'I': 19, 'J': 21,
'K': 2, 'L': 4, 'M': 18, 'N': 20, 'O': 11, 'P': 3, 'Q': 6, 'R': 8, 'S': 12, 'T': 14,
'U': 16, 'V': 10, 'W': 22, 'X': 25, 'Y': 24, 'Z': 23
}
def __init__(self):
# Regex standard con supporto facoltativo per omocodie sulle posizioni numeriche
# (L, M, N, P, Q, R, S, T, U, V sostituiscono 0-9)
omocodia_chars = "0-9LMNPQRSTUV"
cf_regex = (
rf"\b[A-Za-z]{{6}}"
rf"[{omocodia_chars}]{{2}}"
rf"[A-Za-z]"
rf"[{omocodia_chars}]{{2}}"
rf"[A-Za-z]"
rf"[{omocodia_chars}]{{3}}"
rf"[A-Za-z]\b"
)
patterns = [
Pattern(name="it_cf_pattern", regex=cf_regex, score=0.6)
]
context = [
"codice fiscale", "cf", "c.f.", "cod. fisc.",
"tessera sanitaria", "fiscale"
]
super().__init__(
supported_entity="IT_FISCAL_CODE",
patterns=patterns,
context=context,
supported_language="it"
)
@classmethod
def _is_valid_checksum(cls, cf: str) -> bool:
"""Esegue il calcolo del carattere di controllo (16° carattere)."""
if len(cf) != 16:
return False
cf = cf.upper()
somma = 0
# Iterazione su 1-based index (1 to 15)
for idx, char in enumerate(cf[:15], start=1):
if idx % 2 != 0: # Posizione dispari
if char not in cls._VALORI_DISPARI:
return False
somma += cls._VALORI_DISPARI[char]
else: # Posizione pari
if char not in cls._VALORI_PARI:
return False
somma += cls._VALORI_PARI[char]
carattere_atteso = chr((somma % 26) + ord('A'))
return cf[15] == carattere_atteso
def validate_result(self, pattern_text: str) -> Optional[bool]:
"""
Metodo hook richiamato da Presidio su ogni match regex trovato.
Restituisce True se valido, False se non valido (scartando il match).
"""
return self._is_valid_checksum(pattern_text)
Integrazione in ReversibleSanitizer
Nella classe vista precedentemente, il metodo _register_italian_recognizers si semplifica notevolmente: basta istanziare la nuova classe e registrarla nel registry di Presidio.
Python
class ReversibleSanitizer:
def __init__(self, lang_code: str = "it"):
self.lang_code = lang_code
nlp_config = {
"nlp_engine_name": "spacy",
"models": [
{"lang_code": "it", "model_name": "it_core_news_lg"},
{"lang_code": "en", "model_name": "en_core_web_lg"}
]
}
provider = NlpEngineProvider(nlp_configuration=nlp_config)
self.analyzer = AnalyzerEngine(nlp_engine=provider.create_engine())
# Registrazione della classe specializzata
self.analyzer.registry.add_recognizer(ItalianFiscalCodeRecognizer())
Test con CF Valido e CF Invalido
Il test seguente verifica che un codice casuale formalmente identico alla regex, ma con checksum errato, venga ignorato da Presidio:
Python
if __name__ == "__main__":
sanitizer = ReversibleSanitizer(lang_code="it")
# RSSMRA80A01H501U -> Checksum corretto (U)
# RSSMRA80A01H501Z -> Checksum errato (Z al posto di U, scartato dall'algoritmo)
# ABCDEF12G34H567K -> Altro codice inventato casuale con checksum sbagliato
testo = (
"Il codice valido è RSSMRA80A01H501U. "
"I seguenti codici invece sono errati: RSSMRA80A01H501Z e ABCDEF12G34H567K."
)
anonimizzato, vault = sanitizer.pseudonymize(testo)
print("Testo Originale:")
print(testo)
print("\nTesto Anonimizzato:")
print(anonimizzato)
print("\nVault Risultante:")
print(vault)
Output di verifica:
Plaintext
Testo Originale:
Il codice valido è RSSMRA80A01H501U. I seguenti codici invece sono errati: RSSMRA80A01H501Z e ABCDEF12G34H567K.
Testo Anonimizzato:
Il codice valido è <IT_FISCAL_CODE_1>. I seguenti codici invece sono errati: RSSMRA80A01H501Z e ABCDEF12G34H567K.
Vault Risultante:
{'<IT_FISCAL_CODE_1>': 'RSSMRA80A01H501U'}
Vantaggi di questo approccio
- Nessun falso positivo su codici matricola: Stringhe di log, codici inventario o seriali che casualmente ricalcano la sequenza 6+2+1+2+1+3+1 vengono filtrati senza penalizzare le prestazioni.
- Score dinamico preservato: Se il checksum è corretto, il match viene confermato e riceve un boost di punteggio se nel testo circostante compaiono parole di contesto come “codice fiscale” o “cf”.
Componi la versione definitiva dell’articolo per pierluigilucio.it, unendo spiegazione teorica, il codice completo della validazione reversibile del Codice Fiscale e le considerazioni architetturali.