UNPKG

vineguard-mcp-server-standalone

Version:

VineGuard MCP Server v2.1 - Intelligent QA Workflow System with advanced test generation for Jest/RTL, Cypress, and Playwright. Features smart project analysis, progressive testing strategies, and comprehensive quality patterns for React/Vue/Angular proje

332 lines 14.2 kB
/** * Input validation and sanitization module for VineGuard MCP Server * Prevents path traversal attacks, injection attacks, and malformed inputs */ import * as path from 'path'; import * as fs from 'fs/promises'; export class InputValidator { static MAX_PATH_LENGTH = 4096; static MAX_STRING_LENGTH = 10000; static ALLOWED_FILE_EXTENSIONS = [ '.js', '.ts', '.jsx', '.tsx', '.vue', '.svelte', '.json', '.md', '.txt', '.css', '.scss', '.less', '.html', '.test.js', '.test.ts', '.spec.js', '.spec.ts' ]; /** * Validate and sanitize file paths to prevent path traversal attacks */ static validateFilePath(filePath, projectRoot) { if (!filePath || typeof filePath !== 'string') { return { isValid: false, error: 'File path must be a non-empty string' }; } // Check length if (filePath.length > this.MAX_PATH_LENGTH) { return { isValid: false, error: 'File path too long' }; } // Remove any null bytes const sanitizedPath = filePath.replace(/\0/g, ''); // Check for dangerous patterns const dangerousPatterns = [ /\.\./, // Path traversal /\/\//, // Double slashes /\\/, // Backslashes (Windows path separators) /^\/etc/, // System directories /^\/proc/, // System directories /^\/sys/, // System directories /^\/root/, // Root home directory /node_modules/, // Node modules (usually not user code) ]; for (const pattern of dangerousPatterns) { if (pattern.test(sanitizedPath)) { return { isValid: false, error: `Dangerous path pattern detected: ${pattern}` }; } } // Resolve and normalize the path let resolvedPath; try { if (projectRoot) { resolvedPath = path.resolve(projectRoot, sanitizedPath); // Ensure the resolved path is within the project root const normalizedRoot = path.normalize(projectRoot); const normalizedPath = path.normalize(resolvedPath); if (!normalizedPath.startsWith(normalizedRoot)) { return { isValid: false, error: 'Path is outside project root' }; } } else { resolvedPath = path.resolve(sanitizedPath); } } catch (error) { return { isValid: false, error: 'Invalid path format' }; } // Check file extension const ext = path.extname(resolvedPath).toLowerCase(); if (ext && !this.ALLOWED_FILE_EXTENSIONS.includes(ext)) { return { isValid: false, error: `File extension '${ext}' not allowed. Allowed: ${this.ALLOWED_FILE_EXTENSIONS.join(', ')}` }; } return { isValid: true, sanitizedValue: resolvedPath }; } /** * Validate string inputs for command injection */ static validateString(input, maxLength = this.MAX_STRING_LENGTH) { if (typeof input !== 'string') { return { isValid: false, error: 'Input must be a string' }; } if (input.length > maxLength) { return { isValid: false, error: `String too long (max ${maxLength} characters)` }; } // Remove null bytes const sanitized = input.replace(/\0/g, ''); // Check for command injection patterns const dangerousPatterns = [ /[;&|`$()]/, // Shell metacharacters /\$\(/, // Command substitution /`.*`/, // Backticks /eval\s*\(/, // eval() calls /require\s*\(/, // require() calls /import\s*\(/, // dynamic imports /process\./, // Process object access /global\./, // Global object access /__dirname/, // Node.js globals /__filename/, // Node.js globals ]; for (const pattern of dangerousPatterns) { if (pattern.test(sanitized)) { return { isValid: false, error: `Potentially dangerous pattern detected: ${pattern}` }; } } return { isValid: true, sanitizedValue: sanitized }; } /** * Validate boolean inputs */ static validateBoolean(input) { if (typeof input === 'boolean') { return { isValid: true, sanitizedValue: input }; } if (typeof input === 'string') { const lower = input.toLowerCase(); if (lower === 'true' || lower === '1' || lower === 'yes') { return { isValid: true, sanitizedValue: true }; } if (lower === 'false' || lower === '0' || lower === 'no') { return { isValid: true, sanitizedValue: false }; } } if (typeof input === 'number') { return { isValid: true, sanitizedValue: input !== 0 }; } return { isValid: false, error: 'Input must be a boolean or boolean-like value' }; } /** * Validate number inputs */ static validateNumber(input, min, max) { let num; if (typeof input === 'number') { num = input; } else if (typeof input === 'string') { num = parseFloat(input); if (isNaN(num)) { return { isValid: false, error: 'Input must be a valid number' }; } } else { return { isValid: false, error: 'Input must be a number or numeric string' }; } if (!isFinite(num)) { return { isValid: false, error: 'Number must be finite' }; } if (min !== undefined && num < min) { return { isValid: false, error: `Number must be at least ${min}` }; } if (max !== undefined && num > max) { return { isValid: false, error: `Number must be at most ${max}` }; } return { isValid: true, sanitizedValue: num }; } /** * Validate array inputs */ static validateArray(input, allowedValues) { if (!Array.isArray(input)) { return { isValid: false, error: 'Input must be an array' }; } const sanitizedArray = []; for (const item of input) { const stringResult = this.validateString(String(item)); if (!stringResult.isValid) { return { isValid: false, error: `Invalid array item: ${stringResult.error}` }; } const sanitizedItem = stringResult.sanitizedValue; if (allowedValues && !allowedValues.includes(sanitizedItem)) { return { isValid: false, error: `Array item '${sanitizedItem}' not allowed. Allowed: ${allowedValues.join(', ')}` }; } sanitizedArray.push(sanitizedItem); } return { isValid: true, sanitizedValue: sanitizedArray }; } /** * Validate test framework names */ static validateTestFramework(framework) { const allowedFrameworks = ['jest', 'vitest', 'playwright', 'cypress', 'npm']; return this.validateArray([framework], allowedFrameworks).isValid ? { isValid: true, sanitizedValue: framework } : { isValid: false, error: `Invalid framework '${framework}'. Allowed: ${allowedFrameworks.join(', ')}` }; } /** * Validate test type */ static validateTestType(testType) { const allowedTypes = ['unit', 'integration', 'e2e', 'component', 'visual', 'performance', 'accessibility']; return this.validateArray([testType], allowedTypes).isValid ? { isValid: true, sanitizedValue: testType } : { isValid: false, error: `Invalid test type '${testType}'. Allowed: ${allowedTypes.join(', ')}` }; } /** * Validate severity levels */ static validateSeverity(severity) { const allowedSeverities = ['all', 'critical', 'high', 'medium', 'low']; return this.validateArray([severity], allowedSeverities).isValid ? { isValid: true, sanitizedValue: severity } : { isValid: false, error: `Invalid severity '${severity}'. Allowed: ${allowedSeverities.join(', ')}` }; } /** * Validate project type */ static validateProjectType(projectType) { const allowedTypes = ['react', 'vue', 'angular', 'svelte', 'astro', 'next', 'node']; return this.validateArray([projectType], allowedTypes).isValid ? { isValid: true, sanitizedValue: projectType } : { isValid: false, error: `Invalid project type '${projectType}'. Allowed: ${allowedTypes.join(', ')}` }; } /** * Check if a file exists safely */ static async fileExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } /** * Validate and sanitize all tool arguments */ static validateToolArgs(toolName, args) { if (!args || typeof args !== 'object') { return { isValid: true, sanitizedValue: {} }; } const sanitizedArgs = {}; try { switch (toolName) { case 'scan_project': if (args.path) { const pathResult = this.validateFilePath(args.path); if (!pathResult.isValid) return pathResult; sanitizedArgs.path = pathResult.sanitizedValue; } if (args.deep !== undefined) { const boolResult = this.validateBoolean(args.deep); if (!boolResult.isValid) return boolResult; sanitizedArgs.deep = boolResult.sanitizedValue; } break; case 'generate_test': if (args.filePath) { const pathResult = this.validateFilePath(args.filePath); if (!pathResult.isValid) return pathResult; sanitizedArgs.filePath = pathResult.sanitizedValue; } if (args.testType) { const typeResult = this.validateTestType(args.testType); if (!typeResult.isValid) return typeResult; sanitizedArgs.testType = typeResult.sanitizedValue; } if (args.framework) { const frameworkResult = this.validateTestFramework(args.framework); if (!frameworkResult.isValid) return frameworkResult; sanitizedArgs.framework = frameworkResult.sanitizedValue; } break; case 'run_tests': if (args.framework) { const frameworkResult = this.validateTestFramework(args.framework); if (!frameworkResult.isValid) return frameworkResult; sanitizedArgs.framework = frameworkResult.sanitizedValue; } if (args.coverage !== undefined) { const boolResult = this.validateBoolean(args.coverage); if (!boolResult.isValid) return boolResult; sanitizedArgs.coverage = boolResult.sanitizedValue; } if (args.watch !== undefined) { const boolResult = this.validateBoolean(args.watch); if (!boolResult.isValid) return boolResult; sanitizedArgs.watch = boolResult.sanitizedValue; } break; case 'analyze_prd': if (args.prdPath) { const pathResult = this.validateFilePath(args.prdPath); if (!pathResult.isValid) return pathResult; sanitizedArgs.prdPath = pathResult.sanitizedValue; } break; case 'detect_bugs': if (args.scanPath) { const pathResult = this.validateFilePath(args.scanPath); if (!pathResult.isValid) return pathResult; sanitizedArgs.scanPath = pathResult.sanitizedValue; } if (args.severity) { const severityResult = this.validateSeverity(args.severity); if (!severityResult.isValid) return severityResult; sanitizedArgs.severity = severityResult.sanitizedValue; } break; default: // For unknown tools, apply basic string validation to all string values for (const [key, value] of Object.entries(args)) { if (typeof value === 'string') { const stringResult = this.validateString(value); if (!stringResult.isValid) return stringResult; sanitizedArgs[key] = stringResult.sanitizedValue; } else { sanitizedArgs[key] = value; } } } return { isValid: true, sanitizedValue: sanitizedArgs }; } catch (error) { return { isValid: false, error: `Validation error: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } } //# sourceMappingURL=input-validator.js.map