Módulo Audit — visão geral
O módulo de auditoria (@mobilemed/audit) grava e consulta a trilha de eventos do Portal 2.0:
quem fez o quê, quando, sobre qual recurso e a partir de qual IP/aplicação. Diferente de um log de
depuração, ele existe para responder, meses depois, "o que aconteceu com este registro" — para
suporte, investigação de incidente e conformidade (LGPD, HIPAA, ANVISA).
O módulo não tem rota própria de gravação. Qualquer domínio do sistema (exames, usuários, tenants/unidades, papéis, integrações, etc.) publica um evento assíncrono descrevendo a mudança; o Audit consome esse evento, encadeia a entrada por hash em uma cadeia apensável (append-only) e persiste em um banco Postgres próprio. A consulta é feita por uma API REST dedicada, com mascaramento de campos sensíveis aplicado na leitura, e o frontend acessa essa API somente através de um resolver GraphQL no BFF — nunca diretamente.
Nota sobre o sistema legado. Os cards de negócio que descrevem "auditoria" no legado (
mm-pacs-portal-api/mm-pacs-public-api) tratam de coleções MongoDB separadas por domínio (exameHistorico,EmpresaHistorico,UsuarioLog,GrupoUsuarioLog), sem cadeia de hash, com PHI/PII gravado e devolvido em claro, e com pelo menos um endpoint de consulta sem autenticação. Este módulo substitui essa arquitetura por completo por uma trilha única e genérica, independente de domínio. Não presuma que qualquer comportamento específico do legado (dicionário fixo de eventos, contagem de chamadas, etc.) ainda se aplica — o que este módulo garante hoje é descrito abaixo e nas páginas de cada capacidade, a partir do código atual.
Onde o serviço roda
O Audit é um microsserviço próprio (app/audit), separado da API principal do Portal 2.0
(app/main, porta 3000). Em desenvolvimento local ele sobe na porta 3001
(AUDIT_API_PORT, default 3001) e expõe:
- API REST de consulta em
http://localhost:3001/v1/...(versionamento por URI,VersioningType.URI); - Swagger em
http://localhost:3001/docs; - um consumidor RabbitMQ (mesmo processo, via
app.connectMicroservice) que ouve a fila de eventos de auditoria — não é uma rota HTTP.
O BFF GraphQL (consumido pelo frontend) roda dentro da API principal e chama esta API REST
internamente através de AuditApiClient, usando a URL configurada em bff.auditApiUrl.
Arquitetura
As três camadas do pacote
| Pasta | Responsabilidade |
|---|---|
record/ | Lado de gravação: consumidor de fila, mapeamento da mensagem, apensação na cadeia de hash. Não expõe HTTP. |
query/ | Lado de leitura: AuditLogController (REST), serviços de listagem e verificação de integridade, mascaramento de campos na leitura. |
shared/ | Domínio compartilhado pelos dois lados: AuditEntry/AuditMessage, ChainLink/ChainVerification, hash SHA-256, assinatura HMAC dos checkpoints, entidades TypeORM e enums (AuditResourceType, ChainBreach, MaskableField). |
Cadeia de hash (o que resolve uma lacuna real do legado)
Cada entrada persistida carrega um hash calculado sobre seu próprio conteúdo e o previousHash
da entrada anterior na mesma cadeia — uma cadeia por escopo (chainScope): o tenantId do
evento, ou __system__ quando não há tenant. Além disso, cada cadeia tem um checkpoint
(audit_chain_checkpoints): o hash da última entrada, a contagem de entradas e uma assinatura HMAC
sobre esses dois valores, para detectar até a substituição do próprio checkpoint. Ver
Verificar integridade para o que exatamente é detectado (elo quebrado
= possível fork/exclusão; hash que não bate, checkpoint ausente/forjado/divergente = adulteração).
Isso não existia no legado descrito nos cards (trilha em Mongo, sem hash-chain, com possibilidade de apagar uma entrada sem deixar rastro) — é uma melhoria confirmada no código atual, não uma suposição.
Mascaramento na leitura (o que resolve outra lacuna real do legado)
O conteúdo é gravado sem mascaramento (a cadeia de hash precisa do conteúdo original para se
verificar). O mascaramento acontece na leitura, aplicado pelo AuditLogMaskingService sobre uma
tabela de regras (audit_masking_rules, entidade × campo, cacheada em memória por
AUDIT_MASKING_CACHE_TTL_SECONDS, default 300s). Isso troca o dado real por um marcador
(***@***.***, ***.***.***-**, ***-***-**** ou ***, conforme o formato do valor) sem alterar o
que está persistido. Duas cargas de regra já vêm por migração:
- regra coringa (entidade
*):cpf,cns,rg,email,phone,telefone,celular,password,senha— aplicada a qualquer evento, emmetadata,before,after, descrição, IP do ator,userId,targetUserId,clientIderesourceIdquando o nome do campo bate; - regra da entidade
diagnosis(eventos com prefixodiagnosis.*):patientName,patientIdentity,patientBirthDate,patientEmail,accessionNumber,studyInstanceUid,patientCode,html,oit,mammography,echocardiogram— cobre justamente o conteúdo clínico do laudo (o "PHI em claro" apontado no legado).
O conjunto de regras é dado, não código: pode crescer sem alterar o serviço. Ver Consultar logs para como o mascaramento se aplica por rota.
Um evento genérico para qualquer domínio
Ao contrário do legado (uma coleção por domínio — exame, empresa, usuário, grupo), a gravação é
cross-domain: qualquer publicador envia a mesma forma de mensagem (AuditCreatedEvent, com uma
ou mais entries), e cada entrada carrega seu próprio resourceType (enum AuditResourceType:
exam, user, tenant, unit, role, policy, oauth_client, integration_instance, etc.) e
resourceId. A consulta filtra por esse par (resourceType + resourceId) para reconstruir o
histórico de um recurso específico — é o equivalente funcional a "trilha por exame" ou "trilha por
empresa" do legado, só que sobre uma única tabela. Ver
Registrar evento (fluxo assíncrono).
Regra de visibilidade entre rotas
As rotas tenants/:tenantId/audit-logs e units/:unitId/audit-logs nunca retornam entradas com
resourceType: user — mesmo que o chamador peça esse filtro explicitamente, a resposta vem vazia.
Ações sobre usuário só aparecem na rota geral (GET /audit-logs), dentro dos tenants a que o
chamador tem acesso. Isso é uma regra confirmada no código (AuditLogVisibilityScope.excludesUserResources),
não uma suposição de produto.
Páginas deste módulo
| Página | Cobre |
|---|---|
| Consultar logs | GET audit-logs, GET tenants/:tenantId/audit-logs, GET units/:unitId/audit-logs e o resolver GraphQL auditLogs do BFF |
| Verificar integridade | GET audit-logs/integrity — verificação da cadeia de hash e dos checkpoints assinados |
| Registrar evento | Fluxo assíncrono de gravação: fila → verificação de assinatura → apensação na cadeia → Postgres |
:::tip OpenAPI
A documentação interativa (schemas + "Try it out") está disponível em /docs no serviço de auditoria
(local: http://localhost:3001/docs).
:::