@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
782 lines (686 loc) • 22.2 kB
text/typescript
/**
* 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;
};
}