UNPKG

adpa-enterprise-framework-automation

Version:

Modular, standards-compliant Node.js/TypeScript automation framework for enterprise requirements, project, and data management. Provides CLI and API for BABOK v3, PMBOK 7th Edition, and DMBOK 2.0 (in progress). Production-ready Express.js API with TypeSpe

619 lines • 21 kB
/** * Input Validation Service for Interactive CLI * * Provides comprehensive input validation and sanitization for all user inputs * in the interactive CLI system. Ensures robust user experience by preventing * invalid inputs and providing helpful error messages. * * @version 1.0.0 * @author ADPA Team */ export class InputValidationService { static errorHistory = []; static MAX_ERROR_HISTORY = 100; /** * Enhanced validation with timeout and retry support */ static async validateWithRetry(input, validator, options = {}) { const { maxRetries = 3, retryMessage = 'Please try again', fieldName = 'input' } = options; let attempts = 0; while (attempts < maxRetries) { attempts++; const result = validator(input); if (result.isValid) { return { isValid: true, value: result.sanitizedValue, attempts }; } if (attempts < maxRetries) { console.log(`āŒ ${this.formatValidationError(result)}`); console.log(`šŸ’” ${retryMessage} (Attempt ${attempts}/${maxRetries})`); } } return { isValid: false, attempts }; } /** * Validate input with timeout */ static validateWithTimeout(input, validator, timeoutMs = 30000) { return new Promise((resolve) => { const timer = setTimeout(() => { resolve({ isValid: false, error: 'Input validation timed out', suggestions: ['Please try again with valid input'] }); }, timeoutMs); try { const result = validator(input); clearTimeout(timer); resolve(result); } catch (error) { clearTimeout(timer); resolve({ isValid: false, error: `Validation error: ${error instanceof Error ? error.message : String(error)}`, suggestions: ['Please check your input and try again'] }); } }); } /** * Enhanced input sanitization with security measures */ static sanitizeInputSecure(input) { if (!input || typeof input !== 'string') { return ''; } return input .trim() // Remove potential script injection attempts .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') // Remove HTML tags .replace(/<[^>]*>/g, '') // Remove null bytes .replace(/\0/g, '') // Normalize whitespace .replace(/\s+/g, ' ') // Limit length to prevent buffer overflow .substring(0, 1000); } /** * Validate menu choice input */ static validateMenuChoice(input, validChoices) { const trimmed = input.trim(); if (!trimmed) { return { isValid: false, error: 'Please enter a choice', suggestions: ['Enter a number from the menu options', 'Type "help" for navigation commands'] }; } // Check for special navigation commands (case-insensitive) const navigationCommands = ['back', 'b', 'home', 'h', 'help', '?', 'exit', 'quit', 'q', 'status', 's', 'refresh', 'r']; const lowerInput = trimmed.toLowerCase(); if (navigationCommands.includes(lowerInput)) { return { isValid: true, sanitizedValue: lowerInput }; } // Check if it's a valid menu choice if (validChoices.includes(trimmed)) { return { isValid: true, sanitizedValue: trimmed }; } // Check if it's a number that might be valid const numericInput = parseInt(trimmed); if (!isNaN(numericInput)) { const numericChoice = numericInput.toString(); if (validChoices.includes(numericChoice)) { return { isValid: true, sanitizedValue: numericChoice }; } } return { isValid: false, error: `Invalid choice "${trimmed}"`, suggestions: [ `Valid choices: ${validChoices.join(', ')}`, 'Navigation commands: back, home, help, exit, status, refresh' ] }; } /** * Validate project name input */ static validateProjectName(input) { const options = { required: true, minLength: 2, maxLength: 100, pattern: /^[a-zA-Z0-9\s\-_\.]+$/, sanitize: true }; return this.validateText(input, options, 'project name'); } /** * Validate file path input */ static validateFilePath(input) { const trimmed = input.trim(); if (!trimmed) { return { isValid: false, error: 'File path cannot be empty' }; } // Check for dangerous path patterns if (trimmed.includes('..') || trimmed.includes('~')) { return { isValid: false, error: 'File path cannot contain ".." or "~" for security reasons', suggestions: ['Use relative paths from the current directory', 'Use absolute paths starting with "/"'] }; } // Check for invalid characters const invalidChars = /[<>:"|?*]/; if (invalidChars.test(trimmed)) { return { isValid: false, error: 'File path contains invalid characters', suggestions: ['Remove characters: < > : " | ? *'] }; } return { isValid: true, sanitizedValue: trimmed }; } /** * Validate URL input */ static validateUrl(input) { const trimmed = input.trim(); if (!trimmed) { return { isValid: false, error: 'URL cannot be empty' }; } try { const url = new URL(trimmed); // Check for allowed protocols const allowedProtocols = ['http:', 'https:']; if (!allowedProtocols.includes(url.protocol)) { return { isValid: false, error: 'URL must use HTTP or HTTPS protocol', suggestions: ['Example: https://example.com'] }; } return { isValid: true, sanitizedValue: url.toString() }; } catch (error) { return { isValid: false, error: 'Invalid URL format', suggestions: ['Example: https://example.com', 'Include protocol (http:// or https://)'] }; } } /** * Validate email input */ static validateEmail(input) { const trimmed = input.trim().toLowerCase(); if (!trimmed) { return { isValid: false, error: 'Email cannot be empty' }; } const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailPattern.test(trimmed)) { return { isValid: false, error: 'Invalid email format', suggestions: ['Example: user@example.com'] }; } return { isValid: true, sanitizedValue: trimmed }; } /** * Validate API key input */ static validateApiKey(input, provider) { const trimmed = input.trim(); if (!trimmed) { return { isValid: false, error: 'API key cannot be empty' }; } // Basic length validation if (trimmed.length < 10) { return { isValid: false, error: 'API key appears too short', suggestions: ['Check that you copied the complete API key'] }; } // Provider-specific validation if (provider) { switch (provider.toLowerCase()) { case 'google-ai': case 'gemini': if (!trimmed.startsWith('AI') || trimmed.length < 30) { return { isValid: false, error: 'Google AI API key should start with "AI" and be at least 30 characters', suggestions: ['Get your API key from https://aistudio.google.com/app/apikey'] }; } break; case 'openai': if (!trimmed.startsWith('sk-') || trimmed.length < 40) { return { isValid: false, error: 'OpenAI API key should start with "sk-" and be at least 40 characters', suggestions: ['Get your API key from https://platform.openai.com/api-keys'] }; } break; } } return { isValid: true, sanitizedValue: trimmed }; } /** * Validate numeric input */ static validateNumber(input, min, max) { const trimmed = input.trim(); if (!trimmed) { return { isValid: false, error: 'Number cannot be empty' }; } const num = parseFloat(trimmed); if (isNaN(num)) { return { isValid: false, error: 'Invalid number format', suggestions: ['Enter a valid number'] }; } if (min !== undefined && num < min) { return { isValid: false, error: `Number must be at least ${min}`, suggestions: [`Enter a number >= ${min}`] }; } if (max !== undefined && num > max) { return { isValid: false, error: `Number must be at most ${max}`, suggestions: [`Enter a number <= ${max}`] }; } return { isValid: true, sanitizedValue: num.toString() }; } /** * Validate yes/no input */ static validateYesNo(input) { const trimmed = input.trim().toLowerCase(); if (!trimmed) { return { isValid: false, error: 'Please enter yes or no', suggestions: ['Enter "y" or "yes" for yes', 'Enter "n" or "no" for no'] }; } const yesValues = ['y', 'yes', 'true', '1']; const noValues = ['n', 'no', 'false', '0']; if (yesValues.includes(trimmed)) { return { isValid: true, sanitizedValue: 'yes' }; } if (noValues.includes(trimmed)) { return { isValid: true, sanitizedValue: 'no' }; } return { isValid: false, error: `Invalid input "${input}"`, suggestions: ['Enter "y" or "yes" for yes', 'Enter "n" or "no" for no'] }; } /** * Generic text validation */ static validateText(input, options = {}, fieldName = 'input') { let value = options.sanitize ? this.sanitizeInput(input) : input.trim(); // Required validation if (options.required && !value) { return { isValid: false, error: `${fieldName} is required` }; } // Skip further validation if empty and not required if (!value && !options.required) { return { isValid: true, sanitizedValue: value }; } // Length validation if (options.minLength && value.length < options.minLength) { return { isValid: false, error: `${fieldName} must be at least ${options.minLength} characters`, suggestions: [`Current length: ${value.length}`] }; } if (options.maxLength && value.length > options.maxLength) { return { isValid: false, error: `${fieldName} must be at most ${options.maxLength} characters`, suggestions: [`Current length: ${value.length}`] }; } // Pattern validation if (options.pattern && !options.pattern.test(value)) { return { isValid: false, error: `${fieldName} contains invalid characters`, suggestions: ['Use only letters, numbers, spaces, hyphens, and underscores'] }; } // Allowed values validation if (options.allowedValues) { const checkValue = options.caseSensitive ? value : value.toLowerCase(); const allowedValues = options.caseSensitive ? options.allowedValues : options.allowedValues.map(v => v.toLowerCase()); if (!allowedValues.includes(checkValue)) { return { isValid: false, error: `Invalid ${fieldName}`, suggestions: [`Allowed values: ${options.allowedValues.join(', ')}`] }; } } // Custom validation if (options.customValidator) { const customResult = options.customValidator(value); if (!customResult.isValid) { return customResult; } } return { isValid: true, sanitizedValue: value }; } /** * Sanitize input to prevent security issues */ static sanitizeInput(input) { return input .trim() .replace(/[<>]/g, '') // Remove potential HTML/XML tags .replace(/['"]/g, '') // Remove quotes that could cause injection .replace(/\\/g, '/') // Normalize path separators .substring(0, 1000); // Limit length to prevent DoS } /** * Validate multiple inputs at once */ static validateMultiple(inputs) { const results = {}; for (const [key, { value, validator }] of Object.entries(inputs)) { results[key] = validator(value); } return results; } /** * Check if all validation results are valid */ static allValid(results) { return results.every(result => result.isValid); } /** * Get first error from validation results */ static getFirstError(results) { const firstInvalid = results.find(result => !result.isValid); return firstInvalid?.error || null; } /** * Format validation error for display */ static formatValidationError(result) { let message = `āŒ ${result.error}`; if (result.suggestions && result.suggestions.length > 0) { message += '\nšŸ’” Suggestions:'; for (const suggestion of result.suggestions) { message += `\n • ${suggestion}`; } } return message; } /** * Track validation errors for analytics */ static trackValidationError(result) { if (!result.isValid) { this.errorHistory.push({ ...result, timestamp: new Date() }); // Keep history size manageable if (this.errorHistory.length > this.MAX_ERROR_HISTORY) { this.errorHistory.shift(); } } } /** * Get validation error statistics */ static getValidationStats() { const now = new Date(); const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); const recentErrors = this.errorHistory.filter(error => error.timestamp && error.timestamp > oneHourAgo); const errorCounts = {}; this.errorHistory.forEach(error => { if (error.error) { errorCounts[error.error] = (errorCounts[error.error] || 0) + 1; } }); const commonErrors = Object.entries(errorCounts) .sort(([, a], [, b]) => b - a) .slice(0, 5) .map(([error]) => error); return { totalErrors: this.errorHistory.length, recentErrors: recentErrors.length, commonErrors, errorRate: this.errorHistory.length > 0 ? (recentErrors.length / this.errorHistory.length) * 100 : 0 }; } /** * Clear validation error history */ static clearValidationHistory() { this.errorHistory.length = 0; } /** * Validate command line arguments */ static validateCommandArgs(args) { if (!Array.isArray(args)) { return { isValid: false, error: 'Invalid arguments format', suggestions: ['Arguments must be an array of strings'] }; } // Check for potentially dangerous arguments const dangerousPatterns = [ /--eval/i, /--exec/i, /\$\(/, /`[^`]*`/, /\|\s*sh/i, /\|\s*bash/i, /\|\s*cmd/i ]; for (const arg of args) { if (typeof arg !== 'string') { return { isValid: false, error: 'All arguments must be strings', suggestions: ['Check argument types'] }; } for (const pattern of dangerousPatterns) { if (pattern.test(arg)) { return { isValid: false, error: 'Potentially dangerous argument detected', suggestions: ['Remove shell injection attempts', 'Use safe argument values'] }; } } } return { isValid: true, sanitizedValue: args.map(arg => this.sanitizeInputSecure(arg)).join(',') }; } /** * Validate file system paths with security checks */ static validatePathSecure(input) { const sanitized = this.sanitizeInputSecure(input); if (!sanitized) { return { isValid: false, error: 'Path cannot be empty', suggestions: ['Provide a valid file or directory path'] }; } // Check for path traversal attempts if (sanitized.includes('..') || sanitized.includes('~')) { return { isValid: false, error: 'Path traversal not allowed', suggestions: ['Use relative paths within the project directory', 'Avoid ".." and "~" in paths'] }; } // Check for absolute paths outside allowed directories if (sanitized.startsWith('/') && !sanitized.startsWith('/tmp/') && !sanitized.startsWith('/var/tmp/')) { return { isValid: false, error: 'Absolute paths outside allowed directories not permitted', suggestions: ['Use relative paths', 'Use paths within the project directory'] }; } return { isValid: true, sanitizedValue: sanitized }; } /** * Validate numeric input with range checking */ static validateNumericRange(input, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) { const sanitized = this.sanitizeInputSecure(input); if (!sanitized) { return { isValid: false, error: 'Numeric input cannot be empty', suggestions: [`Enter a number between ${min} and ${max}`] }; } const num = parseFloat(sanitized); if (isNaN(num)) { return { isValid: false, error: 'Input must be a valid number', suggestions: ['Enter a numeric value', 'Use decimal notation if needed'] }; } if (num < min || num > max) { return { isValid: false, error: `Number must be between ${min} and ${max}`, suggestions: [`Enter a value between ${min} and ${max}`] }; } return { isValid: true, sanitizedValue: num.toString() }; } } //# sourceMappingURL=InputValidationService.js.map