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.

456 lines (455 loc) 19.3 kB
import path from 'path'; import { extractVibeTaskManagerSecurityConfig } from '../utils/config-loader.js'; import logger from '../../../logger.js'; export class UnifiedSecurityConfigManager { static instance = null; config = null; mcpConfig = null; constructor() { logger.info('Unified Security Configuration Manager initialized'); } static getInstance() { if (!UnifiedSecurityConfigManager.instance) { UnifiedSecurityConfigManager.instance = new UnifiedSecurityConfigManager(); } return UnifiedSecurityConfigManager.instance; } initializeFromMCPConfig(mcpConfig) { this.mcpConfig = mcpConfig; try { const securityConfig = extractVibeTaskManagerSecurityConfig(mcpConfig); this.config = { allowedReadDirectory: securityConfig.allowedReadDirectory, allowedWriteDirectory: securityConfig.allowedWriteDirectory, securityMode: securityConfig.securityMode, allowedDirectories: [ securityConfig.allowedReadDirectory, securityConfig.allowedWriteDirectory ], performanceThresholdMs: 50, enablePermissionChecking: true, enableBlacklist: true, enableExtensionFiltering: securityConfig.securityMode === 'strict', maxPathLength: 4096, allowedDir: securityConfig.allowedReadDirectory, outputDir: securityConfig.allowedWriteDirectory, serviceBoundaries: { vibeTaskManager: { readDir: securityConfig.allowedReadDirectory, writeDir: securityConfig.allowedWriteDirectory }, codeMapGenerator: { allowedDir: securityConfig.allowedReadDirectory, outputDir: securityConfig.allowedWriteDirectory }, contextCurator: { readDir: securityConfig.allowedReadDirectory, outputDir: securityConfig.allowedWriteDirectory } } }; logger.info({ allowedReadDirectory: this.config.allowedReadDirectory, allowedWriteDirectory: this.config.allowedWriteDirectory, securityMode: this.config.securityMode, allowedDirectories: this.config.allowedDirectories }, 'Unified security configuration initialized from MCP client config'); } catch (error) { logger.error({ err: error }, 'Failed to initialize security configuration from MCP client config'); throw error; } } isInitialized() { return this.config !== null; } getConfig() { if (!this.config) { throw new Error('Unified security configuration not initialized. Call initializeFromMCPConfig() first.'); } return { ...this.config }; } getFilesystemSecurityConfig() { const config = this.getConfig(); return { allowedDirectories: config.allowedDirectories, securityMode: config.securityMode, enablePermissionChecking: config.enablePermissionChecking, enableBlacklist: config.enableBlacklist, enableExtensionFiltering: config.enableExtensionFiltering, maxPathLength: config.maxPathLength, performanceThresholdMs: config.performanceThresholdMs }; } getPathValidatorConfig() { const config = this.getConfig(); return { allowedDirectories: config.allowedDirectories, maxPathLength: config.maxPathLength }; } getSecurityManagerConfig() { const config = this.getConfig(); return { pathSecurity: { allowedDirectories: config.allowedDirectories }, strictMode: config.securityMode === 'strict', performanceThresholdMs: config.performanceThresholdMs }; } getCodeMapGeneratorConfig() { const config = this.getConfig(); return { allowedDir: config.allowedReadDirectory, outputDir: config.allowedWriteDirectory, securityMode: config.securityMode }; } getContextCuratorConfig() { const config = this.getConfig(); return { readDir: config.allowedReadDirectory, outputDir: config.allowedWriteDirectory, allowedDirectories: config.allowedDirectories, securityMode: config.securityMode }; } getVibeTaskManagerSecurityValidatorConfig() { const config = this.getConfig(); return { readDir: config.allowedReadDirectory, writeDir: config.allowedWriteDirectory, securityMode: config.securityMode }; } getServiceBoundaries(serviceName) { const config = this.getConfig(); const boundaries = config.serviceBoundaries[serviceName]; if (!boundaries) { throw new Error(`Service boundaries not found for service: ${serviceName}`); } return boundaries; } isPathAllowed(filePath, operation = 'read') { const config = this.getConfig(); try { const resolvedPath = path.resolve(filePath); if (operation === 'read') { return resolvedPath.startsWith(config.allowedReadDirectory); } else { return resolvedPath.startsWith(config.allowedWriteDirectory); } } catch (error) { logger.error({ err: error, filePath, operation }, 'Error validating path'); return false; } } normalizePath(inputPath) { if (!inputPath || typeof inputPath !== 'string') { throw new Error('Path cannot be empty, undefined, or non-string'); } try { const sanitized = inputPath.replace(/[<>:"|?*\x00-\x1f]/g, ''); const normalizedPath = path.normalize(sanitized); const isTestMode = process.env.NODE_ENV === 'test'; if (isTestMode && (normalizedPath.includes('/tmp/') || normalizedPath.includes('temp/'))) { if (path.isAbsolute(normalizedPath)) { logger.debug(`Using test path as-is: ${normalizedPath}`); return normalizedPath; } } return path.isAbsolute(normalizedPath) ? normalizedPath : path.resolve(normalizedPath); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Path normalization failed: ${errorMessage}`); } } isPathWithin(childPath, parentPath) { try { const normalizedChild = this.normalizePath(childPath); const normalizedParent = this.normalizePath(parentPath); if (normalizedChild === normalizedParent) { return true; } const separator = path.sep; const parentWithSep = normalizedParent.endsWith(separator) ? normalizedParent : normalizedParent + separator; return normalizedChild.startsWith(parentWithSep); } catch (error) { logger.error({ err: error, childPath, parentPath }, 'Error checking path containment'); return false; } } validatePathSecurity(inputPath, options = {}) { const { operation = 'read', allowTestMode = true, checkExtensions = false, allowedExtensions = ['.md', '.json', '.txt', '.yaml', '.yml', '.js', '.ts'], strictMode = true } = options; try { const config = this.getConfig(); const isTestMode = process.env.NODE_ENV === 'test'; if (!inputPath || typeof inputPath !== 'string' || inputPath.trim() === '') { return { isValid: false, error: 'Path cannot be empty, undefined, or non-string', violationType: 'invalid_path' }; } const dangerousChars = /[<>:"|?*\x00-\x1f]/; if (strictMode && dangerousChars.test(inputPath)) { return { isValid: false, error: `Path contains dangerous characters: ${inputPath}`, violationType: 'dangerous_characters' }; } if (inputPath.includes('..') && strictMode) { const normalizedTest = path.normalize(inputPath); if (normalizedTest.includes('..')) { return { isValid: false, error: `Path traversal detected: ${inputPath}`, violationType: 'path_traversal' }; } } let normalizedPath; try { normalizedPath = this.normalizePath(inputPath); } catch (error) { return { isValid: false, error: `Path normalization failed: ${error instanceof Error ? error.message : String(error)}`, violationType: 'invalid_path' }; } if (normalizedPath.length > config.maxPathLength) { const multiplier = isTestMode && allowTestMode ? 2 : 1; if (normalizedPath.length > config.maxPathLength * multiplier) { return { isValid: false, error: `Path length ${normalizedPath.length} exceeds maximum ${config.maxPathLength * multiplier}`, violationType: 'invalid_path' }; } } if (checkExtensions) { const ext = path.extname(normalizedPath).toLowerCase(); if (ext && !allowedExtensions.includes(ext)) { if (!(isTestMode && allowTestMode)) { return { isValid: false, error: `File extension '${ext}' not allowed. Allowed extensions: ${allowedExtensions.join(', ')}`, violationType: 'invalid_extension' }; } } } const allowedDirectory = operation === 'read' ? config.allowedReadDirectory : config.allowedWriteDirectory; if (!allowedDirectory) { return { isValid: false, error: `Security boundary not configured for ${operation} operations`, violationType: 'outside_boundary' }; } if (!this.isPathWithin(normalizedPath, allowedDirectory)) { if (isTestMode && allowTestMode) { const testPaths = ['/tmp', path.join(process.cwd(), '__tests__'), path.join(process.cwd(), 'test')]; const isTestPath = testPaths.some(testPath => this.isPathWithin(normalizedPath, testPath)); if (isTestPath) { logger.debug(`Allowing test path: ${normalizedPath}`); return { isValid: true, normalizedPath, warnings: [`Test mode: Path outside normal boundaries but within test paths`] }; } } return { isValid: false, error: `Access denied: Path '${inputPath}' is outside the allowed ${operation} directory '${allowedDirectory}'`, violationType: 'outside_boundary' }; } return { isValid: true, normalizedPath }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error({ err: error, inputPath, options }, 'Unexpected error during path validation'); return { isValid: false, error: `Validation error: ${errorMessage}`, violationType: 'invalid_path' }; } } createSecurePath(inputPath, operation = 'read', options = {}) { const validation = this.validatePathSecurity(inputPath, { ...options, operation }); if (!validation.isValid) { const errorMsg = `Security violation: ${validation.error}`; logger.error({ inputPath, operation, validation }, errorMsg); throw new Error(errorMsg); } return validation.normalizedPath; } isPathAllowedForOperation(inputPath, operation, options = {}) { return this.validatePathSecurity(inputPath, { ...options, operation }); } validateMultiplePaths(paths, operation = 'read', options = {}) { const results = new Map(); for (const inputPath of paths) { try { const result = this.validatePathSecurity(inputPath, { ...options, operation }); results.set(inputPath, result); } catch (error) { results.set(inputPath, { isValid: false, error: `Batch validation error: ${error instanceof Error ? error.message : String(error)}`, violationType: 'invalid_path' }); } } return results; } getConfigStatus() { return { initialized: this.config !== null, mcpConfigPresent: this.mcpConfig !== null, allowedReadDirectory: this.config?.allowedReadDirectory, allowedWriteDirectory: this.config?.allowedWriteDirectory, securityMode: this.config?.securityMode }; } validatePathSecurityCompat(inputPath, allowedDirectory) { if (allowedDirectory) { logger.warn({ inputPath, providedAllowedDirectory: allowedDirectory, centralizedDirectory: this.getConfig().allowedReadDirectory }, 'Path validation using deprecated allowedDirectory parameter. Consider using centralized configuration.'); } const result = this.validatePathSecurity(inputPath, { operation: 'read' }); return { isValid: result.isValid, normalizedPath: result.normalizedPath, error: result.error }; } createSecureReadPath(filePath) { return this.createSecurePath(filePath, 'read'); } createSecureWritePath(filePath) { return this.createSecurePath(filePath, 'write'); } isPathWithinReadDirectory(filePath) { const result = this.isPathAllowedForOperation(filePath, 'read'); return result.isValid; } isPathWithinWriteDirectory(filePath) { const result = this.isPathAllowedForOperation(filePath, 'write'); return result.isValid; } validatePathWithConfig(inputPath, config) { const options = { operation: 'read', checkExtensions: true, allowedExtensions: config?.allowedExtensions, strictMode: config?.strictMode ?? true }; return this.validatePathSecurity(inputPath, options); } reset() { this.config = null; this.mcpConfig = null; logger.debug('Unified security configuration reset'); } getToolOutputDirectory() { const config = this.getConfig(); return config.allowedWriteDirectory; } createSecureToolOutputPath(relativePath) { const config = this.getConfig(); const outputPath = path.join(config.allowedWriteDirectory, relativePath); return this.createSecurePath(outputPath, 'write'); } async ensureToolOutputDirectory(toolName) { const config = this.getConfig(); const toolDir = path.join(config.allowedWriteDirectory, toolName); const validation = this.validatePathSecurity(toolDir, { operation: 'write' }); if (!validation.isValid) { throw new Error(`Invalid tool directory: ${validation.error}`); } try { const fs = await import('fs-extra'); await fs.ensureDir(toolDir); logger.debug({ toolName, toolDir }, 'Ensured tool output directory exists'); return toolDir; } catch (error) { logger.error({ err: error, toolName, toolDir }, 'Failed to ensure tool output directory'); throw new Error(`Failed to create tool output directory: ${error instanceof Error ? error.message : String(error)}`); } } getEnvironmentVariable(varName, fallback) { return process.env[varName] || fallback; } } export function getUnifiedSecurityConfig() { return UnifiedSecurityConfigManager.getInstance(); } export function validatePathSecurity(inputPath, options = {}) { return getUnifiedSecurityConfig().validatePathSecurity(inputPath, options); } export function createSecurePath(inputPath, operation = 'read', options = {}) { return getUnifiedSecurityConfig().createSecurePath(inputPath, operation, options); } export function normalizePath(inputPath) { return getUnifiedSecurityConfig().normalizePath(inputPath); } export function isPathWithin(childPath, parentPath) { return getUnifiedSecurityConfig().isPathWithin(childPath, parentPath); } export function isPathAllowed(inputPath, operation = 'read', options = {}) { const result = getUnifiedSecurityConfig().isPathAllowedForOperation(inputPath, operation, options); return result.isValid; } export function validateMultiplePaths(paths, operation = 'read', options = {}) { return getUnifiedSecurityConfig().validateMultiplePaths(paths, operation, options); } export function validatePathSecurityCompat(inputPath, allowedDirectory) { return getUnifiedSecurityConfig().validatePathSecurityCompat(inputPath, allowedDirectory); } export function createSecureReadPath(filePath) { return getUnifiedSecurityConfig().createSecureReadPath(filePath); } export function createSecureWritePath(filePath) { return getUnifiedSecurityConfig().createSecureWritePath(filePath); } export function isPathWithinReadDirectory(filePath) { return getUnifiedSecurityConfig().isPathWithinReadDirectory(filePath); } export function isPathWithinWriteDirectory(filePath) { return getUnifiedSecurityConfig().isPathWithinWriteDirectory(filePath); } export function getToolOutputDirectory() { return getUnifiedSecurityConfig().getToolOutputDirectory(); } export function createSecureToolOutputPath(relativePath) { return getUnifiedSecurityConfig().createSecureToolOutputPath(relativePath); } export async function ensureToolOutputDirectory(toolName) { return getUnifiedSecurityConfig().ensureToolOutputDirectory(toolName); }