Auditoria PII em Logs — LGPD (2026-05-08)
Status: ✅ FECHADO (2026-05-08) — Etapas 0/1/2/3/4/5/6 concluídas. Lint de CI (scripts/audit/pii-helpers-coverage.ts) ativo no job lint-and-build. Sentinelas Deno verdes (36/36). Memória mem://security/pii-log-masking-standard persistida. Backlog docs/index.md removido. Plano-fonte: /lgpd/PII_LOG_MASKING_PLAN.
Histórico de revisões do plano
| Data | Mudança | Motivo resumido |
|---|---|---|
| 2026-05-08 | Etapa 3 (alertas ntfy): especificação trocada de usuario_id para ${papel} @ ${escola_nome} (com fallback ${papel} @ escola:${escola_id_8c}…). | UUID é opaco para triagem operacional; papel + escola_nome identifica conta e contexto sem expor PF (PJ não é PII). Enquanto ntfy rodar em terceiro (ntfy.sh), nenhum dado de PF deve sair do domínio. |
| 2026-05-08 | Replanejamento formal: cada etapa expandida para formato autossuficiente (Status / Pré-req / Reversibilidade / Objetivo / Arquivos / Testes / Aceite / Docs / Volume / Notas). | Permitir execução ou replanejamento por etapa, em qualquer interação futura, sem depender de contexto anterior. |
Escopo: registros de log que contêm CPF, telefone, e-mail, código (CPF/INEP/CNPJ) e JID WhatsApp em texto plano. Superfícies cobertas: Edge Functions (supabase/functions/), Frontend (src/), Cloudflare Worker (workers/docs-auth), schema (logs_transacoes, RLS), alertas push (ntfy), pipeline de e-mail (email_outbox), exportações. Restrições: nenhuma funcionalidade de listagem/edição/gestão será alterada; o audit trail (logs_transacoes) deve continuar permitindo rastreabilidade operacional e LGPD.
1. Resumo executivo
Foram identificados três grupos de exposição de PII:
registrarLog()→logs_transacoes.detalhes(JSONB persistido): 8 sites com PII em claro noresumo/detalhese 3 sites com vazamento viagerarAlteracoes()(camposemail,telefone,codigo/CPF,telefone_secundariofaltando noexclude).console.*em Edge Functions (Supabase Edge Logs, retidos por dias): 7 sites — destaque parasend-otpque loga telefone E.164 cru a cada OTP (alto volume).enviarAlertaPush()parantfy.sh(terceiro, fora do nosso domínio): 4 sites adicionais (não cobertos pelo scan inicial) que enviamnome_completodo usuário em mensagens de alerta de incidente —verify-password,verify-otp,send-otp(fallback),auth-helpers(token revogado reusado). Risco análogo aconsole.*, mas com agravante de saída para terceiro.
Não foram encontrados vazamentos:
- No frontend (
src/) — nenhumconsole.*com CPF, telefone, e-mail ou código. - No Worker
docs-auth— só registracookie_names(não valores),user-agenttruncado e metadados de host. Sem PII. - Nos webhooks Wasender/Resend — payloads incoming não persistem corpo.
Achado de schema: logs_transacoes tem RLS por linha (admin OLP vê tudo; diretor/gestor vêem logs de usuários da própria escola), sem column-level security sobre o JSONB detalhes. Qualquer campo de PII gravado lá é legível por qualquer admin OLP e por diretor/gestor da mesma escola — agravando a importância de mascarar na escrita.
2. Achados (consolidado: scan anterior + extensão)
Legenda: R Remover · M Mascarar · K Manter (justificado) · D Revisar com DPO.
2.1 registrarLog() — detalhes JSONB persistido em logs_transacoes
| # | Arquivo:linha | Ação | Dado | Justificativa operacional | Classe |
|---|---|---|---|---|---|
| 1 | _shared/gestao-usuarios-helpers.ts:187 | usuario.create | resumo.email | Identificar qual usuário foi criado | ✅ CORRIGIDO (Etapa 5, 2026-05-08) — email_dominio+email_hash+codigo_mascarado+telefone_mascarado. |
| 2 | cadastro-escola-publica/index.ts:231 | escola.trial_signup | email + cnpj | Investigar fraude de cadastro | ✅ CORRIGIDO (Etapa 5, 2026-05-08) — email mascarado; CNPJ mantido (PJ não é PII). |
| 3 | admin-usuarios/index.ts:564 | usuario.hard_delete | email + codigo (CPF/INEP/CNPJ) | Audit trail: prova de "quem foi deletado" | ✅ CORRIGIDO (Etapa 5, 2026-05-08) — codigo_mascarado via maskCodigo + email_dominio+email_hash. |
| 4 | admin-usuarios-escola/index.ts:439 | idem | idem | idem | ✅ CORRIGIDO (Etapa 5, 2026-05-08). |
| 5 | gestao-usuarios-escola/index.ts:1068 | idem | idem | idem | ✅ CORRIGIDO (Etapa 5, 2026-05-08) — em usuarioExcluido. |
| 6 | email-unsubscribe/index.ts:89 | email.unsubscribe | email | Saber qual destinatário descadastrou | ✅ CORRIGIDO (Etapa 5, 2026-05-08) — email_dominio+email_hash. |
| 7 | process-email-outbox/index.ts:86 | email.bloqueado | to_email | Diagnosticar por que e-mail foi bloqueado | ✅ CORRIGIDO (Etapa 5, 2026-05-08) — to_email_dominio+to_email_hash. |
| 8 | gestao-responsaveis/index.ts:382 | responsavel.create | telefone cru | Suporte por telefone após criação | ✅ CORRIGIDO (Etapa 5, 2026-05-08) — telefone_mascarado + codigo_mascarado (substituiu cpf.substring(...) ad-hoc). |
2.2 gerarAlteracoes() — diff vazando PII (insert no mesmo JSONB)
| # | Arquivo:linha | Campos vazados | Classe |
|---|---|---|---|
| 9 | user-profile/index.ts:188 | email, telefone (exclude vazio) | ✅ CORRIGIDO (Etapa 4, 2026-05-08) — gerarAlteracoes(..., [...PII_CONTATO_FIELDS]) + evento perfil.contato_alterado com campos_alterados (sem valores). |
| 10 | gestao-usuarios-escola/index.ts:834 | email, codigo, telefone | ✅ CORRIGIDO (Etapa 4, 2026-05-08) — exclude estendido + evento usuario.contato_alterado. |
| 11 | gestao-responsaveis/index.ts:499 | email, telefone, telefone_secundario (diff manual) | ✅ CORRIGIDO (Etapa 4, 2026-05-08) — diff manual substituído por gerarAlteracoes + PII_CONTATO_FIELDS no exclude + evento responsavel.contato_alterado. |
2.3 console.* — Supabase Edge Logs
| # | Arquivo:linha | Dado | Volume | Classe |
|---|---|---|---|---|
| 12 | gestao-responsaveis/index.ts:319,467 | CPF + nome | baixo (criação) | ✅ CORRIGIDO (Etapa 2, 2026-05-08) — maskCPF + nome→id |
| 13 | send-otp/index.ts:460,461,466 | telefone E.164 cru | alto (toda solicitação OTP) | ✅ CORRIGIDO (Etapa 1, 2026-05-08) — 4 console.log de debug removidos |
| 14 | _shared/portal-login-responsavel.ts:121 | telefone cru (formato inválido) | baixo | ✅ CORRIGIDO (Etapa 2, 2026-05-08) — telefone removido, mantido só comprimento |
| 15 | _shared/portal-login-aluno.ts:334 | telefone cru | baixo | ✅ CORRIGIDO (Etapa 2, 2026-05-08) — idem |
| 16 | gestao-usuarios-escola/index.ts:968 | email + nome (HARD_DELETE) | baixo | ✅ CORRIGIDO (Etapa 2, 2026-05-08) — console.log redundante removido |
| 17 | _shared/wasender-whatsapp.ts:102,211 | errorBody (pode conter JID) | baixo | ✅ CORRIGIDO (Etapa 2, 2026-05-08) — maskTextoComTelefones aplicado |
2.4 NOVO — enviarAlertaPush() (ntfy.sh, fora do domínio)
| # | Arquivo:linha | Dado | Classe |
|---|---|---|---|
| 18 | verify-password/index.ts:326 | nome_completo no corpo do alerta | ✅ CORRIGIDO (Etapa 3, 2026-05-08) — formatAlertSubject({ papel: tipoCodigo, escola_id }) |
| 19 | verify-otp/index.ts:244 | nome_completo + IP | ✅ CORRIGIDO (Etapa 3, 2026-05-08) — formatAlertSubject({ papel: tipoCodigo, escola_id }) + IP mantido |
| 20 | send-otp/index.ts:500 | nome_completo (alerta de falha SMS) | ✅ CORRIGIDO (Etapa 3, 2026-05-08) — formatAlertSubject({ papel: tipoCodigo, escola_id }) |
| 21 | _shared/auth-helpers.ts:121 | nome_completo (token revogado reusado) | ✅ CORRIGIDO (Etapa 3, 2026-05-08) — formatAlertSubject({ papel: payload.principal_role, escola_id: payload.escola_id }) + jti curto |
| 22 | cadastro-escola-publica/index.ts:744 | nome da escola + cidade (PJ) | K — dado de PJ para alerta operacional, não é PII de pessoa física |
| 23 | _shared/jwt-portal.ts:122 (ALERTA token portal) | só jti truncado, sem PII | K já OK |
2.5 Schema — logs_transacoes
- RLS atual (3 policies SELECT):
logs_admin_select:principal_role = 'administrador'→ admin OLP vê tudo (incluindo PII emdetalhes).logs_diretor_select_own_school: diretor vê logs de usuários da sua escola.logs_gestor_select_own_school: gestor escola idem.
- Sem column-level security sobre o JSONB
detalhes. Mascarar na escrita é a única defesa eficaz. - Classe: K — manter o modelo de RLS por linha; o investimento vai em mascaramento de origem.
2.6 E-mails transacionais e exportações
process-email-outboxlogato_emailememail.bloqueado(item 7) — coberto.- Exportações (
*-export-*, geração de planilhas) — varredura não encontrou função que registre o conteúdo do arquivo gerado em log. Apenasinscricao.generate_filecominscricao_id. Sem achado novo.
3. Notas operacionais
- Princípio aplicado: mascarar na escrita (origem), não na leitura. Único caminho que protege simultaneamente
logs_transacoes, Edge Logs e ntfy. - Padrão de mascaramento (a ser implementado em
_shared/pii-helpers.ts):maskEmail("a@b.com")→"a***@b.com"+email_hash(SHA-256 hex truncado em 16) +email_dominio.maskCPF("12345678901")→"***.***.***-01".maskTelefone("+5511987654321")→"+55 (11) ****-4321".maskCodigo(codigo, tipo): CPF como acima; CNPJ →"**.***.***/****-99"; INEP → 4 últimos.
- Audit trail preservado: todo log mantém
usuario_id(FK),escola_id,acao,criado_em— rastreabilidade de "quem fez o quê" intacta.
4. Próximos passos
- Decisão final sobre achados marcados D (nenhum nesta auditoria — todos são R/M/K).
- Execução do plano em
/lgpd/PII_LOG_MASKING_PLAN, uma etapa por interação. - Releitura desta auditoria após cada etapa para virar status de cada linha.
Referências cruzadas: