/** * Phoenix Audit Service * Immutable audit logs, WORM archive, PII boundaries, compliance */ import { getDb } from '../../db/index.js' import { logger } from '../../lib/logger.js' export interface AuditLog { logId: string userId: string | null action: string resourceType: string resourceId: string details: Record timestamp: Date ipAddress?: string userAgent?: string } export interface AuditQuery { userId?: string action?: string resourceType?: string resourceId?: string startDate?: Date endDate?: Date limit?: number } class AuditService { /** * Create immutable audit log */ async log( action: string, resourceType: string, resourceId: string, details: Record, userId?: string, ipAddress?: string, userAgent?: string ): Promise { const db = getDb() // Scrub PII from details const scrubbedDetails = this.scrubPII(details) const result = await db.query( `INSERT INTO audit_logs ( user_id, action, resource_type, resource_id, details, ip_address, user_agent, timestamp ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW()) RETURNING *`, [ userId || null, action, resourceType, resourceId, JSON.stringify(scrubbedDetails), ipAddress || null, userAgent || null ] ) logger.info('Audit log created', { logId: result.rows[0].id, action }) // Archive to WORM storage if needed await this.archiveToWORM(result.rows[0]) return this.mapAuditLog(result.rows[0]) } /** * Query audit logs */ async query(query: AuditQuery): Promise { const db = getDb() const conditions: string[] = [] const params: any[] = [] let paramIndex = 1 if (query.userId) { conditions.push(`user_id = $${paramIndex++}`) params.push(query.userId) } if (query.action) { conditions.push(`action = $${paramIndex++}`) params.push(query.action) } if (query.resourceType) { conditions.push(`resource_type = $${paramIndex++}`) params.push(query.resourceType) } if (query.resourceId) { conditions.push(`resource_id = $${paramIndex++}`) params.push(query.resourceId) } if (query.startDate) { conditions.push(`timestamp >= $${paramIndex++}`) params.push(query.startDate) } if (query.endDate) { conditions.push(`timestamp <= $${paramIndex++}`) params.push(query.endDate) } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '' const limit = query.limit || 1000 params.push(limit) const result = await db.query( `SELECT * FROM audit_logs ${whereClause} ORDER BY timestamp DESC LIMIT $${paramIndex}`, params ) return result.rows.map(this.mapAuditLog) } /** * Export audit logs for compliance */ async exportForCompliance( startDate: Date, endDate: Date, format: 'JSON' | 'CSV' = 'JSON' ): Promise { const logs = await this.query({ startDate, endDate, limit: 1000000 }) if (format === 'JSON') { return JSON.stringify(logs, null, 2) } else { // CSV format const headers = ['logId', 'userId', 'action', 'resourceType', 'resourceId', 'timestamp'] const rows = logs.map(log => [ log.logId, log.userId || '', log.action, log.resourceType, log.resourceId, log.timestamp.toISOString() ]) return [headers.join(','), ...rows.map(row => row.join(','))].join('\n') } } private scrubPII(data: Record): Record { // Placeholder - would implement actual PII scrubbing // Remove SSNs, credit cards, etc. based on PII boundaries const scrubbed = { ...data } // Example: remove credit card numbers if (scrubbed.cardNumber) { scrubbed.cardNumber = '***REDACTED***' } return scrubbed } private async archiveToWORM(log: any): Promise { // Archive to WORM (Write Once Read Many) storage for compliance // This would write to immutable storage (S3 with object lock, etc.) logger.info('Archiving to WORM storage', { logId: log.id }) // Placeholder - would implement actual WORM archiving } private mapAuditLog(row: any): AuditLog { return { logId: row.id, userId: row.user_id, action: row.action, resourceType: row.resource_type, resourceId: row.resource_id, details: typeof row.details === 'string' ? JSON.parse(row.details) : row.details, timestamp: row.timestamp, ipAddress: row.ip_address, userAgent: row.user_agent } } } export const auditService = new AuditService()