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
JavaScript
/**
* 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