UNPKG

mcp-quiz-server

Version:

🧠 AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.

562 lines (561 loc) • 19.4 kB
"use strict"; /** * @fileoverview Audit Logger - Security Event Tracking System * @version 1.0.0 * @since 2025-08-02 * @lastUpdated 2025-08-02 * @module AuditLogger * @description Comprehensive audit logging system for tracking tool executions, * security violations, and resource usage patterns * @contributors Claude Code Agent * @dependencies fs/promises, crypto, path * @requirements SECURITY_002 (Audit Trail), SECURITY_003 (Monitoring) * @testCoverage Unit tests for audit event creation and storage */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.auditLogger = exports.AuditLogger = exports.FileAuditStorage = exports.AuditSeverity = exports.AuditEventType = void 0; const crypto = __importStar(require("crypto")); const events_1 = require("events"); const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); /** * Audit event types */ var AuditEventType; (function (AuditEventType) { AuditEventType["TOOL_EXECUTION_STARTED"] = "TOOL_EXECUTION_STARTED"; AuditEventType["TOOL_EXECUTION_COMPLETED"] = "TOOL_EXECUTION_COMPLETED"; AuditEventType["TOOL_EXECUTION_FAILED"] = "TOOL_EXECUTION_FAILED"; AuditEventType["SECURITY_VIOLATION"] = "SECURITY_VIOLATION"; AuditEventType["RESOURCE_LIMIT_WARNING"] = "RESOURCE_LIMIT_WARNING"; AuditEventType["SANDBOX_CREATED"] = "SANDBOX_CREATED"; AuditEventType["SANDBOX_DESTROYED"] = "SANDBOX_DESTROYED"; AuditEventType["AUTHENTICATION_EVENT"] = "AUTHENTICATION_EVENT"; AuditEventType["CONFIGURATION_CHANGE"] = "CONFIGURATION_CHANGE"; })(AuditEventType || (exports.AuditEventType = AuditEventType = {})); /** * Severity levels for audit events */ var AuditSeverity; (function (AuditSeverity) { AuditSeverity["LOW"] = "low"; AuditSeverity["MEDIUM"] = "medium"; AuditSeverity["HIGH"] = "high"; AuditSeverity["CRITICAL"] = "critical"; })(AuditSeverity || (exports.AuditSeverity = AuditSeverity = {})); /** * File-based audit storage implementation */ class FileAuditStorage { constructor(auditDir = 'logs/audit') { this.maxFileSize = 10 * 1024 * 1024; // 10MB this.maxFiles = 100; this.auditDir = path.resolve(auditDir); this.currentLogFile = this.getCurrentLogFileName(); } /** * Initialize audit storage */ async initialize() { try { await fs.mkdir(this.auditDir, { recursive: true }); } catch (error) { console.error('Failed to create audit directory:', error); throw error; } } /** * Store audit event */ async store(event) { try { // Check if we need to rotate log file await this.rotateLogFileIfNeeded(); // Append event to current log file const logEntry = JSON.stringify(event) + '\n'; await fs.appendFile(this.currentLogFile, logEntry); } catch (error) { console.error('Failed to store audit event:', error); throw error; } } /** * Query audit events */ async query(filter) { const events = []; try { // Get all log files const files = await fs.readdir(this.auditDir); const logFiles = files .filter(file => file.startsWith('audit-') && file.endsWith('.jsonl')) .sort((a, b) => b.localeCompare(a)); // Newest first // Read events from files for (const file of logFiles) { const filePath = path.join(this.auditDir, file); const content = await fs.readFile(filePath, 'utf-8'); for (const line of content.split('\n')) { if (line.trim()) { try { const event = JSON.parse(line); if (this.matchesFilter(event, filter)) { events.push(event); } } catch (parseError) { console.warn('Failed to parse audit event:', parseError); } } } // Stop if we have enough events if (filter.limit && events.length >= filter.limit) { break; } } // Sort and limit results events.sort((a, b) => { const order = filter.sortOrder === 'asc' ? 1 : -1; return (a.timestamp - b.timestamp) * order; }); return filter.limit ? events.slice(0, filter.limit) : events; } catch (error) { console.error('Failed to query audit events:', error); throw error; } } /** * Archive old events */ async archive(olderThan) { let archivedCount = 0; try { const files = await fs.readdir(this.auditDir); const logFiles = files.filter(file => file.startsWith('audit-') && file.endsWith('.jsonl')); for (const file of logFiles) { const filePath = path.join(this.auditDir, file); const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); const newLines = []; for (const line of lines) { if (line.trim()) { try { const event = JSON.parse(line); if (event.timestamp >= olderThan.getTime()) { newLines.push(line); } else { archivedCount++; } } catch (parseError) { // Keep unparseable lines newLines.push(line); } } } // Write back filtered content await fs.writeFile(filePath, newLines.join('\n')); } return archivedCount; } catch (error) { console.error('Failed to archive audit events:', error); throw error; } } /** * Get storage statistics */ async getStats() { const stats = { totalEvents: 0, eventsByType: {}, eventsBySeverity: {}, storageSize: 0, }; try { const files = await fs.readdir(this.auditDir); const logFiles = files.filter(file => file.startsWith('audit-') && file.endsWith('.jsonl')); for (const file of logFiles) { const filePath = path.join(this.auditDir, file); const fileStats = await fs.stat(filePath); stats.storageSize += fileStats.size; const content = await fs.readFile(filePath, 'utf-8'); for (const line of content.split('\n')) { if (line.trim()) { try { const event = JSON.parse(line); stats.totalEvents++; stats.eventsByType[event.type] = (stats.eventsByType[event.type] || 0) + 1; stats.eventsBySeverity[event.severity] = (stats.eventsBySeverity[event.severity] || 0) + 1; const eventDate = new Date(event.timestamp); if (!stats.oldestEvent || eventDate < stats.oldestEvent) { stats.oldestEvent = eventDate; } if (!stats.newestEvent || eventDate > stats.newestEvent) { stats.newestEvent = eventDate; } } catch (parseError) { // Skip unparseable events } } } } return stats; } catch (error) { console.error('Failed to get audit storage stats:', error); throw error; } } /** * Get current log file name */ getCurrentLogFileName() { const now = new Date(); const dateStr = now.toISOString().split('T')[0]; // YYYY-MM-DD return path.join(this.auditDir, `audit-${dateStr}.jsonl`); } /** * Rotate log file if needed */ async rotateLogFileIfNeeded() { try { const stats = await fs.stat(this.currentLogFile); if (stats.size > this.maxFileSize) { // Create new log file this.currentLogFile = this.getCurrentLogFileName(); // Clean up old files if needed await this.cleanupOldFiles(); } } catch (error) { // File doesn't exist yet, create directory await fs.mkdir(path.dirname(this.currentLogFile), { recursive: true }); } } /** * Clean up old log files */ async cleanupOldFiles() { try { const files = await fs.readdir(this.auditDir); const logFiles = files .filter(file => file.startsWith('audit-') && file.endsWith('.jsonl')) .sort((a, b) => b.localeCompare(a)); // Newest first if (logFiles.length > this.maxFiles) { const filesToDelete = logFiles.slice(this.maxFiles); for (const file of filesToDelete) { await fs.unlink(path.join(this.auditDir, file)); } } } catch (error) { console.warn('Failed to cleanup old audit files:', error); } } /** * Check if event matches filter */ matchesFilter(event, filter) { if (filter.types && !filter.types.includes(event.type)) { return false; } if (filter.severities && !filter.severities.includes(event.severity)) { return false; } if (filter.toolNames && event.toolName && !filter.toolNames.includes(event.toolName)) { return false; } if (filter.timeRange) { const eventTime = event.timestamp; if (eventTime < filter.timeRange.from.getTime() || eventTime > filter.timeRange.to.getTime()) { return false; } } return true; } } exports.FileAuditStorage = FileAuditStorage; /** * Main audit logger class */ class AuditLogger extends events_1.EventEmitter { constructor(storage) { super(); this.eventQueue = []; this.flushInterval = null; this.isProcessingQueue = false; this.storage = storage || new FileAuditStorage(); this.startFlushInterval(); } /** * Initialize audit logger */ async initialize() { if ('initialize' in this.storage && typeof this.storage.initialize === 'function') { await this.storage.initialize(); } } /** * Log tool execution started */ async logToolExecutionStarted(executionId, toolName, toolArguments, userId) { const event = { eventId: this.generateEventId(), type: AuditEventType.TOOL_EXECUTION_STARTED, severity: AuditSeverity.LOW, timestamp: Date.now(), timestampIso: new Date().toISOString(), executionId, toolName, userId, description: `Tool execution started: ${toolName}`, data: { toolName, executionId, arguments: toolArguments, }, source: 'SandboxManager', hash: '', }; event.hash = this.generateEventHash(event); await this.queueEvent(event); } /** * Log tool execution completed */ async logToolExecutionCompleted(executionId, toolName, executionTime, resourceUsage, userId) { const event = { eventId: this.generateEventId(), type: AuditEventType.TOOL_EXECUTION_COMPLETED, severity: AuditSeverity.LOW, timestamp: Date.now(), timestampIso: new Date().toISOString(), executionId, toolName, userId, description: `Tool execution completed: ${toolName} (${executionTime}ms)`, data: { toolName, executionId, executionTime, resourceUsage, }, source: 'SandboxManager', hash: '', }; event.hash = this.generateEventHash(event); await this.queueEvent(event); } /** * Log tool execution failed */ async logToolExecutionFailed(executionId, toolName, error, userId) { const event = { eventId: this.generateEventId(), type: AuditEventType.TOOL_EXECUTION_FAILED, severity: AuditSeverity.MEDIUM, timestamp: Date.now(), timestampIso: new Date().toISOString(), executionId, toolName, userId, description: `Tool execution failed: ${toolName} - ${error.message}`, data: { toolName, executionId, error: error.message, }, source: 'SandboxManager', hash: '', }; event.hash = this.generateEventHash(event); await this.queueEvent(event); } /** * Log security violation */ async logSecurityViolation(violation) { const severity = this.mapViolationSeverity(violation.severity); const event = { eventId: this.generateEventId(), type: AuditEventType.SECURITY_VIOLATION, severity, timestamp: Date.now(), timestampIso: new Date().toISOString(), executionId: violation.executionId, toolName: violation.toolName, description: `Security violation: ${violation.description}`, data: { violationType: violation.type, violationDetails: violation, impact: violation.severity, mitigationAction: 'Execution terminated', }, source: 'SandboxManager', hash: '', }; event.hash = this.generateEventHash(event); await this.queueEvent(event); } /** * Query audit events */ async queryEvents(filter) { return this.storage.query(filter); } /** * Get audit statistics */ async getStats() { return this.storage.getStats(); } /** * Archive old events */ async archiveOldEvents(olderThan) { return this.storage.archive(olderThan); } /** * Queue event for processing */ async queueEvent(event) { this.eventQueue.push(event); this.emit('eventQueued', event); // Process immediately if queue is getting large if (this.eventQueue.length > 100) { await this.flushQueue(); } } /** * Flush event queue */ async flushQueue() { if (this.isProcessingQueue || this.eventQueue.length === 0) { return; } this.isProcessingQueue = true; try { const eventsToProcess = [...this.eventQueue]; this.eventQueue = []; for (const event of eventsToProcess) { try { await this.storage.store(event); this.emit('eventStored', event); } catch (error) { console.error('Failed to store audit event:', error); this.emit('eventStorageFailed', { event, error }); } } } finally { this.isProcessingQueue = false; } } /** * Start flush interval */ startFlushInterval() { this.flushInterval = setInterval(() => { this.flushQueue().catch(error => { console.error('Failed to flush audit queue:', error); }); }, 5000); // Flush every 5 seconds } /** * Stop flush interval */ stopFlushInterval() { if (this.flushInterval) { clearInterval(this.flushInterval); this.flushInterval = null; } } /** * Generate unique event ID */ generateEventId() { return `audit_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`; } /** * Generate event hash for integrity */ generateEventHash(event) { const eventData = JSON.stringify({ eventId: event.eventId, type: event.type, timestamp: event.timestamp, data: event.data, }); return crypto.createHash('sha256').update(eventData).digest('hex'); } /** * Map violation severity to audit severity */ mapViolationSeverity(severity) { switch (severity) { case 'low': return AuditSeverity.LOW; case 'medium': return AuditSeverity.MEDIUM; case 'high': return AuditSeverity.HIGH; case 'critical': return AuditSeverity.CRITICAL; default: return AuditSeverity.MEDIUM; } } /** * Cleanup resources */ async cleanup() { this.stopFlushInterval(); await this.flushQueue(); this.removeAllListeners(); } } exports.AuditLogger = AuditLogger; // Export singleton instance exports.auditLogger = new AuditLogger();