UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

754 lines (753 loc) 31.3 kB
import { EventEmitter } from 'events'; import { promises as fs } from 'fs'; import { resolve, normalize } from 'path'; import crypto from 'crypto'; import { ErrorFactory, createErrorContext } from '../utils/enhanced-errors.js'; import { createSuccess, createFailure } from './unified-lifecycle-manager.js'; import { getUnifiedSecurityConfig } from '../security/unified-security-config.js'; import logger from '../../../logger.js'; export function createSecurityId(id) { if (!id || id.trim().length === 0) { throw new Error('Security ID cannot be empty'); } return id; } export function createSessionId(id) { if (!id || id.trim().length === 0) { throw new Error('Session ID cannot be empty'); } return id; } export function createLockId(id) { if (!id || id.trim().length === 0) { throw new Error('Lock ID cannot be empty'); } return id; } export function createAuditId(id) { if (!id || id.trim().length === 0) { throw new Error('Audit ID cannot be empty'); } return id; } export class UnifiedSecurityEngine extends EventEmitter { static instance = null; config; initialized = false; activeSessions = new Map(); activeLocks = new Map(); auditEvents = new Map(); permissionCache = new Map(); eventCount = 0; eventsByType = new Map(); eventsBySeverity = new Map(); violationCount = 0; blockedAttempts = 0; totalResponseTime = 0; lockCleanupTimer = null; auditCleanupTimer = null; metricsTimer = null; constructor(config) { super(); this.config = config; ['authentication', 'authorization', 'access_attempt', 'data_access', 'data_modification', 'security_violation', 'system_event', 'suspicious_activity', 'compliance_event', 'error_event'].forEach(type => { this.eventsByType.set(type, 0); }); ['info', 'low', 'medium', 'high', 'critical'].forEach(severity => { this.eventsBySeverity.set(severity, 0); }); logger.info('Unified Security Engine initialized'); } static getInstance(config) { if (!UnifiedSecurityEngine.instance) { if (!config) { throw new Error('Configuration required for first initialization'); } UnifiedSecurityEngine.instance = new UnifiedSecurityEngine(config); } return UnifiedSecurityEngine.instance; } static resetInstance() { if (UnifiedSecurityEngine.instance) { UnifiedSecurityEngine.instance.dispose(); UnifiedSecurityEngine.instance = null; } } async initialize() { if (this.initialized) { return createSuccess(undefined); } try { await this.loadSecurityConfiguration(); await this.initializeSecurityComponents(); this.startBackgroundProcesses(); this.initialized = true; this.emit('initialized'); logger.info('Security engine initialized successfully'); return createSuccess(undefined); } catch (error) { return createFailure(ErrorFactory.createError('system', `Failed to initialize security engine: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedSecurityEngine', 'initialize').build(), { cause: error instanceof Error ? error : undefined })); } } async loadSecurityConfiguration() { try { getUnifiedSecurityConfig(); logger.info('Security configuration loaded successfully'); } catch (error) { logger.warn('Failed to load security configuration, using defaults:', error); } } async initializeSecurityComponents() { await this.initializeAuditLogging(); await this.initializePathSecurity(); await this.initializeConcurrentAccess(); logger.info('Security components initialized'); } async initializeAuditLogging() { if (!this.config.audit.enabled) { return; } await this.logSecurityEvent({ type: 'system_event', severity: 'info', action: 'security_engine_startup', result: 'success', details: { config: { strictMode: this.config.strictMode, auditEnabled: this.config.audit.enabled, pathSecurityEnabled: this.config.pathSecurity.enabled } } }); } async initializePathSecurity() { if (!this.config.pathSecurity.enabled) { return; } for (const path of this.config.pathSecurity.allowedReadPaths) { try { await fs.access(path); } catch { logger.warn(`Configured read path does not exist: ${path}`); } } for (const path of this.config.pathSecurity.allowedWritePaths) { try { await fs.access(path); } catch { logger.warn(`Configured write path does not exist: ${path}`); } } } async initializeConcurrentAccess() { if (!this.config.concurrentAccess.enabled) { return; } await this.cleanupStaleLocks(); } startBackgroundProcesses() { if (this.config.concurrentAccess.enabled) { this.lockCleanupTimer = setInterval(() => { this.cleanupStaleLocks().catch(error => { logger.error('Lock cleanup process failed:', error); }); }, this.config.concurrentAccess.lockCleanupInterval * 1000); } if (this.config.audit.enabled) { this.auditCleanupTimer = setInterval(() => { this.cleanupOldAuditEvents().catch(error => { logger.error('Audit cleanup process failed:', error); }); }, 24 * 60 * 60 * 1000); } this.metricsTimer = setInterval(() => { this.collectMetrics(); }, 60 * 1000); logger.info('Security background processes started'); } async authenticateUser(credentials) { const startTime = Date.now(); try { if (!this.config.authentication.enabled) { return createFailure(ErrorFactory.createError('permission', 'Authentication is disabled', createErrorContext('UnifiedSecurityEngine', 'authenticateUser').build())); } const isValid = await this.validateCredentials(credentials); if (!isValid) { await this.logSecurityEvent({ type: 'authentication', severity: 'medium', action: 'authentication_failed', result: 'failure', details: { userId: credentials.userId, reason: 'invalid_credentials' } }); return createFailure(ErrorFactory.createError('permission', 'Invalid credentials', createErrorContext('UnifiedSecurityEngine', 'authenticateUser').build())); } const sessionId = createSessionId(crypto.randomUUID()); const userContext = { userId: credentials.userId, sessionId, role: this.determineUserRole(credentials.userId), permissions: await this.getUserPermissions(credentials.userId), authenticatedAt: new Date(), lastActivity: new Date(), metadata: credentials.metadata || {} }; this.activeSessions.set(sessionId, userContext); await this.logSecurityEvent({ type: 'authentication', severity: 'info', action: 'authentication_success', result: 'success', details: { userId: credentials.userId, sessionId, role: userContext.role } }); this.trackPerformance('authentication', Date.now() - startTime); this.emit('userAuthenticated', userContext); return createSuccess(userContext); } catch (error) { return createFailure(ErrorFactory.createError('system', `Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedSecurityEngine', 'authenticateUser').build(), { cause: error instanceof Error ? error : undefined })); } } async authorizeAction(sessionId, permission, resource) { const startTime = Date.now(); try { if (!this.config.authorization.enabled) { return createSuccess(true); } const userContext = this.activeSessions.get(sessionId); if (!userContext) { await this.logSecurityEvent({ type: 'authorization', severity: 'medium', action: 'authorization_failed', result: 'failure', details: { sessionId, permission, resource, reason: 'invalid_session' } }); return createFailure(ErrorFactory.createError('permission', 'Invalid session', createErrorContext('UnifiedSecurityEngine', 'authorizeAction').build())); } const hasPermission = userContext.permissions.includes(permission) || userContext.permissions.includes('system:admin'); if (!hasPermission) { await this.logSecurityEvent({ type: 'authorization', severity: 'medium', action: 'authorization_denied', result: 'blocked', details: { userId: userContext.userId, sessionId, permission, resource, userRole: userContext.role, userPermissions: userContext.permissions } }); this.blockedAttempts++; return createSuccess(false); } userContext.lastActivity = new Date(); await this.logSecurityEvent({ type: 'authorization', severity: 'info', action: 'authorization_granted', result: 'success', details: { userId: userContext.userId, sessionId, permission, resource } }); this.trackPerformance('authorization', Date.now() - startTime); return createSuccess(true); } catch (error) { return createFailure(ErrorFactory.createError('system', `Authorization failed: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedSecurityEngine', 'authorizeAction').build(), { cause: error instanceof Error ? error : undefined })); } } async validatePath(inputPath, operation) { const startTime = Date.now(); try { if (!this.config.pathSecurity.enabled) { return createSuccess({ isValid: true, normalizedPath: inputPath, auditInfo: { originalPath: inputPath, timestamp: new Date(), validationTime: Date.now() - startTime } }); } const result = { isValid: false, auditInfo: { originalPath: inputPath, timestamp: new Date(), validationTime: 0 } }; let normalizedPath; try { normalizedPath = resolve(normalize(inputPath)); } catch { result.error = 'Invalid path format'; result.violationType = 'invalid_path'; result.auditInfo.validationTime = Date.now() - startTime; return createSuccess(result); } if (inputPath.includes('..') || inputPath.includes('~')) { result.error = 'Path traversal detected'; result.violationType = 'path_traversal'; result.auditInfo.validationTime = Date.now() - startTime; await this.logSecurityEvent({ type: 'security_violation', severity: 'high', action: 'path_traversal_attempt', result: 'blocked', details: { originalPath: inputPath, normalizedPath, operation } }); this.violationCount++; return createSuccess(result); } const allowedPaths = operation === 'read' ? this.config.pathSecurity.allowedReadPaths : this.config.pathSecurity.allowedWritePaths; const isAllowed = allowedPaths.some(allowedPath => { const resolvedAllowedPath = resolve(allowedPath); return normalizedPath.startsWith(resolvedAllowedPath); }); if (!isAllowed) { result.error = 'Path outside allowed boundaries'; result.violationType = 'outside_boundary'; result.auditInfo.validationTime = Date.now() - startTime; await this.logSecurityEvent({ type: 'security_violation', severity: 'medium', action: 'boundary_violation', result: 'blocked', details: { originalPath: inputPath, normalizedPath, operation, allowedPaths } }); this.violationCount++; return createSuccess(result); } if (this.config.pathSecurity.allowedExtensions.length > 0) { const ext = inputPath.split('.').pop()?.toLowerCase(); const extWithDot = ext ? `.${ext}` : ''; if (ext && !this.config.pathSecurity.allowedExtensions.includes(extWithDot)) { result.error = 'File extension not allowed'; result.violationType = 'invalid_extension'; result.warnings = [`Extension '${extWithDot}' is not in allowed list: ${this.config.pathSecurity.allowedExtensions.join(', ')}`]; result.auditInfo.validationTime = Date.now() - startTime; return createSuccess(result); } } result.isValid = true; result.normalizedPath = normalizedPath; result.auditInfo.validationTime = Date.now() - startTime; this.trackPerformance('path_validation', Date.now() - startTime); return createSuccess(result); } catch (error) { return createFailure(ErrorFactory.createError('system', `Path validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedSecurityEngine', 'validatePath').build(), { cause: error instanceof Error ? error : undefined })); } } async sanitizeData(data) { const startTime = Date.now(); try { if (!this.config.dataSanitization.enabled) { return createSuccess({ success: true, sanitizedData: data, originalData: data, violations: [], sanitizationTime: Date.now() - startTime }); } const result = { success: false, originalData: data, violations: [], sanitizationTime: 0 }; let sanitizedData; if (typeof data === 'string') { sanitizedData = this.sanitizeString(data); } else if (typeof data === 'object' && data !== null) { sanitizedData = this.sanitizeObject(data); } else { sanitizedData = data; } result.success = true; result.sanitizedData = sanitizedData; result.sanitizationTime = Date.now() - startTime; this.trackPerformance('sanitization', Date.now() - startTime); return createSuccess(result); } catch (error) { return createFailure(ErrorFactory.createError('system', `Data sanitization failed: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedSecurityEngine', 'sanitizeData').build(), { cause: error instanceof Error ? error : undefined })); } } sanitizeString(input) { let sanitized = input; if (!this.config.dataSanitization.allowScripts) { sanitized = sanitized.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ''); } if (!this.config.dataSanitization.allowHtml) { sanitized = sanitized.replace(/<[^>]*>/g, ''); } if (sanitized.length > this.config.dataSanitization.maxStringLength) { sanitized = sanitized.substring(0, this.config.dataSanitization.maxStringLength); } sanitized = sanitized.replace(/\0/g, ''); return sanitized; } sanitizeObject(input) { if (Array.isArray(input)) { return input.map(item => this.sanitizeObject(item)); } if (typeof input === 'object' && input !== null) { const sanitized = {}; for (const [key, value] of Object.entries(input)) { const sanitizedKey = this.sanitizeString(key); sanitized[sanitizedKey] = this.sanitizeObject(value); } return sanitized; } if (typeof input === 'string') { return this.sanitizeString(input); } return input; } async acquireLock(resource, operation, sessionId, timeoutMs) { const startTime = Date.now(); try { if (!this.config.concurrentAccess.enabled) { return createSuccess({ success: true, lockId: createLockId(crypto.randomUUID()) }); } const lockId = createLockId(crypto.randomUUID()); const timeout = timeoutMs || this.config.concurrentAccess.maxLockDuration; const existingLock = Array.from(this.activeLocks.values()).find(lock => lock.resource === resource && (lock.operation === 'write' || operation === 'write')); if (existingLock) { return createSuccess({ success: false, error: 'Resource is locked', existingLock, waitTime: Date.now() - startTime }); } const lockInfo = { id: lockId, resource, owner: sessionId ? this.activeSessions.get(sessionId)?.userId || 'unknown' : 'system', sessionId, operation, acquiredAt: new Date(), expiresAt: new Date(Date.now() + timeout), metadata: {} }; this.activeLocks.set(lockId, lockInfo); await this.logSecurityEvent({ type: 'system_event', severity: 'info', action: 'lock_acquired', result: 'success', details: { lockId, resource, operation, owner: lockInfo.owner } }); this.emit('lockAcquired', lockInfo); return createSuccess({ success: true, lockId, waitTime: Date.now() - startTime }); } catch (error) { return createFailure(ErrorFactory.createError('system', `Lock acquisition failed: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedSecurityEngine', 'acquireLock').build(), { cause: error instanceof Error ? error : undefined })); } } async releaseLock(lockId) { try { const lockInfo = this.activeLocks.get(lockId); if (!lockInfo) { return createFailure(ErrorFactory.createError('validation', `Lock not found: ${lockId}`, createErrorContext('UnifiedSecurityEngine', 'releaseLock').build())); } this.activeLocks.delete(lockId); await this.logSecurityEvent({ type: 'system_event', severity: 'info', action: 'lock_released', result: 'success', details: { lockId, resource: lockInfo.resource, owner: lockInfo.owner } }); this.emit('lockReleased', lockInfo); return createSuccess(undefined); } catch (error) { return createFailure(ErrorFactory.createError('system', `Lock release failed: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedSecurityEngine', 'releaseLock').build(), { cause: error instanceof Error ? error : undefined })); } } async logSecurityEvent(event) { try { if (!this.config.audit.enabled) { return createSuccess(createAuditId('disabled')); } const auditId = createAuditId(crypto.randomUUID()); const auditEvent = { id: auditId, timestamp: new Date(), metadata: {}, ...event }; this.auditEvents.set(auditId, auditEvent); this.eventCount++; this.eventsByType.set(event.type, (this.eventsByType.get(event.type) || 0) + 1); this.eventsBySeverity.set(event.severity, (this.eventsBySeverity.get(event.severity) || 0) + 1); const logMessage = `Security Event: ${event.action} - ${event.result}`; switch (event.severity) { case 'critical': case 'high': logger.error(logMessage, auditEvent); break; case 'medium': logger.warn(logMessage, auditEvent); break; default: logger.info(logMessage, auditEvent); } this.emit('securityEvent', auditEvent); return createSuccess(auditId); } catch (error) { return createFailure(ErrorFactory.createError('system', `Security event logging failed: ${error instanceof Error ? error.message : 'Unknown error'}`, createErrorContext('UnifiedSecurityEngine', 'logSecurityEvent').build(), { cause: error instanceof Error ? error : undefined })); } } async validateCredentials(credentials) { return credentials.userId.length > 0 && credentials.token.length > 0; } determineUserRole(userId) { if (userId === 'admin') return 'admin'; if (userId.startsWith('manager')) return 'manager'; if (userId.startsWith('dev')) return 'developer'; return 'viewer'; } async getUserPermissions(userId) { const cacheKey = `permissions:${userId}`; if (this.config.authorization.permissionCache && this.permissionCache.has(cacheKey)) { return this.permissionCache.get(cacheKey); } const role = this.determineUserRole(userId); let permissions = []; switch (role) { case 'admin': permissions = ['system:admin', 'system:config', 'system:audit']; break; case 'manager': permissions = ['project:create', 'project:read', 'project:update', 'task:create', 'task:read', 'task:update', 'agent:manage']; break; case 'developer': permissions = ['task:create', 'task:read', 'task:update', 'task:execute', 'file:read', 'file:write']; break; case 'viewer': permissions = ['task:read', 'project:read', 'file:read']; break; default: permissions = []; } if (this.config.authorization.permissionCache) { this.permissionCache.set(cacheKey, permissions); } return permissions; } async cleanupStaleLocks() { const now = new Date(); const staleLocks = []; for (const [lockId, lockInfo] of this.activeLocks.entries()) { if (lockInfo.expiresAt < now) { staleLocks.push(lockId); } } for (const lockId of staleLocks) { const lockInfo = this.activeLocks.get(lockId); this.activeLocks.delete(lockId); if (lockInfo) { await this.logSecurityEvent({ type: 'system_event', severity: 'info', action: 'lock_expired', result: 'success', details: { lockId, resource: lockInfo.resource, owner: lockInfo.owner } }); } } if (staleLocks.length > 0) { logger.debug(`Cleaned up ${staleLocks.length} stale locks`); } } async cleanupOldAuditEvents() { const cutoffDate = new Date(Date.now() - this.config.audit.retentionDays * 24 * 60 * 60 * 1000); const oldEvents = []; for (const [auditId, event] of this.auditEvents.entries()) { if (event.timestamp < cutoffDate) { oldEvents.push(auditId); } } for (const auditId of oldEvents) { this.auditEvents.delete(auditId); } if (oldEvents.length > 0) { logger.debug(`Cleaned up ${oldEvents.length} old audit events`); } } trackPerformance(operation, responseTime) { this.totalResponseTime += responseTime; if (responseTime > this.config.performanceThresholdMs) { logger.warn(`Security operation '${operation}' exceeded performance threshold: ${responseTime}ms`); } } collectMetrics() { const stats = { totalEvents: this.eventCount, eventsByType: Object.fromEntries(this.eventsByType), eventsBySeverity: Object.fromEntries(this.eventsBySeverity), violationCount: this.violationCount, blockedAttempts: this.blockedAttempts, activeUsers: new Set(Array.from(this.activeSessions.values()).map(s => s.userId)).size, activeSessions: this.activeSessions.size, activeLocks: this.activeLocks.size, averageResponseTime: this.eventCount > 0 ? this.totalResponseTime / this.eventCount : 0, securityScore: this.calculateSecurityScore() }; this.emit('metricsCollected', stats); } calculateSecurityScore() { let score = 100; if (this.violationCount > 0) { score -= Math.min(this.violationCount * 2, 30); } if (this.blockedAttempts > 0) { score -= Math.min(this.blockedAttempts * 1, 20); } const avgResponseTime = this.eventCount > 0 ? this.totalResponseTime / this.eventCount : 0; if (avgResponseTime > this.config.performanceThresholdMs) { score -= 10; } return Math.max(score, 0); } getStatistics() { return { totalEvents: this.eventCount, eventsByType: Object.fromEntries(this.eventsByType), eventsBySeverity: Object.fromEntries(this.eventsBySeverity), violationCount: this.violationCount, blockedAttempts: this.blockedAttempts, activeUsers: new Set(Array.from(this.activeSessions.values()).map(s => s.userId)).size, activeSessions: this.activeSessions.size, activeLocks: this.activeLocks.size, averageResponseTime: this.eventCount > 0 ? this.totalResponseTime / this.eventCount : 0, securityScore: this.calculateSecurityScore() }; } dispose() { if (this.lockCleanupTimer) { clearInterval(this.lockCleanupTimer); this.lockCleanupTimer = null; } if (this.auditCleanupTimer) { clearInterval(this.auditCleanupTimer); this.auditCleanupTimer = null; } if (this.metricsTimer) { clearInterval(this.metricsTimer); this.metricsTimer = null; } this.activeSessions.clear(); this.activeLocks.clear(); this.auditEvents.clear(); this.permissionCache.clear(); this.removeAllListeners(); this.initialized = false; logger.info('Unified Security Engine disposed'); } } export function createDefaultSecurityConfig() { return { enabled: true, strictMode: false, performanceThresholdMs: 1000, logViolations: true, blockOnCriticalViolations: true, authentication: { enabled: true, tokenExpiryMinutes: 60, maxSessionsPerUser: 5, requireStrongPasswords: true, enableMFA: false }, authorization: { enabled: true, defaultRole: 'viewer', roleHierarchy: { admin: ['manager', 'developer', 'viewer', 'guest'], manager: ['developer', 'viewer', 'guest'], developer: ['viewer', 'guest'], viewer: ['guest'], guest: [] }, permissionCache: true }, pathSecurity: { enabled: true, allowedReadPaths: [process.cwd()], allowedWritePaths: [process.cwd()], allowedExtensions: ['.ts', '.js', '.json', '.md', '.txt', '.yaml', '.yml'], blockSystemPaths: true, followSymlinks: false }, dataSanitization: { enabled: true, strictMode: false, allowHtml: false, allowScripts: false, maxStringLength: 10000, sanitizeFileNames: true }, concurrentAccess: { enabled: true, maxLockDuration: 300000, deadlockDetection: true, lockCleanupInterval: 60, maxLocksPerResource: 10 }, audit: { enabled: true, logLevel: 'info', retentionDays: 30, enableIntegrityChecks: true, compressLogs: false }, filesystem: { enabled: true, systemDirectoryBlacklist: [ '/private/var/spool', '/System', '/usr/bin', '/usr/sbin', '/bin', '/sbin' ], maxFileSize: 10485760, allowedMimeTypes: ['text/plain', 'application/json', 'text/markdown'], scanForMalware: false } }; }