# Spec 14 — Auditoria e Evidências

> **Dono:** Core/Auditoria · **Status:** Vigente · **Plano:** 17
> **Relaciona:** ADR-035, ADR-017, ADR-019, ADR-024, ADR-029, ADR-033, ADR-034
> **Objetivo:** contrato da **trilha de auditoria** — evidência forense **imutável e íntegra**,
> com escopo por área, via única de escrita e enriquecimento automático. **Rigorosa no CMS,
> nenhuma no portal.**

## 1. Escopo por área (política)

Fonte: `config('audit.levels')` (`AuditPolicy`). Área desconhecida → **`none`** (negação por padrão).

| Área | Nível | O que registra |
|---|---|---|
| `cms` | `full` | toda mutação (automática) + leituras sensíveis + login/logout |
| `api` | `sensitive` | mutações e leituras classificadas |
| `portal` | `none` | **nenhuma** trilha individual (só log técnico/analytics agregado) |

- A área corrente vem do `Context('area')` (publicada por `EnforceSecurityHeaders`); console/queue = `cms`.
- A trilha é a **evidência forense** (ADR-019/029); o **log técnico** (ADR-024) é separado e efêmero.

## 2. Forma do registro

`core.core_audit_logs` (particionada por `RANGE(created_at)`):

| Coluna | Papel |
|---|---|
| `area` | `cms` \| `portal` \| `api` |
| `module` | token canônico (snake_case) do dono |
| `origin` | header `X-Audit-Origin` → nome da rota → `console` |
| `event` | `{module}.{action}` (vocabulário compartilhado ADR-024/033) |
| `action` | `created` \| `updated` \| `deleted` \| `view` \| evento de domínio |
| `entity_type` / `entity_id` | alvo (`target`) |
| `actor` | snapshot `{id,name,email}` — **jsonb objeto** (`CHECK jsonb_typeof = 'object'`) |
| `payload_before` / `payload_after` | estado anterior/atual (PII mascarada) |
| `changes` | diff só dos campos alterados |
| `request_id` / `trace_id` | correlação (ADR-024) |
| `metadata` | `ip`, `user_agent`, `url` |
| `integrity_hash` | **BLAKE3** (ADR-029) |
| `created_at` | `timestamptz` (Clock injetado) |

## 3. Via única de escrita

`Core\Contracts\AuditEntry` (DTO `readonly`) + `Core\Contracts\AuditWriter::record(AuditEntry)`:

```php
AuditEntry::model($module, $event, $model);                 // mutação de modelo
AuditEntry::event($module, $event, $targetType, $targetId, $before, $after); // evento de domínio
AuditEntry::access($module, $event, $targetType, $targetId); // leitura sensível
```

- `log(string $action, Model $model)` é **atalho** de compatibilidade (delega a `record`).
- `AuditService` é o único implementador; módulos dependem apenas da **porta**.
- Módulos **nunca** escrevem na tabela; usam a porta.

## 4. Enriquecimento automático

`GlobalAuditSubscriber` intercepta `eloquent.{created,updated,deleted}` e delega; o `AuditService`
resolve, sozinho: `actor` (snapshot), `user_id`, `module` (`ModuleToken`), `area`, `origin`,
`request_id`/`trace_id` (`Context`), `changes` (diff), **masking** (`SENSITIVE_FIELDS`) e `integrity_hash`.

→ **Toda** mutação de modelo sai completa, em qualquer área, sem o autor lembrar.

## 5. Acesso (leitura)

`Core\Internal\Audit\Http\Middleware\AuditAccess` (alias **`audit.access`**), aplicado **só ao CMS**
(`AreaRegistrar`), após `support.group`/`module.activation`. Casa o nome da rota contra
`config('audit.sensitive')` (`*export*`, `*download*`, `*dsar*`, `*lgpd*`, `*audit*`) e grava
`AuditEntry::access()`. O **portal nunca** gera trilha.

## 6. Eventos explícitos

- **Login/logout/falha:** `AuditAuthSubscriber` (eventos nativos) → `core.login`/`core.logout`/`core.login_failed`; **nunca** credenciais.
- **Perfil/permissão:** automático (models `Role`/`User`).
- **Qualquer domínio:** `app(AuditWriter::class)->record(AuditEntry::event(...))` — a via única.

## 7. Imutabilidade e integridade

- **Trigger PL/pgSQL** `trg_immutable_audit` bloqueia `UPDATE`/`DELETE` (mesmo privilegiado).
- **`integrity_hash` BLAKE3** determinístico (chaves ordenadas) sobre a evidência; detecta adulteração em baixo nível.
- `AuditLog::save()` impede update via app (defesa em profundidade).

## 8. Retenção e `legal hold` (ADR-019)

- `config('audit.retention.days')` (default **730**).
- `ExpungeAuditLogs` (command `audit:expunge`, `--dry-run`): partição cujo fim < `now()-retenção` é
  **dropada**, exceto se coberta por `legal hold` ativo (`core.core_audit_legal_holds`).
- Agendado **mensalmente** (`routes/console.php`); expurgo no nível de partição (a trilha é imutável por trigger).

## 9. Mapa de código

| Artefato | Caminho |
|---|---|
| Portas | `src/core/Contracts/{AuditEntry,AuditWriter}.php` |
| Serviço | `src/core/Internal/Audit/Services/AuditService.php` |
| Política | `src/core/Internal/Audit/AuditPolicy.php` |
| Token do módulo | `src/core/Internal/Audit/Support/ModuleToken.php` |
| Listeners | `src/core/Internal/Audit/Listeners/{GlobalAuditSubscriber,AuditAuthSubscriber}.php` |
| Acesso | `src/core/Internal/Audit/Http/Middleware/AuditAccess.php` |
| Retenção | `src/core/Internal/Audit/Actions/ExpungeAuditLogs.php` · `.../Console/ExpungeAuditLogsCommand.php` |
| Model | `src/core/Internal/Audit/Models/AuditLog.php` |
| Config | `config/audit.php` |
| Migrações | `src/core/Internal/Audit/Database/Migrations/*` |

## 10. Verificação

- `AuditTrailTest` (shape, enriquecimento, diff, portal sem trilha, imutabilidade, hash) ·
  `AuditEntryTest` · `AuditAccessTest` · `AuditAuthTest` · `AuditPolicyTest` ·
  `AuditRetentionTest` · `ModuleTokenTest` · `MigrationIdempotencyTest`.
- `make check` **2×** verde; `make exec cmd='php tools/migration-check'` verde.
