# Audit logs are tenant-scoped

- **Pattern:** `ab-002342` (`saas-multi-tenancy.tenant-management.audit-logs-tenant-scoped`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002342
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002342)

## Why it matters

Audit logs that are not scoped by tenant become a cross-tenant intelligence source: a tenant admin can query another organization's activity history, exposing who is active, what features they use, and what actions their users take. CWE-285 and NIST AU-9 both address this; SOC 2 CC7.2 requires that audit records be protected from unauthorized access. Beyond privacy, unscoped audit logs mean a legitimate tenant admin cannot be confident that the logs they see reflect only their organization's actions.

## Severity rationale

Low because exploiting unscoped audit logs requires tenant-admin privileges, limiting the attacker surface, but exposure of another tenant's activity trail is a direct SOC 2 CC7.2 control failure.

## Remediation

Add `organizationId` as a required field on every audit log write and as a mandatory filter on every read:

```ts
// Write — include tenant context on every event
await db.auditLog.create({
  data: {
    organizationId: session.user.organizationId,
    userId: session.user.id,
    action: 'document.deleted',
    resourceId: docId,
    createdAt: new Date()
  }
})

// Read — always scope to the requesting tenant
const logs = await db.auditLog.findMany({
  where: { organizationId: session.user.organizationId },
  orderBy: { createdAt: 'desc' },
  take: 100
})
```

If the audit log table was created without an `organizationId` column, add it via migration and backfill from the related resource's `organizationId` before removing the global-read path.

## Detection

- **ID:** `audit-logs-tenant-scoped`
- **Severity:** `low`
- **What to look for:** Examine audit log storage and retrieval — activity logs, event logs, admin action logs. Check whether audit log records include a tenant identifier and whether the audit log retrieval API scopes results to the requesting tenant. Look for patterns where audit logs are stored in a central table without tenant isolation, or where a tenant admin can query audit events from other tenants.
- **Pass criteria:** Count all audit log write locations. Audit log records include at least 1 tenant identifier. The audit log retrieval API filters by the requesting user's tenant. A tenant admin can see their own tenant's logs but not other tenants' logs.
- **Fail criteria:** Audit logs are stored without a tenant identifier. The audit log API returns results across all tenants without scoping to the requester's tenant.
- **Skip (N/A) when:** No audit logging system is detected. Signal: no audit_log or activity_log table in the schema, no event logging code for user/admin actions.
- **Detail on fail:** Example: `"GET /api/audit-logs in src/app/api/audit-logs/route.ts returns all audit log records with no organizationId filter. Any tenant admin can see activity from all tenants."`
- **Remediation:** Add tenant scoping to audit log queries:

  ```ts
  const logs = await db.auditLog.findMany({
    where: { organizationId: session.user.organizationId },
    orderBy: { createdAt: 'desc' },
    take: 100
  })
  ```

---

## External references

- cwe CWE-285
- nist AU-9
- soc2 CC7.2

Taxons: observability, access-control

HTML version: https://auditbuffet.com/patterns/ab-002342
