- Phoenix API Railing: proxy to PHOENIX_RAILING_URL, tenant me routes - Tenant-auth: X-API-Key support for /api/v1/* (api_keys table) - Migration 026: api_keys table; 025 sovereign stack marketplace - GET /graphql/schema, GET /graphql-playground, api/docs OpenAPI - Integration tests: phoenix-railing.test.ts - docs/api/API_VERSIONING: /api/v1/ railing alignment - docs/phoenix/PORTAL_RAILING_WIRING Made-with: Cursor
192 lines
4.7 KiB
TypeScript
192 lines
4.7 KiB
TypeScript
/**
|
|
* 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<string, any>
|
|
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<string, any>,
|
|
userId?: string,
|
|
ipAddress?: string,
|
|
userAgent?: string
|
|
): Promise<AuditLog> {
|
|
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<AuditLog[]> {
|
|
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<string> {
|
|
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<string, any>): Record<string, any> {
|
|
// 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<void> {
|
|
// 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()
|