- Remove top-level Node.js imports that break bundlers - Use universal adapters for crypto operations - Add dynamic imports for Node.js-specific modules - Add browser field to package.json for bundler hints - Maintain full Node.js functionality while enabling browser usage This allows Brainy to be used with modern bundlers (Vite, Webpack, etc.) without requiring Node.js polyfills. Browser environments get core features while Node.js retains all capabilities including filesystem and networking.
452 lines
No EOL
11 KiB
TypeScript
452 lines
No EOL
11 KiB
TypeScript
/**
|
|
* Audit Logging Augmentation
|
|
* Provides comprehensive audit trail for all Brainy operations
|
|
*/
|
|
|
|
import { BaseAugmentation } from './brainyAugmentation.js'
|
|
import { AugmentationManifest } from './manifest.js'
|
|
import { createHash } from '../universal/crypto.js'
|
|
|
|
export interface AuditLogConfig {
|
|
enabled?: boolean
|
|
logLevel?: 'minimal' | 'standard' | 'detailed'
|
|
includeData?: boolean
|
|
includeMetadata?: boolean
|
|
retention?: number // Days to keep logs
|
|
storage?: 'memory' | 'file' | 'database'
|
|
filePath?: string
|
|
maxMemoryLogs?: number
|
|
}
|
|
|
|
export interface AuditLogEntry {
|
|
id: string
|
|
timestamp: number
|
|
operation: string
|
|
params: any
|
|
result?: any
|
|
error?: any
|
|
duration: number
|
|
userId?: string
|
|
sessionId?: string
|
|
metadata?: Record<string, any>
|
|
}
|
|
|
|
/**
|
|
* Audit Log Augmentation
|
|
*/
|
|
export class AuditLogAugmentation extends BaseAugmentation {
|
|
readonly name = 'auditLogger'
|
|
readonly timing = 'around' as const
|
|
readonly metadata = 'readonly' as const // Read metadata for context
|
|
operations = ['all'] as any // Audit all operations
|
|
readonly priority = 90 // Low priority, runs last
|
|
|
|
// Augmentation metadata
|
|
readonly category = 'core' as const
|
|
readonly description = 'Comprehensive audit logging for compliance and debugging'
|
|
|
|
private logs: AuditLogEntry[] = []
|
|
private sessionId: string
|
|
|
|
constructor(config: AuditLogConfig = {}) {
|
|
super(config)
|
|
|
|
// Merge with defaults
|
|
this.config = {
|
|
enabled: config.enabled ?? true,
|
|
logLevel: config.logLevel ?? 'standard',
|
|
includeData: config.includeData ?? false,
|
|
includeMetadata: config.includeMetadata ?? true,
|
|
retention: config.retention ?? 90, // 90 days default
|
|
storage: config.storage ?? 'memory',
|
|
filePath: config.filePath,
|
|
maxMemoryLogs: config.maxMemoryLogs ?? 10000
|
|
}
|
|
|
|
// Generate session ID
|
|
this.sessionId = this.generateId()
|
|
}
|
|
|
|
getManifest(): AugmentationManifest {
|
|
return {
|
|
id: 'audit-logger',
|
|
name: 'Audit Logger',
|
|
version: '1.0.0',
|
|
description: 'Comprehensive audit trail for all operations',
|
|
longDescription: 'Records detailed audit logs of all Brainy operations for compliance, debugging, and analytics purposes.',
|
|
category: 'analytics',
|
|
configSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
enabled: {
|
|
type: 'boolean',
|
|
default: true,
|
|
description: 'Enable audit logging'
|
|
},
|
|
logLevel: {
|
|
type: 'string',
|
|
enum: ['minimal', 'standard', 'detailed'],
|
|
default: 'standard',
|
|
description: 'Level of detail to log'
|
|
},
|
|
includeData: {
|
|
type: 'boolean',
|
|
default: false,
|
|
description: 'Include actual data in logs (privacy concern)'
|
|
},
|
|
includeMetadata: {
|
|
type: 'boolean',
|
|
default: true,
|
|
description: 'Include metadata in logs'
|
|
},
|
|
retention: {
|
|
type: 'number',
|
|
default: 90,
|
|
minimum: 1,
|
|
maximum: 365,
|
|
description: 'Days to retain logs'
|
|
},
|
|
storage: {
|
|
type: 'string',
|
|
enum: ['memory', 'file', 'database'],
|
|
default: 'memory',
|
|
description: 'Where to store audit logs'
|
|
},
|
|
maxMemoryLogs: {
|
|
type: 'number',
|
|
default: 10000,
|
|
description: 'Maximum logs to keep in memory'
|
|
}
|
|
}
|
|
},
|
|
configDefaults: {
|
|
enabled: true,
|
|
logLevel: 'standard',
|
|
includeData: false,
|
|
includeMetadata: true,
|
|
retention: 90,
|
|
storage: 'memory',
|
|
maxMemoryLogs: 10000
|
|
},
|
|
minBrainyVersion: '3.0.0',
|
|
keywords: ['audit', 'logging', 'compliance', 'analytics'],
|
|
documentation: 'https://docs.brainy.dev/augmentations/audit-log',
|
|
status: 'stable',
|
|
performance: {
|
|
memoryUsage: 'medium',
|
|
cpuUsage: 'low',
|
|
networkUsage: 'none'
|
|
},
|
|
features: ['operation-logging', 'configurable-detail', 'retention-management'],
|
|
enhancedOperations: ['all'],
|
|
metrics: [
|
|
{
|
|
name: 'audit_logs_created',
|
|
type: 'counter',
|
|
description: 'Total audit logs created'
|
|
},
|
|
{
|
|
name: 'audit_log_size',
|
|
type: 'gauge',
|
|
description: 'Current audit log size'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
protected async onInitialize(): Promise<void> {
|
|
if (!this.config.enabled) {
|
|
this.log('Audit logger disabled by configuration')
|
|
return
|
|
}
|
|
|
|
this.log(`Audit logger initialized (level: ${this.config.logLevel}, storage: ${this.config.storage})`)
|
|
|
|
// Start retention cleanup if using memory storage
|
|
if (this.config.storage === 'memory') {
|
|
setInterval(() => {
|
|
this.cleanupOldLogs()
|
|
}, 3600000) // Every hour
|
|
}
|
|
}
|
|
|
|
protected async onShutdown(): Promise<void> {
|
|
// Save any pending logs if using file storage
|
|
if (this.config.storage === 'file' && this.logs.length > 0) {
|
|
await this.flushLogs()
|
|
}
|
|
|
|
this.log('Audit logger shut down')
|
|
}
|
|
|
|
/**
|
|
* Execute augmentation - log operations
|
|
*/
|
|
async execute<T = any>(
|
|
operation: string,
|
|
params: any,
|
|
next: () => Promise<T>
|
|
): Promise<T> {
|
|
// If audit logging is disabled, just pass through
|
|
if (!this.config.enabled) {
|
|
return next()
|
|
}
|
|
|
|
const startTime = Date.now()
|
|
const logEntry: Partial<AuditLogEntry> = {
|
|
id: this.generateId(),
|
|
timestamp: startTime,
|
|
operation,
|
|
sessionId: this.sessionId
|
|
}
|
|
|
|
// Add params based on log level
|
|
if (this.config.logLevel !== 'minimal') {
|
|
logEntry.params = this.sanitizeParams(params)
|
|
}
|
|
|
|
try {
|
|
const result = await next()
|
|
|
|
// Log successful operation
|
|
logEntry.duration = Date.now() - startTime
|
|
|
|
// Add result based on log level and config
|
|
if (this.config.logLevel === 'detailed' && this.config.includeData) {
|
|
logEntry.result = this.sanitizeResult(result)
|
|
}
|
|
|
|
await this.writeLog(logEntry as AuditLogEntry)
|
|
|
|
return result
|
|
} catch (error) {
|
|
// Log failed operation
|
|
logEntry.duration = Date.now() - startTime
|
|
logEntry.error = this.sanitizeError(error)
|
|
|
|
await this.writeLog(logEntry as AuditLogEntry)
|
|
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sanitize parameters to remove sensitive data
|
|
*/
|
|
private sanitizeParams(params: any): any {
|
|
if (!params) return params
|
|
|
|
// Don't include actual data unless configured
|
|
if (!this.config.includeData && params.data) {
|
|
return {
|
|
...params,
|
|
data: '[REDACTED]'
|
|
}
|
|
}
|
|
|
|
// Redact common sensitive fields
|
|
const sanitized = { ...params }
|
|
const sensitiveFields = ['password', 'token', 'apiKey', 'secret']
|
|
|
|
for (const field of sensitiveFields) {
|
|
if (sanitized[field]) {
|
|
sanitized[field] = '[REDACTED]'
|
|
}
|
|
}
|
|
|
|
return sanitized
|
|
}
|
|
|
|
/**
|
|
* Sanitize result data
|
|
*/
|
|
private sanitizeResult(result: any): any {
|
|
if (!result) return result
|
|
|
|
// For arrays, just log count
|
|
if (Array.isArray(result)) {
|
|
return { count: result.length, type: 'array' }
|
|
}
|
|
|
|
// For objects, remove sensitive fields
|
|
if (typeof result === 'object') {
|
|
const sanitized: any = {}
|
|
for (const key in result) {
|
|
if (!['password', 'token', 'apiKey', 'secret'].includes(key)) {
|
|
sanitized[key] = result[key]
|
|
}
|
|
}
|
|
return sanitized
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Sanitize error information
|
|
*/
|
|
private sanitizeError(error: any): any {
|
|
if (!error) return error
|
|
|
|
return {
|
|
message: error.message || 'Unknown error',
|
|
code: error.code,
|
|
statusCode: error.statusCode,
|
|
stack: this.config.logLevel === 'detailed' ? error.stack : undefined
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write log entry
|
|
*/
|
|
private async writeLog(entry: AuditLogEntry): Promise<void> {
|
|
switch (this.config.storage) {
|
|
case 'memory':
|
|
this.logs.push(entry)
|
|
|
|
// Enforce max memory logs
|
|
if (this.logs.length > this.config.maxMemoryLogs!) {
|
|
this.logs.shift() // Remove oldest
|
|
}
|
|
break
|
|
|
|
case 'file':
|
|
// In production, would write to file
|
|
// For now, just add to memory
|
|
this.logs.push(entry)
|
|
break
|
|
|
|
case 'database':
|
|
// In production, would write to database
|
|
// For now, just add to memory
|
|
this.logs.push(entry)
|
|
break
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Flush logs to persistent storage
|
|
*/
|
|
private async flushLogs(): Promise<void> {
|
|
// In production, would write to file/database
|
|
// For now, just clear old logs
|
|
if (this.logs.length > this.config.maxMemoryLogs!) {
|
|
this.logs = this.logs.slice(-this.config.maxMemoryLogs!)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up old logs based on retention
|
|
*/
|
|
private cleanupOldLogs(): void {
|
|
const cutoffTime = Date.now() - (this.config.retention! * 24 * 60 * 60 * 1000)
|
|
|
|
this.logs = this.logs.filter(log => log.timestamp >= cutoffTime)
|
|
}
|
|
|
|
/**
|
|
* Generate unique ID
|
|
*/
|
|
private generateId(): string {
|
|
return createHash('sha256')
|
|
.update(`${Date.now()}-${Math.random()}`)
|
|
.digest('hex')
|
|
.substring(0, 16)
|
|
}
|
|
|
|
/**
|
|
* Query audit logs
|
|
*/
|
|
queryLogs(filter?: {
|
|
operation?: string
|
|
startTime?: number
|
|
endTime?: number
|
|
sessionId?: string
|
|
hasError?: boolean
|
|
}): AuditLogEntry[] {
|
|
let results = [...this.logs]
|
|
|
|
if (filter) {
|
|
if (filter.operation) {
|
|
results = results.filter(log => log.operation === filter.operation)
|
|
}
|
|
|
|
if (filter.startTime) {
|
|
results = results.filter(log => log.timestamp >= filter.startTime!)
|
|
}
|
|
|
|
if (filter.endTime) {
|
|
results = results.filter(log => log.timestamp <= filter.endTime!)
|
|
}
|
|
|
|
if (filter.sessionId) {
|
|
results = results.filter(log => log.sessionId === filter.sessionId)
|
|
}
|
|
|
|
if (filter.hasError !== undefined) {
|
|
results = results.filter(log => (log.error !== undefined) === filter.hasError)
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
/**
|
|
* Get audit statistics
|
|
*/
|
|
getStats(): {
|
|
totalLogs: number
|
|
operations: Record<string, number>
|
|
averageDuration: number
|
|
errorRate: number
|
|
} {
|
|
const stats: any = {
|
|
totalLogs: this.logs.length,
|
|
operations: {},
|
|
averageDuration: 0,
|
|
errorRate: 0
|
|
}
|
|
|
|
let totalDuration = 0
|
|
let errorCount = 0
|
|
|
|
for (const log of this.logs) {
|
|
// Count by operation
|
|
stats.operations[log.operation] = (stats.operations[log.operation] || 0) + 1
|
|
|
|
// Sum duration
|
|
totalDuration += log.duration
|
|
|
|
// Count errors
|
|
if (log.error) errorCount++
|
|
}
|
|
|
|
if (this.logs.length > 0) {
|
|
stats.averageDuration = totalDuration / this.logs.length
|
|
stats.errorRate = errorCount / this.logs.length
|
|
}
|
|
|
|
return stats
|
|
}
|
|
|
|
/**
|
|
* Export logs for analysis
|
|
*/
|
|
exportLogs(): AuditLogEntry[] {
|
|
return [...this.logs]
|
|
}
|
|
|
|
/**
|
|
* Clear all logs
|
|
*/
|
|
clearLogs(): void {
|
|
this.logs = []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create audit log augmentation
|
|
*/
|
|
export function createAuditLogAugmentation(config?: AuditLogConfig): AuditLogAugmentation {
|
|
return new AuditLogAugmentation(config)
|
|
} |