PCI-DSS Req 10.2 mandates logging of all access to payment-related system components, including who performed actions and when. SOC 2 CC7.2 requires detection and logging of security-relevant activities. CWE-778 covers the failure directly: sensitive operations performed without an audit trail leave you unable to answer the most common post-incident questions — "when did this role change happen?", "who approved this data export?", "what did this admin do?" Application logs are insufficient because they are ephemeral, mutable, and not designed for forensic analysis. OWASP A09 identifies missing audit trails as a primary monitoring failure. A breach investigation without an audit log typically takes 3–5x longer and often cannot establish the timeline of events at all.
Critical because sensitive operations without an audit trail cannot be forensically reconstructed after a security incident, and the absence violates PCI-DSS Req 10.2 and SOC 2 CC7.2 directly.
Create a persistent audit_events table and call an insert function from every handler that performs a sensitive operation.
Minimum schema:
CREATE TABLE audit_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
actor_id UUID REFERENCES users(id),
action TEXT NOT NULL, -- e.g. 'user.role.changed'
resource TEXT NOT NULL, -- e.g. 'user', 'subscription'
resource_id TEXT NOT NULL,
metadata JSONB, -- old/new values, IP address, etc.
ip_address INET
);
Call it from sensitive handlers:
await createAuditEvent({
actorId: session.user.id,
action: 'subscription.cancelled',
resource: 'subscription',
resourceId: subscriptionId,
metadata: { reason: body.reason }
})
Target handlers: role changes, account deletion, subscription changes, payment processing, data exports, and all admin API calls. Application logs alone — even structured ones — do not satisfy this requirement.
ID: saas-logging.audit-trail.audit-log-sensitive-ops
Severity: critical
What to look for: Enumerate all relevant files and Search for audit logging of sensitive operations. Sensitive operations include: user role changes, admin actions, account deletion, subscription changes or cancellation, payment processing, password changes, OAuth app authorizations, data exports, and admin API calls. Look for a dedicated audit log table in database schema (prisma/schema.prisma, drizzle schema, SQL migrations), a function/service called createAuditLog, recordAuditEvent, logAuditEvent, or similar, and calls to this function in the relevant route handlers. Also look for Supabase audit log patterns or third-party audit log services.
Pass criteria: No more than 0 violations are acceptable. An audit logging mechanism exists (database table, external service, or append-only log) that records sensitive operations with at minimum: who performed the action (user ID), what action was taken (action type), when it happened (timestamp), and what was affected (resource type and ID). The mechanism is called from the relevant operation handlers.
Fail criteria: No audit logging mechanism found. Sensitive operations are performed without any persistent record beyond transactional database changes. Application logs alone (which are ephemeral and mutable) do not count as an audit trail.
Skip (N/A) when: Application has no authenticated users, no admin functionality, and no payment processing — i.e., it is a fully public read-only application with no state-changing operations beyond purely cosmetic preferences.
Detail on fail: List which sensitive operations exist with no audit log. Example: "User role changes in /api/admin/users/[id]/role and subscription cancellation in /api/billing/cancel have no audit log — no audit_events table or equivalent found in schema"
Remediation: An audit trail is the paper trail that tells you who did what and when. Without it, you cannot investigate security incidents, respond to "did someone change my account?" support tickets, or demonstrate compliance with SOC 2, GDPR, or HIPAA.
At minimum, create an audit_events table:
CREATE TABLE audit_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
actor_id UUID REFERENCES users(id),
action TEXT NOT NULL, -- 'user.role.changed', 'subscription.cancelled', etc.
resource TEXT NOT NULL, -- 'user', 'subscription', 'payment'
resource_id TEXT NOT NULL,
metadata JSONB, -- additional context (old value, new value)
ip_address INET
);
Then create a utility function and call it from your sensitive handlers:
await createAuditEvent({
actorId: session.user.id,
action: 'user.role.changed',
resource: 'user',
resourceId: targetUserId,
metadata: { from: oldRole, to: newRole }
})
For a deeper audit trail implementation, the SaaS Readiness Pack covers audit log architecture and retention requirements.