UNPKG

bc-code-intelligence-mcp

Version:

BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows

353 lines 14.1 kB
/** * Security & Access Control System * * Enterprise-grade security features including authentication, authorization, * rate limiting, input validation, and audit logging for production deployments. */ import { createHash } from 'crypto'; import { EventEmitter } from 'events'; export class SecurityManager extends EventEmitter { config; apiKeys = new Map(); rateLimitBuckets = new Map(); activeSessions = new Map(); auditLog = []; trustedSourceCache = new Map(); constructor(config) { super(); this.config = config; this.startPeriodicCleanup(); if (config.enable_audit_logging) { console.log('🔒 Security manager initialized with audit logging'); } else { console.log('🔒 Security manager initialized'); } } /** * Authenticate request and validate permissions */ async authenticateRequest(apiKey, requestOrigin, ipAddress, requiredPermissions = []) { const context = { api_key: apiKey, origin: requestOrigin, ip_address: ipAddress }; try { // Check if authentication is required if (this.config.enable_authentication) { if (!apiKey) { this.auditSecurityEvent('auth_failure', 'medium', context, { reason: 'missing_api_key' }); return { success: false, error: 'API key required' }; } // Validate API key const keyInfo = this.apiKeys.get(apiKey); if (!keyInfo) { this.auditSecurityEvent('auth_failure', 'high', context, { reason: 'invalid_api_key' }); return { success: false, error: 'Invalid API key' }; } // Update last used timestamp keyInfo.last_used = Date.now(); // Check permissions const hasPermissions = requiredPermissions.every(perm => keyInfo.permissions.includes(perm)); if (!hasPermissions) { this.auditSecurityEvent('access_denied', 'medium', context, { reason: 'insufficient_permissions', required: requiredPermissions, available: keyInfo.permissions }); return { success: false, error: 'Insufficient permissions' }; } context.permissions = keyInfo.permissions; } else { context.permissions = ['*']; // Full access when authentication disabled } // Validate origin if configured if (this.config.allowed_origins && requestOrigin) { const isAllowed = this.config.allowed_origins.some(origin => origin === '*' || origin === requestOrigin || requestOrigin.endsWith(origin)); if (!isAllowed) { this.auditSecurityEvent('access_denied', 'high', context, { reason: 'unauthorized_origin' }); return { success: false, error: 'Origin not allowed' }; } } // Create user context const userContext = { user_id: this.generateUserId(apiKey, ipAddress), api_key: apiKey, permissions: context.permissions, rate_limit_remaining: this.getRemainingRequests(context.user_id || ipAddress || 'anonymous'), session_expires: Date.now() + (this.config.session_timeout_minutes * 60 * 1000), origin: requestOrigin, ip_address: ipAddress }; this.auditSecurityEvent('auth_success', 'low', userContext, { permissions: userContext.permissions }); return { success: true, userContext }; } catch (error) { this.auditSecurityEvent('auth_failure', 'high', context, { error: error instanceof Error ? error.message : String(error) }); return { success: false, error: 'Authentication failed' }; } } /** * Check rate limits for a user/IP */ checkRateLimit(identifier) { if (!this.config.enable_rate_limiting) { return { requests_made: 0, requests_remaining: this.config.max_requests_per_minute, reset_time: Date.now() + 60000, is_limited: false }; } const now = Date.now(); const bucket = this.rateLimitBuckets.get(identifier); // Initialize or reset bucket if needed if (!bucket || now > bucket.reset_time) { const newBucket = { count: 1, reset_time: now + 60000 // 1 minute window }; this.rateLimitBuckets.set(identifier, newBucket); return { requests_made: 1, requests_remaining: this.config.max_requests_per_minute - 1, reset_time: newBucket.reset_time, is_limited: false }; } // Increment request count bucket.count++; const isLimited = bucket.count > this.config.max_requests_per_minute; if (isLimited) { this.auditSecurityEvent('rate_limit_exceeded', 'medium', { user_id: identifier }, { requests_made: bucket.count, limit: this.config.max_requests_per_minute }); } return { requests_made: bucket.count, requests_remaining: Math.max(0, this.config.max_requests_per_minute - bucket.count), reset_time: bucket.reset_time, is_limited: isLimited }; } /** * Validate content security for layer sources */ async validateContentSecurity(sourceUrl, sourceType, content) { const warnings = []; const blockedContent = []; if (!this.config.enable_content_security) { return { secure: true, warnings: [] }; } // Check if source is trusted const isTrusted = await this.isSourceTrusted(sourceUrl); if (!isTrusted) { warnings.push(`Untrusted source: ${sourceUrl}`); } // Scan content for suspicious patterns if provided if (content) { const suspiciousPatterns = [ /javascript:/gi, /<script/gi, /eval\s*\(/gi, /document\.cookie/gi, /window\.location/gi, /\$\{.*\}/gi, // Template injection /<%.*%>/gi // Server-side template injection ]; for (const pattern of suspiciousPatterns) { const matches = content.match(pattern); if (matches) { blockedContent.push(...matches); warnings.push(`Suspicious content pattern detected: ${pattern.toString()}`); } } } // Additional URL validation if (sourceUrl) { // Block localhost and private networks in production if (process.env.NODE_ENV === 'production') { const privatePatterms = [ /localhost/gi, /127\.0\.0\.1/gi, /192\.168\./gi, /10\./gi, /172\.(1[6-9]|2[0-9]|3[0-1])\./gi ]; for (const pattern of privatePatterms) { if (pattern.test(sourceUrl)) { warnings.push(`Private network access blocked: ${sourceUrl}`); break; } } } } const secure = blockedContent.length === 0; if (!secure) { this.auditSecurityEvent('suspicious_activity', 'high', undefined, { source_url: sourceUrl, source_type: sourceType, blockedContent: blockedContent, warnings }); } return { secure, warnings, blockedContent }; } /** * Add or update API key with permissions */ addApiKey(apiKey, permissions) { const hashedKey = this.hashApiKey(apiKey); this.apiKeys.set(hashedKey, { permissions, created: Date.now(), last_used: 0 }); this.auditSecurityEvent('auth_success', 'low', undefined, { action: 'api_key_created', permissions, key_hash: hashedKey.substring(0, 8) + '...' // Log partial hash for identification }); console.log(`🔑 API key added with permissions: ${permissions.join(', ')}`); } /** * Remove API key */ removeApiKey(apiKey) { const hashedKey = this.hashApiKey(apiKey); const removed = this.apiKeys.delete(hashedKey); if (removed) { this.auditSecurityEvent('auth_success', 'low', undefined, { action: 'api_key_removed', key_hash: hashedKey.substring(0, 8) + '...' }); console.log('🔑 API key removed'); } return removed; } /** * Get security audit log */ getAuditLog(limit = 100) { return this.auditLog.slice(-limit); } /** * Get security statistics */ getSecurityStats() { const auditCounts = this.auditLog.reduce((counts, event) => { counts[event.event_type] = (counts[event.event_type] || 0) + 1; return counts; }, {}); const rateLimitedUsers = Array.from(this.rateLimitBuckets.entries()) .filter(([_, bucket]) => bucket.count > this.config.max_requests_per_minute) .length; return { active_api_keys: this.apiKeys.size, rate_limited_users: rateLimitedUsers, audit_events: this.auditLog.length, suspicious_activities: auditCounts['suspicious_activity'] || 0, successful_authentications: auditCounts['auth_success'] || 0, failed_authentications: auditCounts['auth_failure'] || 0 }; } /** * Export security configuration (sanitized) */ exportConfig() { const { api_key_required, ...safeConfig } = this.config; return { ...safeConfig, api_keys_configured: this.apiKeys.size }; } /** * Shutdown security manager */ shutdown() { this.apiKeys.clear(); this.rateLimitBuckets.clear(); this.activeSessions.clear(); this.trustedSourceCache.clear(); console.log('🔒 Security manager shutdown complete'); } // Private helper methods hashApiKey(apiKey) { return createHash('sha256').update(apiKey).digest('hex'); } generateUserId(apiKey, ipAddress) { const identifier = apiKey || ipAddress || 'anonymous'; return createHash('md5').update(identifier).digest('hex'); } getRemainingRequests(identifier) { const bucket = this.rateLimitBuckets.get(identifier); if (!bucket || Date.now() > bucket.reset_time) { return this.config.max_requests_per_minute; } return Math.max(0, this.config.max_requests_per_minute - bucket.count); } async isSourceTrusted(sourceUrl) { // Check cache first const cached = this.trustedSourceCache.get(sourceUrl); if (cached !== undefined) return cached; // Check against trusted sources list const isTrusted = this.config.trusted_sources.some(trusted => sourceUrl.includes(trusted) || sourceUrl.startsWith(trusted)); // Cache result for 1 hour this.trustedSourceCache.set(sourceUrl, isTrusted); setTimeout(() => this.trustedSourceCache.delete(sourceUrl), 3600000); return isTrusted; } auditSecurityEvent(eventType, severity, userContext, details = {}) { if (!this.config.enable_audit_logging) return; const event = { event_type: eventType, timestamp: Date.now(), user_context: userContext, details, severity }; this.auditLog.push(event); // Emit event for external listeners this.emit('security_event', event); // Keep audit log within reasonable size if (this.auditLog.length > 10000) { this.auditLog = this.auditLog.slice(-5000); // Keep last 5000 events } // Log critical events immediately if (severity === 'critical' || severity === 'high') { console.warn(`🚨 Security event [${severity.toUpperCase()}]: ${eventType}`, details); } } startPeriodicCleanup() { // Clean up old rate limit buckets and audit logs every hour setInterval(() => { const now = Date.now(); // Clean expired rate limit buckets for (const [key, bucket] of this.rateLimitBuckets.entries()) { if (now > bucket.reset_time) { this.rateLimitBuckets.delete(key); } } // Clean old audit logs based on retention policy if (this.config.enable_audit_logging && this.config.audit_log_retention_days > 0) { const cutoffTime = now - (this.config.audit_log_retention_days * 24 * 60 * 60 * 1000); this.auditLog = this.auditLog.filter(event => event.timestamp > cutoffTime); } // Clean expired sessions for (const [sessionId, session] of this.activeSessions.entries()) { const sessionTimeout = this.config.session_timeout_minutes * 60 * 1000; if (now - session.last_activity > sessionTimeout) { this.activeSessions.delete(sessionId); } } }, 3600000); // Run every hour } } //# sourceMappingURL=access-control.js.map