UNPKG

vineguard-utils

Version:

Shared utilities for VineGuard - AI-powered testing orchestration

354 lines 14.8 kB
/** * Security utilities for VineGuard * Provides input sanitization, validation, and security configuration */ import { createPackageErrorHandler, ValidationError } from './errors.js'; const errorHandler = createPackageErrorHandler('security'); export const DEFAULT_SECURITY_CONFIG = { maxFileSize: 50 * 1024 * 1024, // 50MB allowedFileExtensions: [ '.ts', '.js', '.tsx', '.jsx', '.json', '.md', '.yml', '.yaml', '.spec.ts', '.spec.js', '.test.ts', '.test.js', '.cy.ts', '.cy.js' ], maxPathDepth: 10, allowedProtocols: ['http:', 'https:', 'file:'], rateLimits: { maxRequestsPerMinute: 100, maxRequestsPerHour: 1000 }, timeouts: { defaultTimeoutMs: 30000, // 30 seconds maxTimeoutMs: 300000 // 5 minutes } }; // Input sanitization export class InputSanitizer { static HTML_ESCAPE_MAP = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;', '/': '&#x2F;' }; static SQL_INJECTION_PATTERNS = [ /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|EXECUTE)\b)/gi, /(--|\/\*|\*\/|;)/g, /(\bUNION\b.*\bSELECT\b)/gi, /(\bOR\b.*=.*\bOR\b)/gi, /('.*OR.*'.*=')/gi ]; static SCRIPT_INJECTION_PATTERNS = [ /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, /javascript:/gi, /vbscript:/gi, /on\w+\s*=/gi, /<iframe\b[^>]*>/gi, /<embed\b[^>]*>/gi, /<object\b[^>]*>/gi ]; static sanitizeHtml(input) { return errorHandler.safeExecuteSync(() => { errorHandler.validator.string(input, 'input'); return input.replace(/[&<>"'\/]/g, (char) => this.HTML_ESCAPE_MAP[char] || char); }); } static sanitizePath(input) { return errorHandler.safeExecuteSync(() => { errorHandler.validator.string(input, 'input'); // Remove potentially dangerous characters let sanitized = input .replace(/\.\./g, '') // Remove directory traversal .replace(/[<>:"|?*]/g, '') // Remove invalid filename chars .replace(/[\x00-\x1f\x80-\x9f]/g, '') // Remove control chars .trim(); // Normalize path separators sanitized = sanitized.replace(/[\\\/]+/g, '/'); // Remove leading/trailing slashes for relative paths if (sanitized.startsWith('./') || sanitized.startsWith('../')) { // Keep relative path indicators } else { sanitized = sanitized.replace(/^\/+/, ''); } if (sanitized.length === 0) { throw new ValidationError('Path cannot be empty after sanitization', 'path', input); } return sanitized; }); } static detectSqlInjection(input) { return this.SQL_INJECTION_PATTERNS.some(pattern => pattern.test(input)); } static detectScriptInjection(input) { return this.SCRIPT_INJECTION_PATTERNS.some(pattern => pattern.test(input)); } static sanitizeCommand(input) { return errorHandler.safeExecuteSync(() => { errorHandler.validator.string(input, 'input'); // Check for potentially dangerous patterns const dangerousPatterns = [ /[;&|`$(){}[\]]/g, // Shell metacharacters /\$\([^)]*\)/g, // Command substitution /`[^`]*`/g, // Backtick command substitution /(^|\s)(sudo|su|rm|rm\s+-rf|mkfs|dd|:\(\)\{)/gi // Dangerous commands ]; for (const pattern of dangerousPatterns) { if (pattern.test(input)) { throw new ValidationError('Command contains potentially dangerous characters or patterns', 'command', input); } } // Basic sanitization return input .trim() .replace(/\s+/g, ' ') // Normalize whitespace .substring(0, 1000); // Limit length }); } static sanitizeEnvironmentValue(input) { return errorHandler.safeExecuteSync(() => { errorHandler.validator.string(input, 'input'); // Remove potentially dangerous characters for environment variables return input .replace(/[\x00-\x1f\x7f-\x9f]/g, '') // Remove control characters .replace(/["`$\\]/g, '') // Remove shell special characters .trim() .substring(0, 1000); // Limit length }); } } // Security validator export class SecurityValidator { static config = DEFAULT_SECURITY_CONFIG; static setConfig(config) { this.config = { ...DEFAULT_SECURITY_CONFIG, ...config }; } static getConfig() { return { ...this.config }; } static validateFileSize(size) { errorHandler.safeExecuteSync(() => { errorHandler.validator.number(size, 'fileSize'); if (size < 0) { throw new ValidationError('File size cannot be negative', 'fileSize', size); } if (size > this.config.maxFileSize) { throw new ValidationError(`File size ${size} exceeds maximum allowed size ${this.config.maxFileSize}`, 'fileSize', size); } }); } static validateFileExtension(filename) { errorHandler.safeExecuteSync(() => { errorHandler.validator.string(filename, 'filename'); const extension = filename.toLowerCase().split('.').pop(); if (!extension) { throw new ValidationError('File must have an extension', 'filename', filename); } const fullExtension = `.${extension}`; const isSpecialTest = filename.includes('.spec.') || filename.includes('.test.') || filename.includes('.cy.'); if (isSpecialTest) { // For test files, check if the base extension is allowed const parts = filename.toLowerCase().split('.'); if (parts.length >= 2) { const baseExtension = `.${parts[parts.length - 1]}`; if (this.config.allowedFileExtensions.includes(baseExtension)) { return; // Valid test file } } } if (!this.config.allowedFileExtensions.includes(fullExtension)) { throw new ValidationError(`File extension ${fullExtension} is not allowed`, 'filename', filename); } }); } static validatePathDepth(filePath) { errorHandler.safeExecuteSync(() => { errorHandler.validator.string(filePath, 'filePath'); const sanitizedPath = InputSanitizer.sanitizePath(filePath); const depth = sanitizedPath.split('/').filter(part => part.length > 0).length; if (depth > this.config.maxPathDepth) { throw new ValidationError(`Path depth ${depth} exceeds maximum allowed depth ${this.config.maxPathDepth}`, 'filePath', filePath); } }); } static validateUrl(url) { errorHandler.safeExecuteSync(() => { errorHandler.validator.string(url, 'url'); errorHandler.validator.url(url, 'url'); const parsedUrl = new URL(url); if (!this.config.allowedProtocols.includes(parsedUrl.protocol)) { throw new ValidationError(`Protocol ${parsedUrl.protocol} is not allowed`, 'url', url); } // Check for localhost in production if (process.env.NODE_ENV === 'production') { const hostname = parsedUrl.hostname.toLowerCase(); if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') { throw new ValidationError('Localhost URLs are not allowed in production', 'url', url); } } // Check for private IP ranges in production if (process.env.NODE_ENV === 'production') { const ip = parsedUrl.hostname; if (this.isPrivateIP(ip)) { throw new ValidationError('Private IP addresses are not allowed in production', 'url', url); } } }); } static validateTimeout(timeoutMs) { errorHandler.safeExecuteSync(() => { errorHandler.validator.number(timeoutMs, 'timeout'); if (timeoutMs <= 0) { throw new ValidationError('Timeout must be positive', 'timeout', timeoutMs); } if (timeoutMs > this.config.timeouts.maxTimeoutMs) { throw new ValidationError(`Timeout ${timeoutMs}ms exceeds maximum allowed timeout ${this.config.timeouts.maxTimeoutMs}ms`, 'timeout', timeoutMs); } }); } static isPrivateIP(ip) { const privateRanges = [ /^10\./, /^172\.(1[6-9]|2[0-9]|3[0-1])\./, /^192\.168\./, /^169\.254\./, // Link-local /^fc00::/, // IPv6 private /^fd00::/, // IPv6 private /^fe80::/ // IPv6 link-local ]; return privateRanges.some(range => range.test(ip)); } } // Rate limiter export class RateLimiter { static requests = new Map(); static hourlyRequests = new Map(); static isAllowed(identifier) { return errorHandler.safeExecuteSync(() => { const now = Date.now(); const config = SecurityValidator.getConfig(); // Check minute limit const minuteKey = `${identifier}:${Math.floor(now / 60000)}`; const minuteData = this.requests.get(minuteKey) || { count: 0, resetTime: now + 60000 }; if (minuteData.count >= config.rateLimits.maxRequestsPerMinute) { return false; } // Check hourly limit const hourKey = `${identifier}:${Math.floor(now / 3600000)}`; const hourData = this.hourlyRequests.get(hourKey) || { count: 0, resetTime: now + 3600000 }; if (hourData.count >= config.rateLimits.maxRequestsPerHour) { return false; } // Update counts minuteData.count++; hourData.count++; this.requests.set(minuteKey, minuteData); this.hourlyRequests.set(hourKey, hourData); // Cleanup old entries this.cleanup(); return true; }); } static cleanup() { const now = Date.now(); // Clean up minute entries for (const [key, data] of this.requests.entries()) { if (data.resetTime < now) { this.requests.delete(key); } } // Clean up hourly entries for (const [key, data] of this.hourlyRequests.entries()) { if (data.resetTime < now) { this.hourlyRequests.delete(key); } } } static getRemainingRequests(identifier) { const now = Date.now(); const config = SecurityValidator.getConfig(); const minuteKey = `${identifier}:${Math.floor(now / 60000)}`; const hourKey = `${identifier}:${Math.floor(now / 3600000)}`; const minuteData = this.requests.get(minuteKey) || { count: 0, resetTime: 0 }; const hourData = this.hourlyRequests.get(hourKey) || { count: 0, resetTime: 0 }; return { minute: Math.max(0, config.rateLimits.maxRequestsPerMinute - minuteData.count), hour: Math.max(0, config.rateLimits.maxRequestsPerHour - hourData.count) }; } } // Environment variable validator export class EnvValidator { static validateRequired(envVars) { errorHandler.safeExecuteSync(() => { const missing = []; for (const envVar of envVars) { if (!process.env[envVar]) { missing.push(envVar); } } if (missing.length > 0) { throw new ValidationError(`Missing required environment variables: ${missing.join(', ')}`, 'environmentVariables', missing); } }); } static sanitizeEnvVar(value) { if (!value) return undefined; return InputSanitizer.sanitizeEnvironmentValue(value); } static validateAndSanitize(envVars) { return errorHandler.safeExecuteSync(() => { const result = {}; const required = []; for (const [key, options] of Object.entries(envVars)) { if (options.required && !process.env[key]) { required.push(key); continue; } let value = process.env[key]; if (value && options.sanitize) { value = this.sanitizeEnvVar(value); } result[key] = value; } if (required.length > 0) { throw new ValidationError(`Missing required environment variables: ${required.join(', ')}`, 'environmentVariables', required); } return result; }); } } // Production security checks export class ProductionSecurity { static performSecurityChecks() { if (process.env.NODE_ENV !== 'production') { return; // Only run in production } errorHandler.safeExecuteSync(() => { // Check for development artifacts if (process.env.DEBUG === 'true' || process.env.NODE_ENV === 'development') { throw new ValidationError('Development settings detected in production environment', 'environment', { DEBUG: process.env.DEBUG, NODE_ENV: process.env.NODE_ENV }); } // Validate critical environment variables EnvValidator.validateRequired([ 'NODE_ENV' ]); // Check for insecure configurations if (process.env.VINEGUARD_ALLOW_INSECURE === 'true') { throw new ValidationError('Insecure configuration detected in production', 'security', 'VINEGUARD_ALLOW_INSECURE is enabled'); } }); } static getSecurityHeaders() { return { 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'DENY', 'X-XSS-Protection': '1; mode=block', 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', 'Content-Security-Policy': "default-src 'self'", 'Referrer-Policy': 'strict-origin-when-cross-origin' }; } } //# sourceMappingURL=security.js.map