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
JavaScript
"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;
}