@devix-tecnologia/anonymizer-core
v0.1.0
Published
DSL e engine de anonimização determinística para bancos de dados
Readme

@devix-tecnologia/anonymizer-core
DSL e engine de anonimização determinística para PostgreSQL. Recebe um perfil declarativo — que tabelas entram, que colunas mascarar, como — e devolve o SQL que constrói uma cópia anonimizada dos dados.
O pacote é puro: não abre conexão, não executa nada, não lê arquivo. Ele
gera texto SQL. Quem executa é o consumidor — no caso da Devix, o
@devix-tecnologia/db-backup.
A ideia
A anonimização acontece dentro do banco, num schema temporário:
CREATE SCHEMA IF NOT EXISTS backup_tmp;
CREATE TABLE backup_tmp.usuario AS
SELECT CASE WHEN "email" IS NULL THEN NULL
ELSE ... END AS email,
...
FROM public.usuario;O dump sai desse schema, nunca do public. Nenhum dado sensível trafega para
fora do PostgreSQL para ser mascarado — o que também torna a operação viável
em bases grandes, já que tudo é feito em SQL sobre o conjunto inteiro.
Uso
import {
buildPlan,
defineProfile,
emailMask,
generateFullPipelineSql,
hash,
mask,
nullify,
} from '@devix-tecnologia/anonymizer-core';
const perfil = defineProfile({
version: '1',
tables: {
usuario: {
include: 'data',
anonymize: {
email: emailMask({ preserveLeft: 2 }),
cpf: hash('hmac-sha256', process.env.ANON_SECRET),
telefone: mask({ preserveRight: 4 }),
observacoes: nullify(),
},
},
// Estrutura sim, linhas não: a tabela existe vazia no dump.
auditoria: { include: 'schema' },
// Fora do dump.
sessao: { include: 'exclude' },
// Recorte por linha.
pedido: { include: 'data', filter: { where: "criado_em > '2026-01-01'" } },
},
});
const sql = generateFullPipelineSql(buildPlan(perfil), 'backup_tmp');defineProfile valida com zod e lança na hora — um perfil errado não chega a
virar SQL.
As três decisões de um perfil
include: o que entra
| Valor | Efeito |
| --- | --- |
| data | estrutura e linhas, com as transformações aplicadas |
| schema | só a estrutura; a tabela vem vazia |
| exclude | a tabela não entra no dump |
anonymize: como cada coluna é mascarada
| Transformação | O que faz | Preserva |
| --- | --- | --- |
| hash(algoritmo, segredo?) | sha256 nativo ou hmac-sha256 do pgcrypto | unicidade e junções |
| emailMask({ preserveLeft, hashLen }) | [email protected] → [email protected] | o domínio, e a forma de e-mail |
| mask({ preserveLeft, preserveRight, char }) | substitui o miolo por * | as pontas, e o comprimento |
| constant(valor) | o mesmo literal em toda linha | nada |
| nullify() | NULL | nada |
Toda transformação preserva NULL como NULL — uma coluna nula continua nula,
e não vira o hash da string vazia.
filter: que linhas entram
filter.where é interpolado literalmente na cláusula WHERE. É recurso de
operador, não entrada de usuário: quem monta o perfil controla o texto.
Determinismo, e o que ele custa
A mesma entrada produz sempre a mesma saída. Isso é o que permite anonimizar
tabelas separadamente e ainda assim juntá-las: se usuario.email e
pedido.email_contato recebem a mesma transformação, continuam casando depois
de mascarados.
O preço é conhecido e precisa ser dito: hash determinístico sem segredo é vulnerável a ataque de dicionário quando o domínio de valores é pequeno e adivinhável. CPF tem 10¹¹ possibilidades; e-mail de um domínio conhecido, menos ainda. Quem tem o dump anonimizado e a lista de candidatos recupera o original testando um por um.
A defesa é hash('hmac-sha256', segredo): sem o segredo, a lista de candidatos
não ajuda. Use-o para todo identificador de pessoa.
Duas ressalvas sobre o segredo:
- ele é interpolado no SQL gerado (com aspas escapadas). Se esse SQL for registrado em log, o segredo vai junto;
hmac()exige a extensãopgcryptono banco de origem. Sem ela, a execução falha — osha256nativo não tem essa dependência.
Onde a anonimização NÃO alcança
Mascarar coluna não anonimiza um conjunto de dados. Continuam identificáveis:
- combinações: CEP, data de nascimento e sexo bastam para singularizar boa parte de uma população, mesmo com nome e CPF mascarados;
- campos livres:
observacoescom um nome digitado dentro passa intacto se a coluna não for tratada — daínullify()ser o padrão sensato para texto livre; - linhas raras: um registro único numa coluna preservada continua único.
O pacote dá a ferramenta; decidir o que é suficiente para o caso é trabalho de quem escreve o perfil.
API
| Export | Papel |
| --- | --- |
| defineProfile | valida o perfil e o devolve tipado |
| hash, emailMask, mask, constant, nullify | construtores de transformação |
| isIncluded, isData | consultas sobre uma regra de tabela |
| buildPlan | perfil → plano, com a expressão SQL de cada coluna |
| generateFullPipelineSql | plano → SQL do pipeline inteiro |
| generateCreateSchemaSql, generateCreateTableSql, generateDropSchemaSql | os pedaços, para quem quer montar o próprio |
Testes
pnpm --filter @devix-tecnologia/anonymizer-core test23 testes, incluindo verificação baseada em propriedades (fast-check): para
qualquer perfil e qualquer nome de coluna, o SQL gerado é sintaticamente
válido. Nenhum toca banco.
