Objetivo da Aula
Ao concluir esta aula, você será capaz de:
Detectar PII em texto livre com regex: CPF, email, telefone, cartão de crédito, passaporte
Implementar mascaramento de dados antes de enviar ao LLM
Adicionar novos padrões PII ao pipeline existente
Avaliar a taxa de recall e precisão do detector de PII
Aplicar o conceito de pseudoanonimização sob a LGPD
Por que isso importa
Enviar CPFs, cartões de crédito e dados de saúde para APIs externas viola a LGPD e pode gerar multas de até 2% do faturamento da empresa (limite: R$50 milhões por infração). Em 2023, um vazamento de PII via LLM custou US$1.2M a uma empresa de healthcare americana. O mascaramento antes de envio é a defesa mais simples e efetiva.
Conceitos Fundamentais
PII_BASICOS: Os Padrões que Toda Empresa Precisa
PII_BASICOS = [
# CPF: 000.000.000-00 ou 00000000000
(r'\d{3}[\.-]?\d{3}[\.-]?\d{3}[-]?\d{2}', '[CPF]'),
# Email pessoal (não corporativo)
(r'[a-zA-Z0-9._%+-]+@(gmail|hotmail|yahoo|outlook)\.(com|br)', '[EMAIL]'),
# Telefone brasileiro: (11) 99999-9999, 11999999999
(r'(\(?\d{2}\)?\s?)?(\d{4,5})[-\s]?(\d{4})', '[TELEFONE]'),
# TODO 1 — cartão de crédito: 4 grupos de 4 dígitos separados por espaço ou hífen
(r'\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}', '[CARTAO]'),
# TODO 1 — passaporte brasileiro: 2 letras maiúsculas + 6 dígitos
(r'\b[A-Z]{2}\d{6}\b', '[PASSAPORTE]'),
]Por que \b no Passaporte
\b[A-Z]{2}\d{6}\b
\b = word boundary: garante que AA123456 não seja detectado no meio de uma string maior
como "SECRAA123456ario" (false positive)
Sem \b: "ABC123456DEF" → detectaria "BC123456" incorretamente
Com \b: captura apenas tokens isolados como "AA123456" ou "AB999999"Por que o Padrão do Cartão
\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}
[\s-]? = separador OPCIONAL: pode ser espaço, hífen, ou nada
Cobre:
- 1234567890123456 (sem separador)
- 1234 5678 9012 3456 (espaços)
- 1234-5678-9012-3456 (hífens)
- 1234 5678-9012 3456 (misto)
Risco: false positives com CEPs colados (improvável em PT-BR)Aprofundamento Técnico
Pipeline de Mascaramento
import re
def mascarar_pii(texto: str, padroes: list[tuple]) -> tuple[str, int]:
"""
Retorna texto mascarado e contagem de ocorrências.
Aplica padrões em ordem: mais específico primeiro.
"""
total = 0
for padrao, token in padroes:
matches = re.findall(padrao, texto)
total += len(matches)
texto = re.sub(padrao, token, texto)
return texto, total
# Teste:
texto = "Olá, meu CPF é 123.456.789-09 e cartão 1234 5678 9012 3456"
mascarado, n = mascarar_pii(texto, PII_BASICOS)
# mascarado: "Olá, meu CPF é [CPF] e cartão [CARTAO]"
# n: 2Avaliando Qualidade do Detector
def avaliar_detector(casos_teste: list[dict]) -> dict:
"""
casos_teste: [{"texto": "...", "esperado": "...", "deve_detectar": True/False}]
"""
verdadeiros_positivos = 0
falsos_positivos = 0
falsos_negativos = 0
for caso in casos_teste:
mascarado, n = mascarar_pii(caso["texto"], PII_BASICOS)
detectou = n > 0
if caso["deve_detectar"] and detectou:
verdadeiros_positivos += 1
elif not caso["deve_detectar"] and detectou:
falsos_positivos += 1 # detectou mas não deveria
elif caso["deve_detectar"] and not detectou:
falsos_negativos += 1 # não detectou mas deveria
precision = verdadeiros_positivos / max(verdadeiros_positivos + falsos_positivos, 1)
recall = verdadeiros_positivos / max(verdadeiros_positivos + falsos_negativos, 1)
return {"precision": precision, "recall": recall, "F1": 2*precision*recall/max(precision+recall, 0.001)}Decisão de Arquitetura: Pipeline PII antes do LLM
Antes de enviar qualquer dado ao LLM, você precisa de um estágio de detecção e tratamento de PII. Quatro decisões: (1) Detector por precision vs recall: alta precision preserva mais dados úteis (menos falsos positivos); alto recall garante maior proteção (menos PII vaza). Defina qual otimizar com base no risco regulatório do domínio. (2) Pseudoanonimização vs anonimização: pseudoanonimização substitui PII por token reversível — LGPD ainda considera dado pessoal se a chave de reversão existir. Anonimização é irreversível — Art. 5, III retira o dado do escopo da LGPD. (3) Dados indiretos: nome e CPF são óbvios; combinação de cargo + cidade + data de nascimento também identifica — o detector de regex não captura isso. (4) Logging do prompt: se o sistema loga prompts para debug, o log pode conter PII mesmo que a chamada ao LLM não contivesse — aplique o mesmo pipeline ao log.
Pseudoanonimização vs Anonimização
Mascaramento [CPF] = PSEUDOANONIMIZAÇÃO
- Token é reversível se você tem o mapeamento original
- LGPD: ainda é dado pessoal, mas com proteção reduzida
- Válido para processamento por terceiros (APIs externas)
Irreversível = ANONIMIZAÇÃO
- Remove qualquer possibilidade de re-identificação
- LGPD não se aplica (Art. 5, III)
- Exemplo: contar CPFs sem armazená-losExemplos Anotados
Exemplo 1: Mascaramento Antes de Envio ao LLM
# ERRADO: enviar dados brutos ao LLM
resposta = client.messages.create(
messages=[{"role": "user", "content": f"Analise: CPF 123.456.789-09 cartão 1234 5678 9012 3456"}]
)
# CORRETO: mascarar antes
texto_original = "Analise: CPF 123.456.789-09 cartão 1234 5678 9012 3456"
texto_mascarado, n_pii = mascarar_pii(texto_original, PII_BASICOS)
# texto_mascarado: "Analise: [CPF] [CARTAO]"
if n_pii > 0:
# Logar tentativa de envio de PII (compliance)
log_pii_detectada(n_pii, tipo="pre-llm")
resposta = client.messages.create(
messages=[{"role": "user", "content": texto_mascarado}]
)Exemplo 2: Passaporte no Texto
textos = [
"Meu passaporte é AA123456", # → "[PASSAPORTE]"
"Código produto: AA123456-ABC", # → não detecta (sem \b no final)
"Passaporte novo: ZZ999999", # → "[PASSAPORTE]"
"Número interno SECRAA123456ario", # → não detecta (\b protege)
]Padrões e Armadilhas
Padrões
Padrão 1: Testar o detector antes de ir para produção
# Casos de teste obrigatórios:
# True positives: CPF com e sem pontuação, cartão com diferentes separadores
# True negatives: CEP (8 dígitos), código de produto que parece PIIPadrão 2: Ordem importa — mais específico primeiro
# Detectar cartão antes de telefone (cartão tem 16 dígitos, telefone tem 8-9)
# Se telefone for detectado primeiro em um cartão, o cartão é parcialmente mascarado
padroes = [PII_CARTAO, PII_CPF, PII_TELEFONE] # não o contrárioArmadilhas
⚠️ Armadilha 1: Regex sem \b captura substrings indesejadas
# Sem \b no passaporte:
re.sub(r'[A-Z]{2}\d{6}', '[PASSAPORTE]', "ID: SECRAA123456Y")
# → "ID: SECR[PASSAPORTE]Y" ← ERRADO
# Com \b:
re.sub(r'\b[A-Z]{2}\d{6}\b', '[PASSAPORTE]', "ID: SECRAA123456Y")
# → "ID: SECRAA123456Y" ← correto, não detecta⚠️ Armadilha 2: Mascarar mas logar os dados originais
# Bug comum: logar o texto_original para debug → vaza PII no log
logger.debug(f"Processando: {texto_original}") # ERRADO
logger.debug(f"Processando: {texto_mascarado} [{n_pii} PII detectadas]") # correto⚠️ Armadilha 3: Padrão de cartão com false positive em IDs longos
# CEP + CNPJ grudados podem ter 16 dígitos
# Mitigação: verificar contexto (palavra "cartão" antes?) ou usar Luhn algorithmSe não for realizar o laboratório, pule para o próximo capítulo.
Ponte para o Lab
TODO 1 — Adicionar a PII_BASICOS: - Cartão de crédito: r'\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}' → '[CARTAO]' - Passaporte BR: r'\b[A-Z]{2}\d{6}\b' → '[PASSAPORTE]'
Ambas as linhas vão na lista PII_BASICOS no starter. A função mascarar_texto já usa PII_BASICOS — nenhuma outra mudança necessária.
Agora você está pronto para o lab.