Seu prompt sabe demais: blindando dados sensíveis com Microsoft Presidio antes que eles cheguem à LLM
Sexta-feira, 17h40. Alguém do jurídico precisa destrinchar um contrato de 40 páginas até segunda. Abre o ChatGPT, cola o documento inteiro, digita "resume os riscos pra mim". Funciona lindamente. No mesmo prédio, um dev cola um log de produção com o e-mail e o telefone do cliente que reportou o bug. E numa healthtech do outro lado da cidade, alguém testa se a LLM resume bem uma evolução de paciente. Só pra ver. Só dessa vez.
Ninguém fez isso por má fé. A ferramenta é boa demais pra ser ignorada, e proibição por política interna só empurra o uso pra debaixo do pano. O problema é o que vai junto no Ctrl+V. Nome completo, CPF, endereço, diagnóstico. No momento em que você aperta Enter, esse texto deixa de ser seu: entra na infraestrutura de um terceiro, possivelmente em outra jurisdição, e vive em logs que você não controla nem audita.
E aqui está o ponto que muita gente entende tarde: o incidente não acontece quando a LLM responde algo indevido. Em setor regulado, o incidente pode ser o próprio envio. O dado transitou sem base legal? O problema já existe, antes de qualquer resposta voltar.
A boa notícia é que a camada que falta nesse fluxo existe, é open source e está madura. A notícia curiosa é como ela virou assunto.
O hype chegou oito anos atrasado
Nas últimas semanas o Microsoft Presidio viralizou como se fosse lançamento. "A Microsoft acabou de lançar uma ferramenta que protege seus dados da IA." Não lançou. O Presidio existe desde 2018: 47 releases, mais de 8 mil estrelas no GitHub, licença MIT. A versão que instalei enquanto escrevia este artigo é a 2.2.363, de junho de 2026.
Segunda ironia, mais recente: o projeto está deixando de ser da Microsoft. Está em transição oficial para uma organização comunitária, a Data Privacy Stack, com governança independente e imagens Docker no GitHub Container Registry. Viralizou como "produto da Microsoft" no momento exato em que a Microsoft o entrega à comunidade.
Leio os dois fatos a favor da ferramenta. Oito anos de produção, código aberto, sem dono comercial: é o perfil que você quer na camada por onde vão passar os dados mais sensíveis da empresa. E a viralização tardia diz menos sobre o Presidio e mais sobre a ansiedade do mercado em 2026: todo mundo colocou LLM no fluxo de trabalho antes de resolver o que entra nela.
O que o Presidio é, em linguagem de gente
O Presidio (do latim praesidium, proteção) é um kit de detecção e desidentificação de dados pessoais. São quatro módulos:
Analyzer é o detetive: recebe texto e devolve onde está cada dado pessoal, de que tipo e com que confiança (score de 0 a 1). Anonymizer é o executor: pega os achados e transforma, substituindo por placeholder, mascarando, removendo, aplicando hash ou cifrando. Image Redactor faz o mesmo em imagem, com OCR e tarja, incluindo DICOM, o formato padrão de imagem médica. Structured cuida de dado tabular, tipo DataFrame e JSON.
A detecção do Analyzer é híbrida, e isso importa. NER (reconhecimento de entidades nomeadas, modelo que identifica nomes, locais e organizações pelo contexto) cuida do que regex não pega. Regex cuida do que tem formato fixo, como e-mail e cartão. Checksum confirma dígito verificador: número com cara de cartão que não passa no Luhn não vira falso positivo. E palavras de contexto ajustam o score: 11 dígitos perto da palavra "CPF" valem mais que os mesmos 11 dígitos soltos.
Tudo roda onde você mandar: pip, Docker, Kubernetes, demo no navegador. O dado não sai de casa pra ser protegido. Esse detalhe carrega o argumento inteiro.
O que ele resolve de verdade: o padrão sanduíche
O caso de uso que interessa aqui é um só: o Presidio entre a sua aplicação e a LLM pública. Anonimiza antes de enviar, desanonimiza a resposta. O fluxo:
usuário escreve o prompt (com PII)
|
v
Presidio Analyzer ........ localiza cada PII: posição, tipo, score
|
v
Presidio Anonymizer ...... troca por placeholder ou cifra (encrypt/AES)
|
v
API da LLM ............... o modelo trabalha SEM ver o dado real
|
v
Presidio Deanonymize ..... decrypt: restaura os valores originais
|
v
resposta final pro usuário, com os dados reais de volta
A peça que fecha o ciclo é o operador encrypt, reversível. O Anonymizer cifra cada entidade com AES e o DeanonymizeEngine decripta na volta, com a mesma chave, dentro da sua infraestrutura. O provedor recebe um prompt funcional, com a frase intacta, mas nunca vê o nome do seu cliente. A chave nunca viaja.
O que o Presidio NÃO faz (leia antes de apostar a LGPD nele)
Essa seção é a mais importante do artigo. Quem pula ela compra guardrail achando que comprou compliance.
Ele não garante achar tudo. Não é opinião minha, está no README oficial, em tom de aviso: por usar detecção automatizada, não há garantia de que toda informação sensível será encontrada, e proteções adicionais devem ser empregadas. Recall de 100% não existe em detecção de PII. Planeje para o falso negativo, porque ele vai acontecer.
Ele entrega pseudonimização, não anonimização jurídica. Se você troca "Maria Souza" por um valor cifrado mas guarda a chave que reverte, o dado continua associável a uma pessoa. O GDPR diz isso com todas as letras no Recital 26: dado pseudonimizado segue sendo dado pessoal. A LGPD define pseudonimização no artigo 13 na mesma lógica. E no sanduíche a chave fica com você por design, senão não haveria volta. Chame do que é: pseudonimização que reduz risco, não anonimização que elimina o vínculo.
Ele não substitui avaliação de conformidade. Presidio é ferramenta, não parecer. Base legal, relatório de impacto, contrato com o operador: trabalho do seu DPO e do jurídico.
Falso positivo degrada a resposta da LLM. Se o Analyzer decide que o nome do seu produto é uma pessoa e o substitui, o modelo recebe um texto mutilado e resume errado. Proteção de privacidade tem custo de qualidade, e você precisa medir os dois lados.
Ele não fala português de fábrica. A lista oficial de entidades cobre quase vinte países, dos Estados Unidos à Coreia. Brasil não está nela. CPF, CNPJ, RG e CNS não existem como reconhecedores prontos. Você mesmo escreve, e é menos difícil do que parece. Olha aí embaixo.
Mão na massa: Python, CPF e dígito verificador
Instalação (Python 3.10 a 3.13):
pip install presidio-analyzer presidio-anonymizer
python -m spacy download en_core_web_lg
O fluxo básico, com o modelo default em inglês:
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()
texto = "Contact John Smith at john.smith@acme.com or +1 212 555 0199."
achados = analyzer.analyze(
text=texto,
entities=["PERSON", "EMAIL_ADDRESS", "PHONE_NUMBER"],
language="en",
)
print(anonymizer.anonymize(text=texto, analyzer_results=achados).text)
# Contact <PERSON> at <EMAIL_ADDRESS> or <PHONE_NUMBER>.
Repare que o exemplo está em inglês. Detectar nomes em português exige configurar um modelo spaCy ou transformer em pt, assunto pra outro dia. Mas reconhecedor de regex e checksum não depende de idioma. Então vamos criar o que o Presidio não traz: um reconhecedor de CPF que valida dígito verificador, pra não marcar qualquer sequência de 11 dígitos.
from presidio_analyzer import Pattern, PatternRecognizer
class CpfRecognizer(PatternRecognizer):
PATTERNS = [
Pattern("cpf_formatado", r"\b\d{3}\.\d{3}\.\d{3}-\d{2}\b", score=0.5),
Pattern("cpf_numerico", r"\b\d{11}\b", score=0.3),
]
CONTEXT = ["cpf", "documento", "cadastro"] # palavras próximas elevam o score
def __init__(self):
super().__init__(
supported_entity="BR_CPF",
patterns=self.PATTERNS,
context=self.CONTEXT,
supported_language="en", # casa com o language="en" da chamada
)
def validate_result(self, pattern_text: str):
# O dígito verificador separa um CPF de qualquer número de 11 dígitos.
# True crava o score em 1.0; False derruba pra 0 e descarta o achado.
digitos = [int(d) for d in pattern_text if d.isdigit()]
if len(digitos) != 11 or len(set(digitos)) == 1:
return False
for i in (9, 10):
soma = sum(d * p for d, p in zip(digitos[:i], range(i + 1, 1, -1)))
if (soma * 10) % 11 % 10 != digitos[i]:
return False
return True
analyzer.registry.add_recognizer(CpfRecognizer())
texto = "O cliente de CPF 529.982.247-25 reclamou. Protocolo 123.456.789-00."
achados = analyzer.analyze(text=texto, entities=["BR_CPF"], language="en")
print(anonymizer.anonymize(text=texto, analyzer_results=achados).text)
# O cliente de CPF <BR_CPF> reclamou. Protocolo 123.456.789-00.
Rodei esse código antes de publicar. O CPF válido (o clássico 529.982.247-25 dos exemplos de teste, gerado algoritmicamente) sai com score 1.0. O protocolo com cara de CPF, que não passa no dígito verificador, fica intocado. É o checksum trabalhando nas duas direções: menos vazamento e menos falso positivo.
Pra fechar o sanduíche, a versão reversível:
from presidio_anonymizer import DeanonymizeEngine
from presidio_anonymizer.entities import OperatorConfig
chave = "WmZq4t7w!z%C&F)J" # 128 bits; em produção venha de um cofre de segredos
blindado = anonymizer.anonymize(
text=texto,
analyzer_results=achados,
operators={"DEFAULT": OperatorConfig("encrypt", {"key": chave})},
)
# blindado.text vai pra LLM. A resposta volta com os valores cifrados intactos...
restaurado = DeanonymizeEngine().deanonymize(
text=blindado.text, # na prática: a resposta da LLM
entities=blindado.items,
operators={"DEFAULT": OperatorConfig("decrypt", {"key": chave})},
)
print(restaurado.text)
Aviso de quem já quebrou a cara: a desanonimização só funciona se o modelo devolver os valores cifrados intactos. Em resumo livre, ele às vezes parafraseia ou omite o placeholder. Funciona bem em extração, classificação e reescrita; teste antes de prometer round-trip perfeito.
O lado .NET: intercepte a chamada, não a aplicação
Presidio é Python, mas ninguém reescreve backend por causa disso. Os módulos viram serviços REST em containers:
docker run -d -p 5002:3000 ghcr.io/data-privacy-stack/presidio-analyzer:latest
docker run -d -p 5001:3000 ghcr.io/data-privacy-stack/presidio-anonymizer:latest
Do lado C#, o encaixe natural é um DelegatingHandler: intercepta a requisição do HttpClient que fala com a LLM, manda o prompt pro Presidio e só deixa seguir o texto limpo.
public sealed class PresidioPiiHandler : DelegatingHandler
{
private readonly HttpClient _presidio;
public PresidioPiiHandler(IHttpClientFactory factory)
=> _presidio = factory.CreateClient("presidio");
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken ct)
{
if (request.Content is null)
return await base.SendAsync(request, ct);
var payload = JsonNode.Parse(await request.Content.ReadAsStringAsync(ct))!;
// Formato chat/completions: sanitiza o conteúdo das mensagens do usuário
foreach (var msg in payload["messages"]!.AsArray())
{
if ((string?)msg!["role"] != "user") continue;
msg["content"] = await SanitizeAsync((string)msg["content"]!, ct);
}
request.Content = new StringContent(
payload.ToJsonString(), Encoding.UTF8, "application/json");
return await base.SendAsync(request, ct);
}
private async Task<string> SanitizeAsync(string text, CancellationToken ct)
{
// 1) /analyze: onde estão as PII?
var analyze = await _presidio.PostAsJsonAsync(
"http://localhost:5002/analyze", new { text, language = "en" }, ct);
var achados = await analyze.Content.ReadFromJsonAsync<JsonArray>(ct);
// 2) /anonymize: sem operadores explícitos, o default é replace
var anonymize = await _presidio.PostAsJsonAsync(
"http://localhost:5001/anonymize",
new { text, analyzer_results = achados }, ct);
var resultado = await anonymize.Content.ReadFromJsonAsync<JsonNode>(ct);
return (string)resultado!["text"]!;
}
}
Registro no Program.cs:
builder.Services.AddHttpClient("presidio");
builder.Services.AddTransient<PresidioPiiHandler>();
builder.Services.AddHttpClient("llm", c => c.BaseAddress = new Uri("https://api.seu-provedor.com/"))
.AddHttpMessageHandler<PresidioPiiHandler>();
Pra versão reversível, o mesmo princípio no caminho de volta: /deanonymize com o operador decrypt sobre a resposta, antes de devolver ao chamador.
A jogada que interessa ao decisor: não precisa colocar esse handler em cada aplicação. Se as chamadas de LLM da empresa passam por um gateway como o Azure API Management, a mesma lógica vira policy de entrada e saída, e toda aplicação fica coberta sem mudar uma linha de código. A Microsoft publicou o padrão com o nome de PII Shield: o APIM intercepta, o Presidio cifra na ida e decripta na volta, a chave mora no Key Vault. Guardrail organizacional, não boa vontade de cada time.
Expectativa vs realidade: quem não mede, não sabe o que vaza
Instalar é a parte fácil. O trabalho de verdade é calibrar.
Todo achado vem com score, e você decide o corte. Threshold baixo demais mutila texto inocente e a LLM responde pior. Alto demais deixa PII passar. Não existe número mágico: existe o seu domínio, seus textos, seu apetite de risco.
A prática que separa projeto sério de demo é o golden file: textos reais do seu negócio, anotados à mão, marcando cada PII que deveria ser detectada. Rode o Presidio contra esse conjunto e meça recall (quanto do que existia foi achado) e precisão (quanto do achado existia mesmo) antes de produção, e de novo a cada ajuste. Sem isso você não sabe o que vaza, só descobre quando doer. E o risco não é hipótese acadêmica: pesquisadores extraíram dados de treinamento de modelos em produção, e a OWASP mantém a divulgação de informação sensível no topo dos riscos de aplicações com LLM, incluindo vazamento via RAG.
O caso extremo: saúde
Se o tema aperta em qualquer empresa, em saúde ele esmaga. Pense num hospital ou healthtech querendo usar LLM pública pra resumir evolução ou triar mensagens de pacientes. Dois fatos técnicos mudam a conversa.
Primeiro: um recurso FHIR Patient carrega PII por definição, no próprio schema. Nome, documento, data de nascimento, endereço e telefone são elementos do recurso. Não existe "Patient sem dado pessoal": o dado pessoal é a estrutura. Qualquer integração que serializa um Patient pra fora já decidiu, talvez sem perceber, exportar PII.
Segundo: a LGPD classifica dado referente à saúde como dado sensível (artigo 5º, inciso II), com requisitos de tratamento mais duros que os do dado pessoal comum. Junte os dois e a conclusão é desconfortável: o trânsito de uma evolução de paciente pela infraestrutura de um provedor de LLM, sem base legal adequada, já pode configurar violação. Antes do resumo voltar.
Nesse cenário o Presidio não é luxo de arquiteto, é pré-requisito de conversa. E é onde a limitação dos reconhecedores pesa: nome, endereço e documento brasileiros, nada vem pronto. No próximo artigo da série eu construo a peça que falta: reconhecedores brasileiros (CPF, CNPJ, RG, CNS) e anonimização aplicada a dados clínicos.
O ganho real, sem hype
O Presidio não te dá segurança total. Nada dá, e desconfie de quem prometer. O que ele dá é redução drástica e mensurável da superfície de exposição: camada auditável (cada detecção vira log seu, não do provedor), self-hosted (o dado é protegido dentro de casa), MIT e agora governança comunitária, ou seja, sem lock-in na peça mais crítica do fluxo.
Sem a camada, sua política de privacidade depende do bom senso de cada pessoa com um Ctrl+V. Com ela, depende de um pipeline que você versiona, testa e melhora. Continua imperfeito. Passa a ser imperfeito sob controle.
Passando a régua
Sua empresa vai usar LLM pública. Provavelmente já usa, com ou sem bênção oficial. A pergunta útil não é "como proíbo", é "o que coloco entre o prompt e o provedor". O Presidio responde bem: maduro desde 2018, MIT, sanduíche com criptografia reversível, e um aviso honesto no README de que não pega tudo, mais do que a maioria dos vendedores de bala de prata oferece.
Comece pequeno: dois containers, um golden file com 50 textos do seu negócio, um reconhecedor de CPF. Meça, ajuste, e só então prometa algo pro compliance.
E pare de colar CPF no ChatGPT.
Referências: repositório do Presidio · documentação oficial · transição de governança · entidades suportadas · docs do Anonymizer · PII Shield no APIM · extração de dados de treino (Carlini et al.) · OWASP LLM02:2025 · FHIR Patient · LGPD
Faça login para comentar.