Files
Sankofa/api/src/services/sovereign-stack/audit-service.ts
defiQUG 8436e22f4c API: Phoenix railing proxy, API key auth for /api/v1/*, schema export, docs, migrations, tests
- 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
2026-03-11 12:57:41 -07:00

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()