UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

782 lines (686 loc) 22.2 kB
/** * Security Manager * Implements MCP Design Guide Section 5.2 principles for zero-trust architecture */ import { ErrorHandler } from './error-handler.js'; export interface SecurityContext { userId?: string; sessionId: string; permissions: string[]; roleLevel: 'read' | 'write' | 'admin' | 'system'; origin: string; timestamp: number; ipAddress?: string; } export interface ToolSecurityPolicy { toolName: string; requiredPermissions: string[]; minimumRoleLevel: SecurityContext['roleLevel']; requiresHumanApproval: boolean; maxUsagePerHour: number; allowedOrigins: string[]; logLevel: 'none' | 'basic' | 'detailed'; } export interface SecurityEvent { type: 'access_granted' | 'access_denied' | 'suspicious_activity' | 'policy_violation'; toolName: string; context: SecurityContext; timestamp: number; details: Record<string, any>; riskLevel: 'low' | 'medium' | 'high' | 'critical'; } /** * Implements zero-trust security model for MCP tool access */ export class SecurityManager { private static instance: SecurityManager; private securityPolicies: Map<string, ToolSecurityPolicy> = new Map(); private securityEvents: SecurityEvent[] = []; private usageTracker: Map<string, { count: number; lastReset: number }> = new Map(); private pendingApprovals: Map<string, any> = new Map(); private constructor() { this.initializeDefaultPolicies(); } static getInstance(): SecurityManager { if (!SecurityManager.instance) { SecurityManager.instance = new SecurityManager(); } return SecurityManager.instance; } /** * Initialize default security policies for critical tools */ private initializeDefaultPolicies(): void { // 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: ToolSecurityPolicy): void { this.securityPolicies.set(policy.toolName, policy); } /** * Validate access to a tool based on security context and policies */ async validateToolAccess( toolName: string, context: SecurityContext, parameters?: any ): Promise<{ allowed: boolean; reason?: string; requiresApproval?: boolean }> { 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: string, context: SecurityContext, parameters: any, justification: string ): Promise<string> { 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(): { totalEvents: number; accessDenied: number; suspiciousActivity: number; highRiskEvents: number; topTargetedTools: Array<{ tool: string; count: number }>; alerts: string[]; } { 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: Record<string, number> = {}; 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: string[] = []; 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 }; } private hasRequiredRoleLevel(userRole: SecurityContext['roleLevel'], requiredRole: SecurityContext['roleLevel']): boolean { const roleHierarchy = ['read', 'write', 'admin', 'system']; const userLevel = roleHierarchy.indexOf(userRole); const requiredLevel = roleHierarchy.indexOf(requiredRole); return userLevel >= requiredLevel; } private checkRateLimit(toolName: string, maxPerHour: number): boolean { 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; } private updateUsageCounter(toolName: string): void { const key = toolName; const usage = this.usageTracker.get(key); if (usage) { usage.count++; } } private detectSuspiciousActivity( toolName: string, context: SecurityContext, parameters: any ): Record<string, any> | null { const suspiciousPatterns: Record<string, any> = {}; // 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; } private isReadOnlyTool(toolName: string): boolean { 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); } private sanitizeParameters(parameters: any): any { if (!parameters) return parameters; const sensitiveKeys = ['password', 'token', 'secret', 'key', 'auth']; const sanitized = JSON.parse(JSON.stringify(parameters)); const sanitizeObject = (obj: any): void => { 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; } private logSecurityEvent(event: SecurityEvent): void { 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(): Promise<any> { 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: { timeRange?: string; eventTypes?: string[]; severity?: string; }): SecurityEvent[] { let events = [...this.securityEvents]; // Apply time range filter if (filters.timeRange) { const ranges: Record<string, number> = { '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: { requireApprovalFor?: string[]; roles?: Record<string, string[]>; riskThresholds?: Record<string, number>; logLevel?: string; }): Promise<void> { // 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: { approvalId: string; decision: 'approve' | 'deny'; reason?: string; }): any { 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?: { status?: string; toolName?: string }): Promise<any[]> { // 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: string | undefined, sessionId: string, permissions: string[], roleLevel: SecurityContext['roleLevel'], origin: string = 'unknown', ipAddress?: string ): SecurityContext { return { userId, sessionId, permissions, roleLevel, origin, timestamp: Date.now(), ipAddress }; } /** * Decorator for automatic security validation */ export function requiresSecurity( requiredPermissions: string[], minimumRoleLevel: SecurityContext['roleLevel'] = 'read' ) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = async function (...args: any[]) { 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; }; }