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.

370 lines (369 loc) 14.5 kB
import fs from 'fs/promises'; import fsSync from 'fs'; import path from 'path'; import { getUnifiedSecurityConfig } from './unified-security-config.js'; import logger from '../../../logger.js'; const SYSTEM_DIRECTORY_BLACKLIST = new Set([ '/private/var/spool/postfix', '/private/var/spool/cups', '/private/var/spool/mail', '/private/var/spool/mqueue', '/private/var/db/sudo', '/private/var/db/dslocal', '/private/var/folders', '/private/var/vm', '/private/var/tmp', '/System', '/usr/bin', '/usr/sbin', '/bin', '/sbin', '/private/etc', '/private/var/root', '/private/var/log', '/Library/Application Support', '/Library/Caches', '/Library/Logs', '/Library/Preferences', '/Users/Shared', 'C:\\Windows', 'C:\\Program Files', 'C:\\Program Files (x86)', 'C:\\ProgramData', 'C:\\System Volume Information', 'C:\\$Recycle.Bin', '/proc', '/sys', '/dev', '/boot', '/root', '/var/log', '/var/spool', '/var/cache', '/var/lib', '/etc', '/tmp' ]); const SAFE_FILE_EXTENSIONS = new Set([ '.txt', '.md', '.json', '.yaml', '.yml', '.xml', '.csv', '.js', '.ts', '.jsx', '.tsx', '.vue', '.svelte', '.py', '.java', '.go', '.rs', '.cpp', '.c', '.h', '.hpp', '.html', '.css', '.scss', '.sass', '.less', '.sql', '.sh', '.bat', '.ps1', '.dockerfile', '.gitignore', '.gitattributes', '.editorconfig', '.eslintrc', '.prettierrc', '.babelrc', '.log', '.env' ]); export class FilesystemSecurity { static instance; config; securityMode; constructor(config) { try { const unifiedConfig = getUnifiedSecurityConfig(); const unifiedSecurityConfig = unifiedConfig.getFilesystemSecurityConfig(); this.securityMode = unifiedSecurityConfig.securityMode; this.config = { enablePermissionChecking: unifiedSecurityConfig.enablePermissionChecking, enableBlacklist: unifiedSecurityConfig.enableBlacklist, enableExtensionFiltering: unifiedSecurityConfig.enableExtensionFiltering, maxPathLength: unifiedSecurityConfig.maxPathLength, performanceThresholdMs: unifiedSecurityConfig.performanceThresholdMs, allowedDirectories: unifiedSecurityConfig.allowedDirectories, additionalBlacklistedPaths: [], additionalSafeExtensions: [], ...config }; logger.info({ securityMode: this.securityMode, config: this.config, source: 'unified-security-config' }, 'Filesystem Security initialized from unified configuration'); } catch (error) { logger.warn({ err: error }, 'Unified security config not available, falling back to environment variables'); this.securityMode = process.env.VIBE_TASK_MANAGER_SECURITY_MODE || 'strict'; this.config = { enablePermissionChecking: true, enableBlacklist: true, enableExtensionFiltering: this.securityMode === 'strict', maxPathLength: 4096, performanceThresholdMs: 50, allowedDirectories: [ process.env.VIBE_TASK_MANAGER_READ_DIR || process.cwd(), process.env.VIBE_CODER_OUTPUT_DIR || path.join(process.cwd(), 'VibeCoderOutput') ], additionalBlacklistedPaths: [], additionalSafeExtensions: [], ...config }; logger.info({ securityMode: this.securityMode, config: this.config, source: 'environment-variables' }, 'Filesystem Security initialized from environment variables (fallback)'); } } static getInstance(config) { if (!FilesystemSecurity.instance) { FilesystemSecurity.instance = new FilesystemSecurity(config); } return FilesystemSecurity.instance; } async checkPathSecurity(filePath, operation = 'read') { const startTime = Date.now(); try { if (!filePath || typeof filePath !== 'string') { return { allowed: false, reason: 'Invalid path input', securityViolation: true }; } if (filePath.length > this.config.maxPathLength) { return { allowed: false, reason: 'Path too long', securityViolation: true }; } const normalizedPath = this.normalizePath(filePath); if (this.config.enableBlacklist && this.isBlacklisted(normalizedPath)) { return { allowed: false, reason: 'Path is in system directory blacklist', normalizedPath, securityViolation: true }; } if (!this.isWithinAllowedDirectories(normalizedPath)) { return { allowed: false, reason: 'Path is outside allowed directories', normalizedPath, securityViolation: true }; } if (operation === 'read' && this.config.enableExtensionFiltering) { const ext = path.extname(normalizedPath).toLowerCase(); if (ext && !this.isSafeExtension(ext)) { return { allowed: false, reason: 'File extension not in safe list', normalizedPath, securityViolation: false }; } } if (this.config.enablePermissionChecking) { const permissionResult = await this.checkPermissions(normalizedPath, operation); if (!permissionResult.allowed) { return permissionResult; } } const duration = Date.now() - startTime; if (duration > this.config.performanceThresholdMs) { logger.warn({ filePath, duration, threshold: this.config.performanceThresholdMs }, 'Security check exceeded performance threshold'); } return { allowed: true, normalizedPath }; } catch (error) { logger.error({ err: error, filePath }, 'Error during security check'); return { allowed: false, reason: `Security check failed: ${error instanceof Error ? error.message : String(error)}`, securityViolation: true }; } } async readDirSecure(dirPath) { const securityCheck = await this.checkPathSecurity(dirPath, 'read'); if (!securityCheck.allowed) { throw new Error(`Access denied: ${securityCheck.reason}`); } const securePath = securityCheck.normalizedPath; try { await fs.access(securePath, fsSync.constants.R_OK); const entries = await fs.readdir(securePath, { withFileTypes: true }); logger.debug({ path: securePath, entryCount: entries.length }, 'Directory read successfully'); return entries; } catch (error) { if (error instanceof Error && 'code' in error) { const fsError = error; if (fsError.code === 'ENOENT') { throw new Error(`Directory not found: ${dirPath}`); } else if (fsError.code === 'EACCES') { logger.warn({ path: securePath }, 'Permission denied for directory access'); throw new Error(`Permission denied for directory: ${dirPath}`); } } const errorMessage = error instanceof Error ? error.message : String(error); logger.error({ err: error, path: securePath }, `Error reading directory: ${errorMessage}`); throw new Error(`Could not read directory '${dirPath}': ${errorMessage}`); } } async statSecure(filePath) { const securityCheck = await this.checkPathSecurity(filePath, 'read'); if (!securityCheck.allowed) { throw new Error(`Access denied: ${securityCheck.reason}`); } const securePath = securityCheck.normalizedPath; try { const stats = await fs.stat(securePath); logger.debug({ path: securePath }, 'File stats retrieved successfully'); return stats; } catch (error) { if (error instanceof Error && 'code' in error) { const fsError = error; if (fsError.code === 'ENOENT') { throw new Error(`File not found: ${filePath}`); } else if (fsError.code === 'EACCES') { logger.warn({ path: securePath }, 'Permission denied for file access'); throw new Error(`Permission denied for file: ${filePath}`); } } const errorMessage = error instanceof Error ? error.message : String(error); logger.error({ err: error, path: securePath }, `Error getting file stats: ${errorMessage}`); throw new Error(`Could not get stats for '${filePath}': ${errorMessage}`); } } normalizePath(inputPath) { try { return path.resolve(inputPath); } catch (error) { logger.warn({ inputPath, error }, 'Failed to normalize path'); return inputPath; } } isBlacklisted(normalizedPath) { for (const blacklistedPath of SYSTEM_DIRECTORY_BLACKLIST) { if (this.isPathWithin(normalizedPath, blacklistedPath)) { return true; } } for (const blacklistedPath of this.config.additionalBlacklistedPaths) { if (this.isPathWithin(normalizedPath, blacklistedPath)) { return true; } } return false; } isWithinAllowedDirectories(normalizedPath) { for (const allowedDir of this.config.allowedDirectories) { if (this.isPathWithin(normalizedPath, allowedDir)) { return true; } } return false; } isPathWithin(childPath, parentPath) { const normalizedChild = path.resolve(childPath); const normalizedParent = path.resolve(parentPath); if (normalizedChild === normalizedParent) { return true; } const parentWithSep = normalizedParent.endsWith(path.sep) ? normalizedParent : normalizedParent + path.sep; return normalizedChild.startsWith(parentWithSep); } isSafeExtension(extension) { return SAFE_FILE_EXTENSIONS.has(extension.toLowerCase()) || this.config.additionalSafeExtensions.includes(extension.toLowerCase()); } async checkPermissions(normalizedPath, operation) { try { let accessMode; switch (operation) { case 'read': accessMode = fsSync.constants.R_OK; break; case 'write': accessMode = fsSync.constants.W_OK; break; case 'execute': accessMode = fsSync.constants.X_OK; break; default: accessMode = fsSync.constants.F_OK; } await fs.access(normalizedPath, accessMode); return { allowed: true, normalizedPath }; } catch (error) { if (error instanceof Error && 'code' in error) { const fsError = error; if (fsError.code === 'ENOENT') { return { allowed: false, reason: 'Path does not exist', normalizedPath, securityViolation: false }; } else if (fsError.code === 'EACCES') { return { allowed: false, reason: `Permission denied for ${operation} operation`, normalizedPath, securityViolation: false }; } } return { allowed: false, reason: `Permission check failed: ${error instanceof Error ? error.message : String(error)}`, normalizedPath, securityViolation: true }; } } updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; logger.info({ config: this.config }, 'Filesystem security configuration updated'); } getConfig() { return { ...this.config }; } getSecurityMode() { return this.securityMode; } getSecurityStats() { return { securityMode: this.securityMode, blacklistedPathsCount: SYSTEM_DIRECTORY_BLACKLIST.size + this.config.additionalBlacklistedPaths.length, allowedDirectoriesCount: this.config.allowedDirectories.length, safeExtensionsCount: SAFE_FILE_EXTENSIONS.size + this.config.additionalSafeExtensions.length }; } addToBlacklist(pathToBlock) { const normalizedPath = path.resolve(pathToBlock); if (!this.config.additionalBlacklistedPaths.includes(normalizedPath)) { this.config.additionalBlacklistedPaths.push(normalizedPath); logger.info({ path: normalizedPath }, 'Path added to blacklist'); } } removeFromBlacklist(pathToUnblock) { const normalizedPath = path.resolve(pathToUnblock); const index = this.config.additionalBlacklistedPaths.indexOf(normalizedPath); if (index !== -1) { this.config.additionalBlacklistedPaths.splice(index, 1); logger.info({ path: normalizedPath }, 'Path removed from blacklist'); } } }