UNPKG

woaru

Version:

Universal Project Setup Autopilot - Analyze and automatically configure development tools for ANY programming language

542 lines • 21 kB
/** * WOARU Startup Check Module * Production-Ready implementation with comprehensive security and error handling */ import { spawn } from 'child_process'; import fs from 'fs-extra'; import * as path from 'path'; import chalk from 'chalk'; import inquirer from 'inquirer'; import { VersionManager } from './versionManager.js'; import { t, initializeI18n } from '../config/i18n.js'; /** * Sanitizes error messages to prevent information leakage * @param error - The error to sanitize * @returns Sanitized error message safe for display */ function sanitizeErrorMessage(error) { if (typeof error === 'string') { // Remove sensitive paths and limit length return error.replace(/\/[^\s]*\/[^\s]*/g, '[PATH]').substring(0, 200); } if (error instanceof Error) { return sanitizeErrorMessage(error.message); } return 'Unknown error occurred'; } /** * Validates command name to prevent injection attacks * @param command - Command name to validate * @returns True if command name is safe */ function isValidCommand(command) { const validCommands = ['git', 'docker', 'snyk']; return validCommands.includes(command) && /^[a-zA-Z0-9-_]+$/.test(command); } /** * Type guard for version info validation * @param versionInfo - Version info to validate * @returns True if version info is valid */ function isValidVersionInfo(versionInfo) { return (typeof versionInfo === 'object' && versionInfo !== null && typeof versionInfo.isUpToDate === 'boolean'); } export class StartupCheck { /** * Secure command execution using spawn instead of execSync * Prevents command injection vulnerabilities */ static secureCommandExecution(command, args, timeout) { return new Promise(resolve => { const process = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], timeout, }); let output = ''; let errorOutput = ''; process.stdout.on('data', data => { output += data.toString(); }); process.stderr.on('data', data => { errorOutput += data.toString(); }); process.on('close', code => { if (code === 0) { resolve(output.trim()); } else { console.error(`Command failed: ${command}`, errorOutput); resolve(''); } }); process.on('error', error => { console.error(`Command error: ${command}`, error); resolve(''); }); }); } static CACHE_FILE = path.join(process.env.HOME || process.env.USERPROFILE || '~', '.woaru', 'startup-cache.json'); static CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours in milliseconds static MAX_CACHE_SIZE = 1024; // Maximum cache file size in bytes /** * Check if we should run the startup check based on cache * Implements secure cache validation with size limits */ static shouldRunCheck() { try { if (!fs.existsSync(this.CACHE_FILE)) { return true; } // Check cache file size for security const stats = fs.statSync(this.CACHE_FILE); if (stats.size > this.MAX_CACHE_SIZE) { console.debug('Cache file too large, running check'); return true; } const cache = fs.readJsonSync(this.CACHE_FILE); // Validate cache structure if (typeof cache !== 'object' || cache === null || typeof cache.timestamp !== 'number') { console.debug('Invalid cache structure, running check'); return true; } const now = Date.now(); const cacheAge = now - cache.timestamp; // Validate timestamp is reasonable (not in future, not too old) if (cache.timestamp > now || cache.timestamp < now - 365 * 24 * 60 * 60 * 1000) { console.debug('Invalid cache timestamp, running check'); return true; } return cacheAge > this.CACHE_DURATION; } catch (error) { console.debug(`Cache check failed: ${sanitizeErrorMessage(error)}`); return true; } } /** * Update the cache timestamp with security validation */ static updateCache() { try { // Validate cache directory path const woaruDir = path.dirname(this.CACHE_FILE); const normalizedDir = path.normalize(woaruDir); // Ensure path is within expected bounds if (!normalizedDir.includes('.woaru')) { console.debug('Invalid cache directory path'); return; } fs.ensureDirSync(normalizedDir); const cache = { timestamp: Date.now(), lastCheck: new Date().toISOString(), version: '1.0', // Cache format version }; // Write with restricted permissions fs.writeJsonSync(this.CACHE_FILE, cache, { spaces: 2 }); // Set restrictive file permissions (Unix-like systems) try { fs.chmodSync(this.CACHE_FILE, 0o600); } catch { // Ignore permission errors on Windows } } catch (error) { console.debug(`Cache update failed: ${sanitizeErrorMessage(error)}`); } } /** * Check if git is available in the system with security validation */ static async checkGitAvailability() { try { await initializeI18n(); if (!isValidCommand('git')) { return { available: false, error: t('startup_check.git_not_available'), }; } // Use secure spawn instead of execSync const output = await this.secureCommandExecution('git', ['--version'], 5000); if (!output) { return { available: false, error: t('startup_check.git_command_failed'), }; } // Extract version for logging (optional) const versionMatch = output.toString().match(/git version ([\d.]+)/); const version = versionMatch ? versionMatch[1] : 'unknown'; return { available: true, version: sanitizeErrorMessage(version), }; } catch { return { available: false, error: t('startup_check.git_not_available'), }; } } /** * Check if docker is available (optional) with security validation */ static async checkDockerAvailability() { try { await initializeI18n(); if (!isValidCommand('docker')) { return { available: false, error: t('startup_check.docker_not_available'), }; } // Use secure spawn instead of execSync const output = await this.secureCommandExecution('docker', ['--version'], 5000); if (!output) { return { available: false, error: t('startup_check.docker_command_failed'), }; } const versionMatch = output.toString().match(/Docker version ([\d.]+)/); const version = versionMatch ? versionMatch[1] : 'unknown'; return { available: true, version: sanitizeErrorMessage(version), }; } catch { return { available: false, error: t('startup_check.docker_not_available'), }; } } /** * Check if snyk is available (optional) with security validation */ static async checkSnykAvailability() { try { await initializeI18n(); if (!isValidCommand('snyk')) { return { available: false, error: t('startup_check.snyk_not_available'), }; } // Use secure spawn instead of execSync const output = await this.secureCommandExecution('snyk', ['--version'], 10000); if (!output) { return { available: false, error: t('startup_check.snyk_command_failed'), }; } const version = output.toString().trim(); return { available: true, version: sanitizeErrorMessage(version), }; } catch { return { available: false, error: t('startup_check.snyk_not_available'), }; } } /** * Perform environment checks with comprehensive validation */ static async performEnvironmentCheck() { const errors = []; const warnings = []; const toolsAvailable = { git: false, docker: false, snyk: false, }; try { await initializeI18n(); // Check Git (required) with parallel execution safety const gitCheck = await this.checkGitAvailability(); toolsAvailable.git = gitCheck.available; if (!gitCheck.available) { errors.push(`${t('startup_check.warning_prefix')} ${gitCheck.error}`); warnings.push(t('startup_check.git_commands_warning')); } // Check Docker (optional) const dockerCheck = await this.checkDockerAvailability(); toolsAvailable.docker = dockerCheck.available; if (!dockerCheck.available) { warnings.push(t('startup_check.docker_tip')); } // Check Snyk (optional) const snykCheck = await this.checkSnykAvailability(); toolsAvailable.snyk = snykCheck.available; if (!snykCheck.available) { warnings.push(t('startup_check.snyk_tip')); } return { errors, warnings, toolsAvailable }; } catch (error) { // Fallback error handling const sanitizedError = sanitizeErrorMessage(error); console.debug(`Environment check failed: ${sanitizedError}`); return { errors: ['Environment check failed'], warnings: ['Some tools may not be available'], toolsAvailable, }; } } /** * Perform version check and prompt for update if needed with security validation */ static async performVersionCheck() { const errors = []; try { await initializeI18n(); const versionInfo = await VersionManager.checkVersion(); // Validate version info structure if (!isValidVersionInfo(versionInfo)) { errors.push(t('startup_check.version_check_failed', { error: 'Invalid version information', })); return { updated: false, errors }; } if (!versionInfo.isUpToDate && versionInfo.latest) { // Sanitize version string before display const sanitizedVersion = sanitizeErrorMessage(versionInfo.latest); console.log(chalk.yellow(t('startup_check.new_version_available', { version: sanitizedVersion, }))); if (versionInfo.releaseDate) { const sanitizedDate = sanitizeErrorMessage(versionInfo.releaseDate); console.log(chalk.gray(` ${t('startup_check.released_on', { date: sanitizedDate })}`)); } try { const answer = await inquirer.prompt([ { type: 'confirm', name: 'update', message: t('startup_check.update_prompt'), default: false, timeout: 30000, // 30 second timeout }, ]); // Validate answer structure if (typeof answer !== 'object' || answer === null || typeof answer.update !== 'boolean') { console.debug('Invalid user input received'); return { updated: false, errors, versionInfo }; } if (answer.update) { try { await VersionManager.updateToLatest(); return { updated: true, errors: [], versionInfo }; } catch (updateError) { const sanitizedError = sanitizeErrorMessage(updateError); errors.push(t('startup_check.update_failed', { error: sanitizedError })); return { updated: false, errors, versionInfo }; } } } catch (promptError) { // Handle prompt timeout or other issues console.debug(`Prompt failed: ${sanitizeErrorMessage(promptError)}`); return { updated: false, errors, versionInfo }; } } return { updated: false, errors: [], versionInfo }; } catch (error) { const sanitizedError = sanitizeErrorMessage(error); errors.push(t('startup_check.version_check_failed', { error: sanitizedError })); return { updated: false, errors }; } } /** * Main startup check function with comprehensive error handling */ static async performStartupCheck() { try { await initializeI18n(); const cacheUsed = !this.shouldRunCheck(); // Check if we should run the check based on cache if (cacheUsed) { return { versionCheck: true, environmentCheck: true, errors: [], warnings: [], cacheUsed: true, }; } const result = { versionCheck: true, environmentCheck: true, errors: [], warnings: [], cacheUsed: false, }; // Perform environment check with error isolation try { const envCheck = await this.performEnvironmentCheck(); result.errors.push(...envCheck.errors); result.warnings.push(...envCheck.warnings); result.toolsAvailable = envCheck.toolsAvailable; if (envCheck.errors.length > 0) { result.environmentCheck = false; } } catch (envError) { const sanitizedError = sanitizeErrorMessage(envError); result.errors.push(`Environment check failed: ${sanitizedError}`); result.environmentCheck = false; } // Perform version check with error isolation try { const versionCheck = await this.performVersionCheck(); result.errors.push(...versionCheck.errors); if (versionCheck.errors.length > 0) { result.versionCheck = false; } } catch (versionError) { const sanitizedError = sanitizeErrorMessage(versionError); result.errors.push(t('startup_check.version_check_failed', { error: sanitizedError })); result.versionCheck = false; } // Update cache safely this.updateCache(); // Display results with safe output await this.displayResults(result); return result; } catch (criticalError) { // Final fallback for catastrophic failures const sanitizedError = sanitizeErrorMessage(criticalError); console.error(`Critical startup check failure: ${sanitizedError}`); return { versionCheck: false, environmentCheck: false, errors: ['Critical startup check failure'], warnings: [], cacheUsed: false, }; } } /** * Silent startup check (no interactive prompts) with enhanced security */ static async performSilentStartupCheck() { try { await initializeI18n(); const cacheUsed = !this.shouldRunCheck(); // Check if we should run the check based on cache if (cacheUsed) { return { versionCheck: true, environmentCheck: true, errors: [], warnings: [], cacheUsed: true, }; } const result = { versionCheck: true, environmentCheck: true, errors: [], warnings: [], cacheUsed: false, }; // Perform environment check with isolation try { const envCheck = await this.performEnvironmentCheck(); result.errors.push(...envCheck.errors); result.warnings.push(...envCheck.warnings); result.toolsAvailable = envCheck.toolsAvailable; if (envCheck.errors.length > 0) { result.environmentCheck = false; } } catch (envError) { const sanitizedError = sanitizeErrorMessage(envError); result.errors.push(`Environment check failed: ${sanitizedError}`); result.environmentCheck = false; } // Silent version check (no prompts) with validation try { const versionInfo = await VersionManager.checkVersion(); if (isValidVersionInfo(versionInfo) && !versionInfo.isUpToDate && versionInfo.latest) { const sanitizedVersion = sanitizeErrorMessage(versionInfo.latest); result.warnings.push(t('startup_check.new_version_silent', { version: sanitizedVersion })); } } catch (error) { const sanitizedError = sanitizeErrorMessage(error); result.errors.push(t('startup_check.version_check_failed', { error: sanitizedError })); result.versionCheck = false; } // Update cache this.updateCache(); return result; } catch (criticalError) { // Final fallback const sanitizedError = sanitizeErrorMessage(criticalError); console.debug(`Silent startup check failed: ${sanitizedError}`); return { versionCheck: false, environmentCheck: false, errors: ['Silent startup check failed'], warnings: [], cacheUsed: false, }; } } /** * Display startup check results with safe i18n output */ static async displayResults(result) { try { // Display warnings if any if (result.warnings.length > 0) { console.log(chalk.yellow(`\n${t('startup_check.startup_notes')}`)); result.warnings.forEach(warning => { console.log(` ${warning}`); }); } // Display errors if any if (result.errors.length > 0) { console.log(chalk.red(`\n${t('startup_check.startup_problems')}`)); result.errors.forEach(error => { console.log(` ${error}`); }); } } catch { // Fallback display console.log('\nšŸ“‹ Startup Notes:'); result.warnings.forEach(warning => console.log(` ${warning}`)); if (result.errors.length > 0) { console.log('\nāŒ Startup Problems:'); result.errors.forEach(error => console.log(` ${error}`)); } } } } //# sourceMappingURL=startupCheck.js.map