UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

591 lines 22.3 kB
/** * Security Manager * Implements MCP Design Guide Section 5.2 principles for zero-trust architecture */ import { ErrorHandler } from './error-handler.js'; /** * Implements zero-trust security model for MCP tool access */ export class SecurityManager { static instance; securityPolicies = new Map(); securityEvents = []; usageTracker = new Map(); pendingApprovals = new Map(); constructor() { this.initializeDefaultPolicies(); } static getInstance() { if (!SecurityManager.instance) { SecurityManager.instance = new SecurityManager(); } return SecurityManager.instance; } /** * Initialize default security policies for critical tools */ initializeDefaultPolicies() { // High-risk development tools this.addSecurityPolicy({ toolName: 'run_tests', requiredPermissions: ['execute:tests'], minimumRoleLevel: 'write', requiresHumanApproval: false, maxUsagePerHour: 50, allowedOrigins: ['*'], logLevel: 'basic' }); // Code modification tools this.addSecurityPolicy({ toolName: 'implement_feature_code', requiredPermissions: ['modify:code'], minimumRoleLevel: 'write', requiresHumanApproval: true, maxUsagePerHour: 20, allowedOrigins: ['*'], logLevel: 'detailed' }); // Data access tools this.addSecurityPolicy({ toolName: 'store_memory', requiredPermissions: ['write:memory'], minimumRoleLevel: 'write', requiresHumanApproval: false, maxUsagePerHour: 100, allowedOrigins: ['*'], logLevel: 'basic' }); // Security scanning tools this.addSecurityPolicy({ toolName: 'perform_security_scan', requiredPermissions: ['execute:security'], minimumRoleLevel: 'read', requiresHumanApproval: false, maxUsagePerHour: 10, allowedOrigins: ['*'], logLevel: 'detailed' }); // Administrative tools this.addSecurityPolicy({ toolName: 'configure_security_settings', requiredPermissions: ['admin:security'], minimumRoleLevel: 'admin', requiresHumanApproval: true, maxUsagePerHour: 5, allowedOrigins: ['*'], logLevel: 'detailed' }); // File system access this.addSecurityPolicy({ toolName: 'create_kanban_board', requiredPermissions: ['write:kanban'], minimumRoleLevel: 'write', requiresHumanApproval: false, maxUsagePerHour: 20, allowedOrigins: ['*'], logLevel: 'basic' }); } /** * Add or update a security policy for a tool */ addSecurityPolicy(policy) { this.securityPolicies.set(policy.toolName, policy); } /** * Validate access to a tool based on security context and policies */ async validateToolAccess(toolName, context, parameters) { const policy = this.securityPolicies.get(toolName); // If no policy exists, apply default restrictions if (!policy) { this.logSecurityEvent({ type: 'policy_violation', toolName, context, timestamp: Date.now(), details: { reason: 'No security policy defined' }, riskLevel: 'medium' }); // Default to allowing basic read operations if (this.isReadOnlyTool(toolName)) { return { allowed: true }; } return { allowed: false, reason: 'No security policy defined for this tool' }; } // Check role level if (!this.hasRequiredRoleLevel(context.roleLevel, policy.minimumRoleLevel)) { this.logSecurityEvent({ type: 'access_denied', toolName, context, timestamp: Date.now(), details: { reason: 'Insufficient role level', required: policy.minimumRoleLevel, provided: context.roleLevel }, riskLevel: 'medium' }); return { allowed: false, reason: `Requires ${policy.minimumRoleLevel} role level or higher` }; } // Check permissions const hasPermissions = policy.requiredPermissions.every(permission => context.permissions.includes(permission)); if (!hasPermissions) { this.logSecurityEvent({ type: 'access_denied', toolName, context, timestamp: Date.now(), details: { reason: 'Missing required permissions', required: policy.requiredPermissions, provided: context.permissions }, riskLevel: 'high' }); return { allowed: false, reason: `Missing required permissions: ${policy.requiredPermissions.join(', ')}` }; } // Check rate limiting if (!this.checkRateLimit(toolName, policy.maxUsagePerHour)) { this.logSecurityEvent({ type: 'policy_violation', toolName, context, timestamp: Date.now(), details: { reason: 'Rate limit exceeded', limit: policy.maxUsagePerHour }, riskLevel: 'medium' }); return { allowed: false, reason: `Rate limit exceeded: maximum ${policy.maxUsagePerHour} uses per hour` }; } // Check for suspicious patterns const suspiciousActivity = this.detectSuspiciousActivity(toolName, context, parameters); if (suspiciousActivity) { this.logSecurityEvent({ type: 'suspicious_activity', toolName, context, timestamp: Date.now(), details: suspiciousActivity, riskLevel: 'high' }); return { allowed: false, reason: 'Suspicious activity detected' }; } // Check if human approval is required if (policy.requiresHumanApproval) { return { allowed: false, requiresApproval: true, reason: 'This operation requires human approval' }; } // Log successful access this.logSecurityEvent({ type: 'access_granted', toolName, context, timestamp: Date.now(), details: { parameters: this.sanitizeParameters(parameters) }, riskLevel: 'low' }); // Update usage counter this.updateUsageCounter(toolName); return { allowed: true }; } /** * Request human approval for a tool operation */ async requestHumanApproval(toolName, context, parameters, justification) { const approvalId = `approval_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; this.pendingApprovals.set(approvalId, { toolName, context, parameters: this.sanitizeParameters(parameters), justification, timestamp: Date.now(), status: 'pending' }); this.logSecurityEvent({ type: 'access_denied', toolName, context, timestamp: Date.now(), details: { reason: 'Pending human approval', approvalId, justification }, riskLevel: 'medium' }); return approvalId; } /** * Generate security metrics and alerts */ generateSecurityMetrics() { const total = this.securityEvents.length; const denied = this.securityEvents.filter(e => e.type === 'access_denied').length; const suspicious = this.securityEvents.filter(e => e.type === 'suspicious_activity').length; const highRisk = this.securityEvents.filter(e => e.riskLevel === 'high' || e.riskLevel === 'critical').length; // Count tool usage const toolCounts = {}; this.securityEvents.forEach(event => { toolCounts[event.toolName] = (toolCounts[event.toolName] || 0) + 1; }); const topTools = Object.entries(toolCounts) .sort(([, a], [, b]) => b - a) .slice(0, 5) .map(([tool, count]) => ({ tool, count })); // Generate alerts const alerts = []; if (suspicious > 0) { alerts.push(`${suspicious} suspicious activities detected`); } if (highRisk > total * 0.1) { alerts.push(`High percentage of high-risk events (${Math.round(highRisk / total * 100)}%)`); } if (denied > total * 0.3) { alerts.push(`High access denial rate (${Math.round(denied / total * 100)}%)`); } return { totalEvents: total, accessDenied: denied, suspiciousActivity: suspicious, highRiskEvents: highRisk, topTargetedTools: topTools, alerts }; } hasRequiredRoleLevel(userRole, requiredRole) { const roleHierarchy = ['read', 'write', 'admin', 'system']; const userLevel = roleHierarchy.indexOf(userRole); const requiredLevel = roleHierarchy.indexOf(requiredRole); return userLevel >= requiredLevel; } checkRateLimit(toolName, maxPerHour) { const key = toolName; const now = Date.now(); const hourInMs = 60 * 60 * 1000; let usage = this.usageTracker.get(key); if (!usage || now - usage.lastReset > hourInMs) { usage = { count: 0, lastReset: now }; this.usageTracker.set(key, usage); } return usage.count < maxPerHour; } updateUsageCounter(toolName) { const key = toolName; const usage = this.usageTracker.get(key); if (usage) { usage.count++; } } detectSuspiciousActivity(toolName, context, parameters) { const suspiciousPatterns = {}; // Check for rapid successive calls const recentEvents = this.securityEvents .filter(e => e.toolName === toolName && e.context.sessionId === context.sessionId) .filter(e => Date.now() - e.timestamp < 60000); // Last minute if (recentEvents.length > 10) { suspiciousPatterns.rapidCalls = { count: recentEvents.length, timeframe: '1 minute' }; } // Check for unusual parameter patterns if (parameters) { // Look for potential injection attempts const paramString = JSON.stringify(parameters).toLowerCase(); const injectionPatterns = [ 'script', 'eval(', 'exec(', 'system(', 'rm -rf', 'drop table', 'union select', '<script', 'javascript:' ]; const foundPatterns = injectionPatterns.filter(pattern => paramString.includes(pattern)); if (foundPatterns.length > 0) { suspiciousPatterns.injectionAttempt = { patterns: foundPatterns }; } } return Object.keys(suspiciousPatterns).length > 0 ? suspiciousPatterns : null; } isReadOnlyTool(toolName) { const readOnlyTools = [ 'list_kanban_boards', 'list_agile_sprints', 'list_agile_backlog', 'get_sprint_status', 'search_memories', 'check_project_status', 'generate_velocity_report' ]; return readOnlyTools.includes(toolName); } sanitizeParameters(parameters) { if (!parameters) return parameters; const sensitiveKeys = ['password', 'token', 'secret', 'key', 'auth']; const sanitized = JSON.parse(JSON.stringify(parameters)); const sanitizeObject = (obj) => { if (typeof obj !== 'object' || obj === null) return; Object.keys(obj).forEach(key => { if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) { obj[key] = '[REDACTED]'; } else if (typeof obj[key] === 'object') { sanitizeObject(obj[key]); } }); }; sanitizeObject(sanitized); return sanitized; } logSecurityEvent(event) { this.securityEvents.push(event); // Keep only last 1000 events to prevent memory issues if (this.securityEvents.length > 1000) { this.securityEvents = this.securityEvents.slice(-1000); } // Log to console for immediate visibility const logLevel = event.riskLevel === 'high' || event.riskLevel === 'critical' ? 'error' : 'info'; console[logLevel](`[Security] ${event.type}: ${event.toolName}`, { user: event.context.userId, session: event.context.sessionId, risk: event.riskLevel, details: event.details }); } /** * Get comprehensive security status overview */ async getSecurityStatus() { const now = Date.now(); const last24Hours = now - (24 * 60 * 60 * 1000); const recentEvents = this.securityEvents.filter(e => e.timestamp >= last24Hours); const denials = recentEvents.filter(e => e.type === 'access_denied').length; const violations = recentEvents.filter(e => e.type === 'policy_violation').length; const approvals = recentEvents.filter(e => e.type === 'access_granted').length; const alerts = []; if (denials > 10) { alerts.push({ severity: 'high', message: `High number of access denials: ${denials} in last 24 hours` }); } if (violations > 5) { alerts.push({ severity: 'critical', message: `Multiple policy violations detected: ${violations}` }); } return { overall: { status: alerts.length === 0 ? 'healthy' : 'attention_required', level: 'high', lastScan: new Date().toISOString(), pendingApprovals: this.pendingApprovals.size }, recentEvents: { total: recentEvents.length, denials, violations, approvals }, policies: { active: this.securityPolicies.size, accessControl: true, humanApproval: true, auditLogging: true, riskAssessment: true }, alerts, recommendations: [ 'Review access denial patterns', 'Update security policies regularly', 'Monitor high-risk operations' ] }; } /** * Get security events with filtering */ getSecurityEvents(filters) { let events = [...this.securityEvents]; // Apply time range filter if (filters.timeRange) { const ranges = { '1h': 60 * 60 * 1000, '24h': 24 * 60 * 60 * 1000, '7d': 7 * 24 * 60 * 60 * 1000, '30d': 30 * 24 * 60 * 60 * 1000 }; const cutoff = Date.now() - (ranges[filters.timeRange] || ranges['24h']); events = events.filter(e => e.timestamp >= cutoff); } // Apply event type filter if (filters.eventTypes && filters.eventTypes.length > 0) { events = events.filter(e => filters.eventTypes.includes(e.type)); } // Apply severity filter if (filters.severity) { const severityLevels = ['low', 'medium', 'high', 'critical']; const minLevel = severityLevels.indexOf(filters.severity); events = events.filter(e => severityLevels.indexOf(e.riskLevel) >= minLevel); } // Sort by timestamp descending return events.sort((a, b) => b.timestamp - a.timestamp); } /** * Configure security policy settings */ async configureSecurityPolicy(config) { // Update approval requirements if (config.requireApprovalFor) { config.requireApprovalFor.forEach(toolName => { const policy = this.securityPolicies.get(toolName); if (policy) { policy.requiresHumanApproval = true; } }); } // Update role definitions (simplified for now) if (config.roles) { console.log('Role definitions updated:', config.roles); } // Update risk thresholds if (config.riskThresholds) { console.log('Risk thresholds updated:', config.riskThresholds); } // Update log level if (config.logLevel) { console.log('Security log level set to:', config.logLevel); } // Log configuration change this.logSecurityEvent({ type: 'policy_violation', toolName: 'configure_security_policy', context: createSecurityContext('system', 'config-update', ['admin'], 'admin', 'internal'), timestamp: Date.now(), details: { config }, riskLevel: 'low' }); } /** * Process approval request */ processApproval(params) { const approval = this.pendingApprovals.get(params.approvalId); if (!approval) { throw new Error(`Approval request ${params.approvalId} not found`); } this.pendingApprovals.delete(params.approvalId); const result = { approvalId: params.approvalId, operation: approval.toolName, decision: params.decision, reason: params.reason, executionResult: params.decision === 'approve' ? { success: true, message: 'Operation approved and executed' } : null }; // Log the approval decision this.logSecurityEvent({ type: params.decision === 'approve' ? 'access_granted' : 'access_denied', toolName: approval.toolName, context: approval.context, timestamp: Date.now(), details: { approvalId: params.approvalId, decision: params.decision, reason: params.reason }, riskLevel: 'medium' }); return result; } /** * Get pending approval requests (optional method for dashboard) */ getPendingApprovals(options) { // This method is optional and can be implemented by extending classes // For now, return a mock implementation const mockApprovals = Array.from(this.pendingApprovals.entries()).map(([id, approval]) => ({ id, toolName: approval.toolName, requestedBy: approval.context.userId || 'unknown', status: approval.status, createdAt: new Date(approval.timestamp).toISOString(), context: approval.parameters })); // Apply filters if provided let filtered = mockApprovals; if (options?.status && options.status !== 'all') { filtered = filtered.filter(a => a.status === options.status); } if (options?.toolName) { filtered = filtered.filter(a => a.toolName === options.toolName); } return Promise.resolve(filtered); } } /** * Create security context from request information */ export function createSecurityContext(userId, sessionId, permissions, roleLevel, origin = 'unknown', ipAddress) { return { userId, sessionId, permissions, roleLevel, origin, timestamp: Date.now(), ipAddress }; } /** * Decorator for automatic security validation */ export function requiresSecurity(requiredPermissions, minimumRoleLevel = 'read') { return function (target, propertyKey, descriptor) { const originalMethod = descriptor.value; descriptor.value = async function (...args) { const securityManager = SecurityManager.getInstance(); // Extract security context from arguments (assume first arg has security info) const context = args[0]?.securityContext; if (!context) { throw ErrorHandler.createValidationError('securityContext', undefined, 'is required for security validation', { tool: propertyKey, module: target.constructor.name }); } const validation = await securityManager.validateToolAccess(propertyKey, context, args[0]); if (!validation.allowed) { if (validation.requiresApproval) { const approvalId = await securityManager.requestHumanApproval(propertyKey, context, args[0], `${target.constructor.name}.${propertyKey} requires approval`); throw ErrorHandler.transformError(new Error(`Human approval required. Approval ID: ${approvalId}`), { tool: propertyKey, module: target.constructor.name }); } throw ErrorHandler.transformError(new Error(`Access denied: ${validation.reason}`), { tool: propertyKey, module: target.constructor.name }); } return await originalMethod.apply(this, args); }; return descriptor; }; } //# sourceMappingURL=security-manager.js.map