mozak.tech Engenharia de IA Corporativa 77%

Parte III — Governança, Conformidade e Responsabilidade na IA

3.5 — Proteção de Dados Sensíveis: Detecção e Mascaramento de PII (PII & Data Security)

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: 2

Avaliando 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á-los

Exemplos 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 PII

Padrã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ário

Armadilhas

⚠️ 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 algorithm
⚗ Laboratório prático — mozak.tech
Se 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.