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
JavaScript
"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();