Seu prompt sabe demais: blindando dados sensíveis com Microsoft Presidio antes que eles cheguem à LLM

28 de julho de 2026 13 min de leitura
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

0 comentários

Faça login para comentar.