Without structured audit logs, there is no forensic record of who accessed sensitive data, who changed permissions, or when an account was compromised — making incident response guesswork and regulatory compliance impossible. NIST 800-53 rev5 AU-2 (Event Logging) defines the minimum event set; AU-3 (Content of Audit Records) mandates user ID, timestamp, event type, and outcome in every record; AU-12 (Audit Record Generation) requires the system to generate these records reliably. CMMC 2.0 AU.L2-3.3.1 and FedRAMP rev5 AU-2 enforce this for any system handling federal or controlled unclassified information. Missing audit trails also violate incident-reporting obligations under many state and federal data breach notification laws.
Medium because the absence of audit logging does not directly cause a breach but eliminates the ability to detect, contain, or legally demonstrate the scope of one after the fact.
Create a centralized logAuditEvent function and call it at every authentication, permission change, and destructive operation. Structure logs as JSON so they are machine-parseable by SIEM tools.
// lib/audit-logger.ts
import pino from 'pino'
const logger = pino({ level: 'info' })
export function logAuditEvent({
userId,
action,
resource,
outcome,
detail
}: {
userId: string
action: string
resource: string
outcome: 'success' | 'fail'
detail?: string
}) {
logger.info({
ts: new Date().toISOString(),
userId,
action,
resource,
outcome,
detail
})
}
Call logAuditEvent for at minimum: login/logout, failed login attempts, role changes, record deletions, and admin actions. Retain logs for at least 90 days per AU-11; ship them to a managed log service (Datadog, CloudWatch, Logtail) rather than local disk.
ID: gov-fisma-fedramp.audit-accountability.audit-logging
Severity: medium
What to look for: Search for logging configuration and implementation. List all logging libraries found (winston, pino, bunyan, etc.) and audit log tables/collections in database. Enumerate all critical event types and for each classify whether it is logged: user authentication, permission changes, sensitive data access, administrative actions. Verify logs include timestamp, user ID/email, action type, and outcome (success/fail). Count the number of event types logged out of total required event types.
Pass criteria: An audit logging system exists with at least 1 structured logging library. At least 4 critical event types (login, role changes, data deletion, admin actions) are logged with timestamp, user ID, action type, and outcome. Logs are retained for at least 90 days. Report the count of event types covered.
Fail criteria: No audit logging found, or logs are incomplete (missing user ID, timestamp, or action type), or fewer than 4 sensitive event types are logged.
Skip (N/A) when: The site serves only public read-only content with no user actions.
Detail on fail: Specify what's missing. Example: "No audit logging found in codebase. Logins, role changes, and data deletions are not logged." or "Basic logging exists but missing user ID and outcome status — logs show 'DELETE /api/users/123' without user context or success/failure."
Remediation: Implement audit logging for critical events:
// lib/audit-logger.ts
import winston from 'winston'
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
defaultMeta: { service: 'auditapp' },
transports: [
new winston.transports.File({ filename: 'audit.log' })
]
})
export async function logAuditEvent(
userId: string,
action: string,
resource: string,
outcome: 'success' | 'fail',
details?: string
) {
logger.info({
timestamp: new Date().toISOString(),
userId,
action,
resource,
outcome,
details
})
}
// app/api/users/[id]/route.ts
import { logAuditEvent } from '@/lib/audit-logger'
export const DELETE = async (req: Request, { params }: { params: { id: string } }) => {
const session = await getServerSession()
try {
await db.user.delete({ where: { id: params.id } })
await logAuditEvent(
session.user.id,
'DELETE_USER',
`users/${params.id}`,
'success'
)
return Response.json({ ok: true })
} catch (error) {
await logAuditEvent(
session.user.id,
'DELETE_USER',
`users/${params.id}`,
'fail',
error.message
)
return Response.json({ error: 'Failed to delete user' }, { status: 500 })
}
}