.do
Enterprises

Audit Logs & Compliance

Enterprise-grade audit logging with WorkOS for compliance, security monitoring, and incident response supporting SOC 2, HIPAA, and GDPR requirements

Audit Logs & Compliance

Enterprise audit logging provides comprehensive tracking of all security-relevant events across your application, meeting compliance requirements for SOC 2, HIPAA, GDPR, and other regulatory frameworks.

Overview

Audit logs enable:

  • Compliance Requirements - Meet SOC 2, HIPAA, GDPR, ISO 27001 audit requirements
  • Security Monitoring - Detect suspicious activity and security incidents
  • Forensic Analysis - Investigate security breaches and data access
  • User Activity Tracking - Monitor who accessed what and when
  • Change History - Track all configuration and data modifications
  • Regulatory Reporting - Generate compliance reports for auditors

Event Types

Authentication Events

// User login
{
  type: 'auth.login.success',
  actor: {
    id: 'user_01ABC...',
    email: '[email protected]',
    organizationId: 'org_01XYZ...'
  },
  timestamp: '2025-01-20T14:30:00.000Z',
  metadata: {
    authMethod: 'sso',
    ipAddress: '203.0.113.45',
    userAgent: 'Mozilla/5.0...',
    location: 'San Francisco, CA, US'
  }
}

// Failed login attempt
{
  type: 'auth.login.failed',
  actor: {
    email: '[email protected]' // User may not exist
  },
  timestamp: '2025-01-20T14:28:00.000Z',
  metadata: {
    reason: 'invalid_credentials',
    ipAddress: '203.0.113.45',
    attemptCount: 3
  }
}

// Multi-factor authentication
{
  type: 'auth.mfa.verified',
  actor: { id: 'user_01ABC...', email: '[email protected]' },
  timestamp: '2025-01-20T14:30:05.000Z',
  metadata: {
    method: 'totp', // or 'sms', 'push'
    deviceId: 'device_01DEF...'
  }
}

Data Access Events

// Sensitive data accessed
{
  type: 'data.accessed',
  actor: { id: 'user_01ABC...', email: '[email protected]' },
  target: {
    type: 'customer_record',
    id: 'customer_01GHI...',
    classification: 'pii' // or 'phi', 'pci', 'confidential'
  },
  timestamp: '2025-01-20T15:00:00.000Z',
  metadata: {
    fields: ['ssn', 'creditCardNumber'],
    reason: 'customer_support_request',
    ticketId: 'TICKET-12345'
  }
}

// Data exported
{
  type: 'data.exported',
  actor: { id: 'user_01ABC...', email: '[email protected]' },
  target: { type: 'customer_list', count: 1500 },
  timestamp: '2025-01-20T16:00:00.000Z',
  metadata: {
    format: 'csv',
    filters: { segment: 'enterprise_customers' },
    recordCount: 1500
  }
}

Permission Changes

// Role assigned
{
  type: 'permission.role.assigned',
  actor: { id: 'user_admin_01...', email: '[email protected]' },
  target: { id: 'user_01ABC...', email: '[email protected]' },
  timestamp: '2025-01-20T10:00:00.000Z',
  metadata: {
    role: 'admin',
    previousRole: 'member',
    reason: 'promotion_to_team_lead'
  }
}

// Permission granted
{
  type: 'permission.granted',
  actor: { id: 'user_admin_01...', email: '[email protected]' },
  target: { id: 'user_01ABC...', email: '[email protected]' },
  timestamp: '2025-01-20T10:05:00.000Z',
  metadata: {
    permission: 'billing:manage',
    resource: 'organization_01XYZ...',
    expiresAt: '2025-02-20T10:05:00.000Z'
  }
}

Configuration Changes

// SSO configuration changed
{
  type: 'config.sso.updated',
  actor: { id: 'user_admin_01...', email: '[email protected]' },
  target: { type: 'sso_connection', id: 'conn_01ABC...' },
  timestamp: '2025-01-20T09:00:00.000Z',
  metadata: {
    changes: {
      domains: {
        before: ['acme.com'],
        after: ['acme.com', 'acme.co.uk']
      }
    }
  }
}

// Security policy updated
{
  type: 'config.security.updated',
  actor: { id: 'user_admin_01...', email: '[email protected]' },
  target: { type: 'organization', id: 'org_01XYZ...' },
  timestamp: '2025-01-20T09:30:00.000Z',
  metadata: {
    changes: {
      requireMFA: { before: false, after: true },
      sessionTimeout: { before: 86400, after: 3600 }
    }
  }
}

Implementing Audit Logging

Basic Logging

import { WorkOS } from '@workos-inc/node'

const workos = new WorkOS(process.env.WORKOS_API_KEY)

// Create audit log event
async function logEvent(event) {
  await workos.auditLogs.createEvent({
    organizationId: event.organizationId,
    event: {
      action: event.type,
      actor: {
        id: event.actor.id,
        name: event.actor.name,
        type: 'user'
      },
      targets: event.targets?.map(t => ({
        id: t.id,
        type: t.type,
        name: t.name
      })),
      context: {
        location: event.metadata?.location,
        userAgent: event.metadata?.userAgent,
        ipAddress: event.metadata?.ipAddress
      },
      metadata: event.metadata,
      occurredAt: event.timestamp || new Date().toISOString()
    }
  })
}

// Example: Log user login
on($.User.login, async ({ user, request }) => {
  await logEvent({
    type: 'user.login',
    organizationId: user.organizationId,
    actor: {
      id: user.id,
      name: `${user.firstName} ${user.lastName}`,
      email: user.email
    },
    timestamp: new Date().toISOString(),
    metadata: {
      authMethod: user.authMethod,
      ipAddress: request.headers['cf-connecting-ip'],
      userAgent: request.headers['user-agent'],
      location: request.cf?.city
    }
  })
})

Automatic Event Tracking

Track all database operations automatically:

// Intercept all database writes
on($.Database.before.write, async ({ operation, collection, data, user }) => {
  // Determine event type
  const eventType = {
    create: 'data.created',
    update: 'data.updated',
    delete: 'data.deleted'
  }[operation]

  // Log the event
  await logEvent({
    type: eventType,
    organizationId: user.organizationId,
    actor: {
      id: user.id,
      name: user.name,
      email: user.email
    },
    targets: [{
      id: data.id,
      type: collection,
      name: data.name || data.title || data.id
    }],
    metadata: {
      changes: operation === 'update' ? data._changes : undefined,
      previousValue: operation === 'delete' ? data : undefined
    }
  })
})

Sensitive Data Access

Track access to sensitive fields:

const sensitiveFields = {
  User: ['ssn', 'taxId', 'password'],
  CreditCard: ['number', 'cvv'],
  HealthRecord: ['diagnosis', 'medications']
}

on($.Database.after.read, async ({ collection, data, fields, user }) => {
  const accessed = fields.filter(f =>
    sensitiveFields[collection]?.includes(f)
  )

  if (accessed.length > 0) {
    await logEvent({
      type: 'data.sensitive.accessed',
      organizationId: user.organizationId,
      actor: {
        id: user.id,
        name: user.name,
        email: user.email
      },
      targets: [{
        id: data.id,
        type: collection,
        classification: 'sensitive'
      }],
      metadata: {
        fields: accessed,
        dataClassification: 'pii' // or 'phi', 'pci'
      }
    })
  }
})

Querying Audit Logs

Via WorkOS API

// Get recent events
const events = await workos.auditLogs.getEvents({
  organizationId: 'org_01XYZ...',
  limit: 100,
  order: 'desc',
  rangeStart: '2025-01-01T00:00:00.000Z',
  rangeEnd: '2025-01-31T23:59:59.000Z'
})

// Filter by event type
const loginEvents = await workos.auditLogs.getEvents({
  organizationId: 'org_01XYZ...',
  actions: ['user.login', 'user.logout'],
  limit: 100
})

// Filter by actor
const userEvents = await workos.auditLogs.getEvents({
  organizationId: 'org_01XYZ...',
  actorId: 'user_01ABC...',
  limit: 100
})

// Filter by target
const resourceEvents = await workos.auditLogs.getEvents({
  organizationId: 'org_01XYZ...',
  targetId: 'project_01DEF...',
  limit: 100
})

Exporting Logs

// Export to CSV
async function exportAuditLogs(organizationId, startDate, endDate) {
  const events = []
  let after = null

  // Fetch all events (paginated)
  while (true) {
    const response = await workos.auditLogs.getEvents({
      organizationId,
      rangeStart: startDate,
      rangeEnd: endDate,
      limit: 1000,
      after
    })

    events.push(...response.data)

    if (!response.listMetadata.after) break
    after = response.listMetadata.after
  }

  // Convert to CSV
  const csv = [
    'Timestamp,Event Type,Actor,Target,IP Address,Location',
    ...events.map(e => [
      e.occurredAt,
      e.action,
      e.actor.name,
      e.targets?.[0]?.name || '',
      e.context.ipAddress,
      e.context.location
    ].join(','))
  ].join('\n')

  return csv
}

Audit Log Streaming

Stream audit logs to external systems:

To S3

// Stream to S3 for long-term retention
on($.AuditLog.created, async ({ event }) => {
  const s3Key = `audit-logs/${event.organizationId}/${event.occurredAt.split('T')[0]}/${event.id}.json`

  await env.S3.put(s3Key, JSON.stringify(event, null, 2), {
    httpMetadata: {
      contentType: 'application/json'
    }
  })
})

To SIEM (Splunk, Datadog, etc.)

// Forward to SIEM for real-time monitoring
on($.AuditLog.created, async ({ event, organization }) => {
  const siem = organization.settings.siem

  if (siem?.enabled) {
    await fetch(siem.endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${siem.apiKey}`
      },
      body: JSON.stringify({
        source: 'platform.do',
        sourcetype: 'audit_log',
        event
      })
    })
  }
})

To ClickHouse (Analytics)

// Store in ClickHouse for analytics
on($.AuditLog.created, async ({ event }) => {
  await env.CLICKHOUSE.insert('audit_logs', {
    timestamp: event.occurredAt,
    organization_id: event.organizationId,
    event_type: event.action,
    actor_id: event.actor.id,
    actor_email: event.actor.email,
    target_id: event.targets?.[0]?.id,
    target_type: event.targets?.[0]?.type,
    ip_address: event.context.ipAddress,
    location: event.context.location,
    metadata: JSON.stringify(event.metadata)
  })
})

Compliance Reports

SOC 2 Report

async function generateSOC2Report(organizationId, startDate, endDate) {
  // Trust Services Criteria: Security
  const securityEvents = await workos.auditLogs.getEvents({
    organizationId,
    rangeStart: startDate,
    rangeEnd: endDate,
    actions: [
      'auth.login.failed',
      'auth.mfa.failed',
      'permission.denied',
      'data.unauthorized_access'
    ]
  })

  // Trust Services Criteria: Availability
  const availabilityMetrics = await db.query(`
    SELECT
      COUNT(*) FILTER (WHERE event_type = 'system.outage') as outages,
      SUM(duration) FILTER (WHERE event_type = 'system.outage') as total_downtime
    FROM events
    WHERE organization_id = $1
      AND timestamp >= $2
      AND timestamp <= $3
  `, [organizationId, startDate, endDate])

  // Trust Services Criteria: Processing Integrity
  const dataEvents = await workos.auditLogs.getEvents({
    organizationId,
    rangeStart: startDate,
    rangeEnd: endDate,
    actions: [
      'data.created',
      'data.updated',
      'data.deleted',
      'data.exported'
    ]
  })

  // Trust Services Criteria: Confidentiality
  const accessEvents = await workos.auditLogs.getEvents({
    organizationId,
    rangeStart: startDate,
    rangeEnd: endDate,
    actions: [
      'data.sensitive.accessed',
      'permission.role.assigned',
      'config.security.updated'
    ]
  })

  return {
    period: { startDate, endDate },
    security: {
      failedLogins: securityEvents.data.filter(e => e.action === 'auth.login.failed').length,
      unauthorizedAccess: securityEvents.data.filter(e => e.action === 'data.unauthorized_access').length
    },
    availability: {
      outages: availabilityMetrics.outages,
      totalDowntime: availabilityMetrics.total_downtime
    },
    processing: {
      recordsCreated: dataEvents.data.filter(e => e.action === 'data.created').length,
      recordsUpdated: dataEvents.data.filter(e => e.action === 'data.updated').length,
      recordsDeleted: dataEvents.data.filter(e => e.action === 'data.deleted').length
    },
    confidentiality: {
      sensitiveDataAccess: accessEvents.data.filter(e => e.action === 'data.sensitive.accessed').length,
      permissionChanges: accessEvents.data.filter(e => e.action === 'permission.role.assigned').length
    }
  }
}

HIPAA Audit Trail

async function generateHIPAAReport(organizationId, startDate, endDate) {
  // 45 CFR § 164.312(b) - Audit controls
  const phi_access = await workos.auditLogs.getEvents({
    organizationId,
    rangeStart: startDate,
    rangeEnd: endDate,
    actions: [
      'phi.accessed',
      'phi.created',
      'phi.updated',
      'phi.deleted',
      'phi.exported'
    ]
  })

  return {
    period: { startDate, endDate },
    requiredElements: {
      // Who accessed
      actors: [...new Set(phi_access.data.map(e => e.actor.id))],
      // What was accessed
      records: [...new Set(phi_access.data.map(e => e.targets?.[0]?.id))],
      // When accessed
      timestamps: phi_access.data.map(e => e.occurredAt),
      // What action was performed
      actions: phi_access.data.map(e => e.action),
      // Where accessed from
      locations: phi_access.data.map(e => e.context.ipAddress)
    },
    summary: {
      totalAccesses: phi_access.data.length,
      uniqueUsers: [...new Set(phi_access.data.map(e => e.actor.id))].length,
      uniqueRecords: [...new Set(phi_access.data.map(e => e.targets?.[0]?.id))].length
    }
  }
}

GDPR Data Access Report

// GDPR Article 15 - Right of access
async function generateGDPRDataAccessReport(userId, organizationId) {
  const events = await workos.auditLogs.getEvents({
    organizationId,
    actorId: userId,
    limit: 10000
  })

  return {
    subject: {
      userId,
      requestDate: new Date().toISOString()
    },
    personalDataProcessing: {
      purposes: [...new Set(events.data.map(e => e.action))],
      categories: ['user_profile', 'authentication', 'activity_logs'],
      recipients: ['internal_systems', 'cloud_providers'],
      retentionPeriod: '2 years',
      dataSubjectRights: [
        'right_to_access',
        'right_to_rectification',
        'right_to_erasure',
        'right_to_restrict_processing'
      ]
    },
    activityLog: events.data.map(e => ({
      timestamp: e.occurredAt,
      action: e.action,
      target: e.targets?.[0]?.type,
      purpose: e.metadata?.purpose
    }))
  }
}

Retention Policies

Configure Retention

const retentionPolicies = {
  authentication: {
    events: ['auth.*'],
    retention: 365, // days
    reason: 'SOC 2 Type II requirement'
  },
  dataAccess: {
    events: ['data.accessed', 'data.sensitive.accessed'],
    retention: 730, // 2 years
    reason: 'HIPAA requirement'
  },
  permissionChanges: {
    events: ['permission.*', 'config.*'],
    retention: 2555, // 7 years
    reason: 'Legal requirement'
  },
  general: {
    events: ['*'],
    retention: 90,
    reason: 'Standard retention'
  }
}

// Enforce retention
on($.AuditLog.cleanup, async () => {
  for (const [category, policy] of Object.entries(retentionPolicies)) {
    const cutoffDate = subDays(new Date(), policy.retention)

    await db.delete($.AuditLog, {
      eventType: { in: policy.events },
      createdAt: { lt: cutoffDate }
    })

    console.log(`Deleted ${category} events older than ${policy.retention} days`)
  }
})

Alerting & Monitoring

Real-Time Alerts

// Alert on suspicious activity
on($.AuditLog.created, async ({ event }) => {
  // Multiple failed login attempts
  if (event.action === 'auth.login.failed') {
    const recentFailures = await db.count($.AuditLog, {
      eventType: 'auth.login.failed',
      'actor.email': event.actor.email,
      createdAt: { gte: subMinutes(new Date(), 15) }
    })

    if (recentFailures >= 5) {
      await send($.Alert.security, {
        severity: 'high',
        title: 'Multiple failed login attempts',
        description: `${event.actor.email} had ${recentFailures} failed login attempts in 15 minutes`,
        metadata: event
      })
    }
  }

  // Unusual data export
  if (event.action === 'data.exported' && event.metadata.recordCount > 1000) {
    await send($.Alert.security, {
      severity: 'medium',
      title: 'Large data export',
      description: `${event.actor.email} exported ${event.metadata.recordCount} records`,
      metadata: event
    })
  }

  // Permission escalation
  if (event.action === 'permission.role.assigned' && event.metadata.role === 'admin') {
    await send($.Alert.security, {
      severity: 'high',
      title: 'Admin role assigned',
      description: `${event.actor.email} granted admin role to ${event.targets[0].name}`,
      metadata: event
    })
  }
})

User Activity Timeline

Show users their own activity:

function UserActivityTimeline({ userId }) {
  const [events, setEvents] = useState([])

  useEffect(() => {
    async function fetchActivity() {
      const response = await fetch(`/api/audit-logs/user/${userId}`)
      const data = await response.json()
      setEvents(data)
    }

    fetchActivity()
  }, [userId])

  return (
    <div className="activity-timeline">
      <h2>Your Activity</h2>
      {events.map(event => (
        <div key={event.id} className="activity-item">
          <time>{formatDate(event.occurredAt)}</time>
          <span className="event-type">{event.action}</span>
          <span className="event-description">
            {formatEventDescription(event)}
          </span>
          <span className="event-location">
            {event.context.location} ({event.context.ipAddress})
          </span>
        </div>
      ))}
    </div>
  )
}