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.

411 lines (410 loc) 17.8 kB
"use strict"; /** * @moduleName: audit-logger * @version: 1.0.0 * @since: 2025-01-16 * @lastUpdated: 2025-07-27 * @projectSummary: MCP Quiz Server - Comprehensive audit logging system with multiple transport options * @techStack: TypeScript, Winston, Google Cloud Logging, Express.js * @dependency: winston, @google-cloud/logging * @interModuleDependency: logging-config.ts, server.ts * @requirementsTraceability: * {@link Requirements.REQ_COMPLIANCE_001} (Security Audit Logging) * {@link Requirements.REQ_COMPLIANCE_002} (Data Privacy Compliance) * {@link Requirements.REQ_COMPLIANCE_003} (Audit Trail Management) * {@link Requirements.REQ_COMPLIANCE_004} (Access Control Logging) * {@link Requirements.REQ_SEC_002} (Security Event Tracking) * {@link Requirements.REQ_SEC_003} (PII Protection) * @briefDescription: Enterprise-grade audit logging with PII filtering, Google Cloud integration, and request correlation * @methods: logAuditEvent, logUserAction, logSecurityEvent, logDatabaseOperation, createAuditMiddleware * @contributors: Garden Golem * @examples: const logger = getAuditLogger(); logger.logUserAction('LOGIN', { userId: '123' }); * @vulnerabilitiesAssessment: PII filtering prevents data leakage, sampling prevents log flooding */ 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 = void 0; exports.createAuditMiddleware = createAuditMiddleware; exports.getAuditLogger = getAuditLogger; exports.initializeAuditLogger = initializeAuditLogger; const crypto = __importStar(require("crypto")); const winston = __importStar(require("winston")); const logging_config_1 = require("../config/logging-config"); /** * Comprehensive audit logger with multiple transport options and PII filtering */ class AuditLogger { constructor(config) { this.config = config || (0, logging_config_1.getEnvironmentLoggingConfig)(); this.logger = this.createLogger(); this.auditLogger = this.createAuditLogger(); } // Create main application logger createLogger() { const transports = []; // Console transport (always enabled in development) if (this.config.console.enabled || process.env.NODE_ENV === 'development') { transports.push(new winston.transports.Console({ level: this.config.level, format: winston.format.combine(winston.format.colorize(), winston.format.timestamp(), winston.format.printf(({ timestamp, level, message, ...meta }) => { return `${timestamp} [${level}]: ${message} ${Object.keys(meta).length ? JSON.stringify(meta) : ''}`; })), })); } // File transport if (this.config.file.enabled) { transports.push(new winston.transports.File({ filename: this.config.file.filename, level: this.config.level, maxsize: this.config.file.maxsize, maxFiles: this.config.file.maxFiles, format: winston.format.combine(winston.format.timestamp(), winston.format.json()), })); } // Google Cloud Logging transport if (this.config.googleCloud.enabled) { try { // Try to load Google Cloud Logging - conditional require // eslint-disable-next-line @typescript-eslint/no-var-requires const { LoggingWinston } = require('@google-cloud/logging-winston'); const googleCloudTransport = new LoggingWinston({ projectId: this.config.googleCloud.projectId, keyFilename: this.config.googleCloud.keyFilename, logName: this.config.googleCloud.logName, resource: { type: this.config.googleCloud.resourceType, labels: this.config.googleCloud.resourceLabels || {}, }, serviceContext: { service: 'mcp-quiz-server', version: process.env.npm_package_version || '1.0.0', }, }); transports.push(googleCloudTransport); // Use logger instance for consistent logging (not console.log directly) setTimeout(() => { var _a; (_a = this.logger) === null || _a === void 0 ? void 0 : _a.info('Google Cloud Logging transport configured', { provider: 'google-cloud', status: 'enabled', service: 'application-logs', }); }, 100); } catch (error) { // Use logger instance for error reporting (not console.warn directly) setTimeout(() => { var _a; (_a = this.logger) === null || _a === void 0 ? void 0 : _a.warn('Google Cloud Logging transport unavailable', { provider: 'google-cloud', status: 'fallback-to-console', reason: 'dependency-missing', suggestion: 'npm install @google-cloud/logging-winston', error: error instanceof Error ? error.message : 'Unknown error', }); }, 100); // Fall back to console logging if (!this.config.console.enabled) { transports.push(new winston.transports.Console({ level: this.config.level, format: winston.format.combine(winston.format.colorize(), winston.format.timestamp(), winston.format.printf(({ timestamp, level, message, ...meta }) => { return `${timestamp} [${level}]: ${message} ${Object.keys(meta).length ? JSON.stringify(meta) : ''}`; })), })); } } } return winston.createLogger({ level: 'info', format: winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json()), transports: transports.length > 0 ? transports : [new winston.transports.Console()], }); } // Create dedicated audit logger with special formatting createAuditLogger() { const transports = []; // Audit file transport (separate from main logs) if (this.config.auditEnabled && this.config.file.enabled) { transports.push(new winston.transports.File({ filename: this.config.file.filename.replace('.log', '-audit.log'), level: 'info', maxsize: this.config.file.maxsize, maxFiles: this.config.file.maxFiles, format: winston.format.combine(winston.format.timestamp(), winston.format.json()), })); } // Always log audit events to console in development if (process.env.NODE_ENV === 'development') { transports.push(new winston.transports.Console({ format: winston.format.combine(winston.format.colorize(), winston.format.timestamp(), winston.format.printf(({ timestamp, level, message, ...meta }) => { return `🔍 AUDIT ${timestamp}: ${message} ${JSON.stringify(meta)}`; })), })); } // Google Cloud Logging for audit events if (this.config.auditEnabled && this.config.googleCloud.enabled) { try { // eslint-disable-next-line @typescript-eslint/no-var-requires const { LoggingWinston } = require('@google-cloud/logging-winston'); const googleCloudAuditTransport = new LoggingWinston({ projectId: this.config.googleCloud.projectId, keyFilename: this.config.googleCloud.keyFilename, logName: this.config.googleCloud.auditLogName, resource: { type: this.config.googleCloud.resourceType, labels: { ...this.config.googleCloud.resourceLabels, log_type: 'audit', }, }, serviceContext: { service: 'mcp-quiz-server-audit', version: process.env.npm_package_version || '1.0.0', }, }); transports.push(googleCloudAuditTransport); // Use audit logger instance for consistent audit logging setTimeout(() => { var _a; (_a = this.auditLogger) === null || _a === void 0 ? void 0 : _a.info('Google Cloud audit transport configured', { provider: 'google-cloud', status: 'enabled', service: 'audit-logs', }); }, 100); } catch (error) { // Use audit logger instance for structured error reporting setTimeout(() => { var _a; (_a = this.auditLogger) === null || _a === void 0 ? void 0 : _a.warn('Google Cloud audit transport unavailable', { provider: 'google-cloud', status: 'fallback-to-console', reason: 'dependency-missing', suggestion: 'npm install @google-cloud/logging-winston', }); }, 100); } } return winston.createLogger({ level: 'info', format: winston.format.combine(winston.format.timestamp(), winston.format.json()), transports: transports.length > 0 ? transports : [new winston.transports.Console()], }); } // PII filtering utility filterPII(obj) { if (!this.config.piiFiltering.enabled) return obj; const filtered = JSON.parse(JSON.stringify(obj)); const redactValue = (value, key) => { if (typeof value === 'string' && this.config.piiFiltering.fields.some(field => key.toLowerCase().includes(field.toLowerCase()))) { return '[REDACTED]'; } if (typeof value === 'object' && value !== null) { if (Array.isArray(value)) { return value.map((item, index) => redactValue(item, `${key}[${index}]`)); } else { const result = {}; for (const [k, v] of Object.entries(value)) { result[k] = redactValue(v, k); } return result; } } return value; }; return redactValue(filtered, ''); } // Generate request ID for correlation generateRequestId() { return crypto.randomBytes(16).toString('hex'); } // Check sampling rate shouldLog() { if (!this.config.sampling.enabled) return true; return Math.random() < this.config.sampling.rate; } // Public logging methods info(message, meta) { if (this.shouldLog()) { this.logger.info(message, this.filterPII(meta || {})); } } warn(message, meta) { if (this.shouldLog()) { this.logger.warn(message, this.filterPII(meta || {})); } } error(message, error, meta) { const errorMeta = { ...meta, error: error ? { name: error.name, message: error.message, stack: process.env.NODE_ENV === 'development' ? error.stack : undefined, } : undefined, }; this.logger.error(message, this.filterPII(errorMeta)); } // Audit logging methods logAuditEvent(event) { if (!this.config.auditEnabled) return; const auditRecord = { ...event, timestamp: new Date().toISOString(), audit_type: 'APPLICATION_AUDIT', service: 'mcp-quiz-server', version: process.env.npm_package_version || '1.0.0', }; this.auditLogger.info('AUDIT_EVENT', this.filterPII(auditRecord)); } logUserAction(action, meta) { var _a; this.logAuditEvent({ eventType: 'USER_ACTION', action, timestamp: new Date().toISOString(), success: (_a = meta.success) !== null && _a !== void 0 ? _a : true, risk_level: this.calculateRiskLevel(action, meta), ...meta, }); } logSecurityEvent(action, meta) { this.logAuditEvent({ eventType: 'SECURITY_EVENT', action, timestamp: new Date().toISOString(), success: !meta.blocked, risk_level: meta.severity || 'HIGH', ...meta, }); } logDatabaseOperation(event) { if (!this.config.auditEnabled) return; const auditRecord = { ...event, audit_type: 'DATABASE_AUDIT', service: 'mcp-quiz-server', timestamp: new Date().toISOString(), }; this.auditLogger.info('DATABASE_AUDIT', this.filterPII(auditRecord)); } calculateRiskLevel(action, meta) { // High-risk actions if (['DELETE', 'ADMIN_ACCESS', 'PASSWORD_CHANGE', 'PERMISSION_CHANGE'].includes(action)) { return 'HIGH'; } // Medium-risk actions if (['CREATE', 'UPDATE', 'LOGIN', 'LOGOUT'].includes(action)) { return 'MEDIUM'; } // Failed operations are higher risk if (meta.success === false) { return 'MEDIUM'; } return 'LOW'; } } exports.AuditLogger = AuditLogger; // Express middleware for automatic request auditing function createAuditMiddleware(auditLogger) { return (req, res, next) => { const requestId = req.headers['x-request-id'] || crypto.randomBytes(16).toString('hex'); const startTime = Date.now(); // Add request ID to request for correlation req.requestId = requestId; // Log request start auditLogger.logAuditEvent({ eventType: 'SYSTEM_EVENT', action: 'HTTP_REQUEST_START', requestId, resource: `${req.method} ${req.path}`, ipAddress: req.ip || req.connection.remoteAddress, userAgent: req.get('User-Agent'), timestamp: new Date().toISOString(), success: true, metadata: { method: req.method, url: req.url, headers: auditLogger.config.includeRequestBody ? req.headers : undefined, body: auditLogger.config.includeRequestBody && req.body ? JSON.stringify(req.body).substring(0, auditLogger.config.maxRequestBodySize) : undefined, }, }); // Capture response const originalSend = res.send; res.send = function (body) { const duration = Date.now() - startTime; auditLogger.logAuditEvent({ eventType: 'SYSTEM_EVENT', action: 'HTTP_REQUEST_COMPLETE', requestId, resource: `${req.method} ${req.path}`, timestamp: new Date().toISOString(), success: res.statusCode < 400, metadata: { statusCode: res.statusCode, duration, responseBody: auditLogger.config.includeResponseBody && body ? JSON.stringify(body).substring(0, auditLogger.config.maxRequestBodySize) : undefined, }, }); return originalSend.call(this, body); }; next(); }; } // Global audit logger instance let globalAuditLogger; function getAuditLogger() { if (!globalAuditLogger) { globalAuditLogger = new AuditLogger(); } return globalAuditLogger; } function initializeAuditLogger(config) { globalAuditLogger = new AuditLogger(config); return globalAuditLogger; }