Skip to Content
Deneva MCPComponentsAudit Log

Audit log

Source: src/security/audit-log.service.ts Table: audit_log (src/db/schema.ts)

Append-only security event trail. SOC 2 evidence. GDPR-aware (no PII).

Tamper-evidence

The mcp_app Postgres role has INSERT on audit_log only. UPDATE and DELETE are explicitly REVOKEd in src/db/roles.sql. An attacker who compromises the application cannot rewrite the trail from inside the app — they would need to escalate to the mcp_admin role (separate password, never used by the running service).

Event taxonomy

The event types are a closed union — adding one means updating both AuditEventType and any dashboard/log-shipping consumer. Phase 1 produces:

FamilyEvents
API key authapi_key.auth_success, api_key.auth_failure, api_key.created, api_key.rotated, api_key.revoked
OAuth (Phase 2+)oauth.flow_started, oauth.flow_completed, oauth.flow_failed, oauth.token_refreshed, oauth.token_revoked
MCPmcp.tool_called, mcp.tool_failed
Lifecycletenant.created, tenant.deleted
Throttlerate_limit.exceeded, auth.blocked_ip
Sync (Phase 4)sync.exhausted

PII handling

The metadata column is JSONB — flexible, but a footgun for GDPR if callers throw arbitrary context in. writeAuditEvent strips a closed list of known PII keys defensively:

email, name, firstName, lastName, phone, address, fullName

This is a defence in depth layer. The primary control is that callers should pass PII-free context in the first place. The strip is what catches the case where an adapter SDK echoes a user’s email into an error message.

Retention

WindowAction
0 – 12 monthsLive in audit_log.
12 months+Archived (not deleted) into a cold table by an Inngest cron (Phase 4). SOC 2 wants the trail preserved; GDPR data minimisation is satisfied because the trail carries no PII.

GDPR right-to-erasure on a tenant doesn’t delete that tenant’s audit rows — it sets tenant_id to NULL (anonymise) and strips identifying metadata fields. See deneva-mcp-tool-architecture.md §9.

Calling pattern

import { writeAuditEvent } from '../security/audit-log.service.js'; await writeAuditEvent('api_key.auth_failure', 'failure', { ip: req.ip, requestId: req.id, reason: 'invalid', });

Always use await for events on the failure path (so 4xx responses guarantee the row landed). For success paths and high-throughput events, fire-and-forget (void writeAuditEvent(...)) is acceptable — Phase 1 awaits everything for simplicity; Phase 5 may relax some hot-path writes.

Tests

tests/integration/audit.test.ts:

  • mcp_app cannot UPDATE audit_log
  • mcp_app cannot DELETE audit_log
  • mcp_app CAN INSERT
  • PII keys are stripped from metadata