Skip to main content
Glama
audit-logger.ts12 kB
/** * Audit Logger - Structured logging for security and compliance */ import { writeFile, readFile, appendFile } from 'fs/promises'; import { existsSync } from 'fs'; export type AuditEventType = | 'tool_execution' | 'authentication' | 'authorization' | 'configuration_change' | 'rate_limit' | 'error' | 'system'; export type AuditEventLevel = 'info' | 'warn' | 'error' | 'critical'; export interface AuditEvent { id: string; timestamp: string; type: AuditEventType; level: AuditEventLevel; actor: { id: string; type: 'user' | 'system' | 'api_key'; name?: string; }; action: string; resource?: { type: string; id: string; name?: string; }; result: 'success' | 'failure'; details?: any; metadata?: { ip?: string; userAgent?: string; sessionId?: string; [key: string]: any; }; duration?: number; // milliseconds error?: { message: string; code: string; stack?: string; }; } export interface AuditLogOptions { logFilePath?: string; maxFileSize?: number; // bytes retentionDays?: number; enableConsoleOutput?: boolean; minLevel?: AuditEventLevel; } export interface AuditLogQuery { startDate?: Date; endDate?: Date; type?: AuditEventType; level?: AuditEventLevel; actorId?: string; action?: string; result?: 'success' | 'failure'; limit?: number; offset?: number; } export class AuditLogger { private events: AuditEvent[] = []; private options: Required<AuditLogOptions>; private eventCounter = 0; constructor(options: AuditLogOptions = {}) { this.options = { logFilePath: options.logFilePath || './audit.log', maxFileSize: options.maxFileSize || 100 * 1024 * 1024, // 100MB retentionDays: options.retentionDays || 90, enableConsoleOutput: options.enableConsoleOutput ?? true, minLevel: options.minLevel || 'info', }; } /** * Log an audit event */ async log(event: Omit<AuditEvent, 'id' | 'timestamp'>): Promise<void> { const auditEvent: AuditEvent = { id: this.generateEventId(), timestamp: new Date().toISOString(), ...event, }; // Check level filter if (!this.shouldLog(auditEvent.level)) { return; } // Add to in-memory store this.events.push(auditEvent); // Keep only recent events in memory (last 10000) if (this.events.length > 10000) { this.events = this.events.slice(-10000); } // Write to file if (this.options.logFilePath) { await this.writeToFile(auditEvent); } // Console output if (this.options.enableConsoleOutput) { this.logToConsole(auditEvent); } } /** * Log tool execution */ async logToolExecution( actorId: string, toolName: string, parameters: Record<string, any>, result: 'success' | 'failure', duration: number, error?: Error ): Promise<void> { await this.log({ type: 'tool_execution', level: result === 'success' ? 'info' : 'error', actor: { id: actorId, type: 'api_key' }, action: 'execute', resource: { type: 'tool', id: toolName, name: toolName }, result, details: { parameters: this.sanitizeParameters(parameters), }, duration, error: error ? { message: error.message, code: 'TOOL_EXECUTION_ERROR', stack: error.stack, } : undefined, }); } /** * Log authentication event */ async logAuthentication( actorId: string, result: 'success' | 'failure', metadata?: Record<string, any> ): Promise<void> { await this.log({ type: 'authentication', level: result === 'success' ? 'info' : 'warn', actor: { id: actorId, type: 'api_key' }, action: 'authenticate', result, metadata, }); } /** * Log authorization event */ async logAuthorization( actorId: string, permission: string, result: 'success' | 'failure' ): Promise<void> { await this.log({ type: 'authorization', level: result === 'success' ? 'info' : 'warn', actor: { id: actorId, type: 'api_key' }, action: 'check_permission', result, details: { permission }, }); } /** * Log configuration change */ async logConfigChange( actorId: string, configKey: string, oldValue: any, newValue: any ): Promise<void> { await this.log({ type: 'configuration_change', level: 'info', actor: { id: actorId, type: 'system' }, action: 'update_config', resource: { type: 'configuration', id: configKey }, result: 'success', details: { oldValue: this.sanitizeValue(oldValue), newValue: this.sanitizeValue(newValue), }, }); } /** * Log rate limit event */ async logRateLimit( actorId: string, remaining: number, resetAt: number ): Promise<void> { await this.log({ type: 'rate_limit', level: 'warn', actor: { id: actorId, type: 'api_key' }, action: 'rate_limit_exceeded', result: 'failure', details: { remaining, resetAt: new Date(resetAt).toISOString(), }, }); } /** * Query audit logs */ query(query: AuditLogQuery = {}): AuditEvent[] { let filtered = [...this.events]; // Filter by date range if (query.startDate) { const startTime = query.startDate.getTime(); filtered = filtered.filter( e => new Date(e.timestamp).getTime() >= startTime ); } if (query.endDate) { const endTime = query.endDate.getTime(); filtered = filtered.filter( e => new Date(e.timestamp).getTime() <= endTime ); } // Filter by type if (query.type) { filtered = filtered.filter(e => e.type === query.type); } // Filter by level if (query.level) { filtered = filtered.filter(e => e.level === query.level); } // Filter by actor if (query.actorId) { filtered = filtered.filter(e => e.actor.id === query.actorId); } // Filter by action if (query.action) { filtered = filtered.filter(e => e.action === query.action); } // Filter by result if (query.result) { filtered = filtered.filter(e => e.result === query.result); } // Apply pagination const offset = query.offset || 0; const limit = query.limit || 100; return filtered.slice(offset, offset + limit); } /** * Get audit statistics */ getStats(timeWindowMs?: number): { total: number; byType: Record<AuditEventType, number>; byLevel: Record<AuditEventLevel, number>; byResult: Record<'success' | 'failure', number>; averageDuration: number; } { let events = this.events; // Filter by time window if specified if (timeWindowMs) { const cutoff = Date.now() - timeWindowMs; events = events.filter( e => new Date(e.timestamp).getTime() >= cutoff ); } const byType: Record<string, number> = {}; const byLevel: Record<string, number> = {}; const byResult: Record<string, number> = {}; let totalDuration = 0; let countWithDuration = 0; for (const event of events) { byType[event.type] = (byType[event.type] || 0) + 1; byLevel[event.level] = (byLevel[event.level] || 0) + 1; byResult[event.result] = (byResult[event.result] || 0) + 1; if (event.duration !== undefined) { totalDuration += event.duration; countWithDuration++; } } return { total: events.length, byType: byType as any, byLevel: byLevel as any, byResult: byResult as any, averageDuration: countWithDuration > 0 ? totalDuration / countWithDuration : 0, }; } /** * Export logs to file */ async export(filePath: string, query?: AuditLogQuery): Promise<void> { const events = query ? this.query(query) : this.events; const content = events.map(e => JSON.stringify(e)).join('\n'); await writeFile(filePath, content, 'utf-8'); } /** * Load logs from file */ async load(filePath: string): Promise<void> { if (!existsSync(filePath)) { return; } const content = await readFile(filePath, 'utf-8'); const lines = content.split('\n').filter(line => line.trim()); for (const line of lines) { try { const event = JSON.parse(line); this.events.push(event); } catch (error) { console.error('Failed to parse audit log line:', error); } } console.error(`Loaded ${lines.length} audit log entries from ${filePath}`); } /** * Clear old logs based on retention policy */ async cleanup(): Promise<number> { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - this.options.retentionDays); const cutoffTime = cutoffDate.getTime(); const initialCount = this.events.length; this.events = this.events.filter( e => new Date(e.timestamp).getTime() >= cutoffTime ); const removed = initialCount - this.events.length; if (removed > 0) { console.error(`Cleaned up ${removed} old audit log entries`); } return removed; } /** * Write event to file */ private async writeToFile(event: AuditEvent): Promise<void> { try { const line = JSON.stringify(event) + '\n'; await appendFile(this.options.logFilePath, line, 'utf-8'); } catch (error) { console.error('Failed to write audit log to file:', error); } } /** * Log to console */ private logToConsole(event: AuditEvent): void { const prefix = `[AUDIT] [${event.level.toUpperCase()}] [${event.type}]`; const message = `${prefix} ${event.action} by ${event.actor.id}: ${event.result}`; switch (event.level) { case 'error': case 'critical': console.error(message, event.error || ''); break; case 'warn': console.warn(message); break; default: console.log(message); } } /** * Check if event should be logged based on level */ private shouldLog(level: AuditEventLevel): boolean { const levels: AuditEventLevel[] = ['info', 'warn', 'error', 'critical']; const minIndex = levels.indexOf(this.options.minLevel); const eventIndex = levels.indexOf(level); return eventIndex >= minIndex; } /** * Generate unique event ID */ private generateEventId(): string { this.eventCounter++; const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 10); return `${timestamp}-${this.eventCounter}-${random}`; } /** * Sanitize parameters to remove sensitive data */ private sanitizeParameters(params: Record<string, any>): Record<string, any> { const sanitized = { ...params }; const sensitiveKeys = ['password', 'secret', 'token', 'apiKey', 'api_key']; for (const key of Object.keys(sanitized)) { if (sensitiveKeys.some(sk => key.toLowerCase().includes(sk))) { sanitized[key] = '***REDACTED***'; } } return sanitized; } /** * Sanitize value for logging */ private sanitizeValue(value: any): any { if (typeof value === 'object' && value !== null) { return this.sanitizeParameters(value); } return value; } } // Global singleton instance let globalAuditLogger: AuditLogger | null = null; /** * Get the global audit logger instance */ export function getAuditLogger(): AuditLogger { if (!globalAuditLogger) { globalAuditLogger = new AuditLogger({ logFilePath: './logs/audit.log', enableConsoleOutput: true, minLevel: 'info', }); } return globalAuditLogger; } /** * Initialize the global audit logger with custom options */ export function initializeAuditLogger(options: AuditLogOptions): AuditLogger { globalAuditLogger = new AuditLogger(options); return globalAuditLogger; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/krtw00/search-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server