UNPKG

ssh-bridge-ai

Version:

AI-Powered SSH Tool with Bulletproof Connections & Enterprise Sandbox Security + Cursor-like Confirmation - Enable AI assistants to securely SSH into your servers with persistent sessions, keepalive, automatic recovery, sandbox command testing, and user c

775 lines (633 loc) โ€ข 27.5 kB
const axios = require('axios'); const fs = require('fs').promises; const crypto = require('crypto'); const path = require('path'); const { ValidationUtils } = require('./utils/validation'); const { ErrorHandler, NetworkError, SecurityError } = require('./utils/errors'); const logger = require('./utils/logger'); const { API, UPDATE } = require('./utils/constants'); const Config = require('./config'); const chalk = require('chalk'); const ora = require('ora'); /** * Secure update system with verification */ class SecureUpdater { constructor() { this.packageName = 'ssh-bridge-ai'; this.npmRegistry = process.env.NPM_REGISTRY_URL || 'https://registry.npmjs.org'; this.verificationEnabled = process.env.UPDATE_VERIFICATION !== 'false'; this.checksumCache = new Map(); } /** * Verify package integrity before update * @param {string} version - Version to verify * @returns {Promise<boolean>} - True if verification passes */ async verifyPackageIntegrity(version) { if (!this.verificationEnabled) { logger.warn('Update verification disabled - proceeding without verification'); return true; } try { // Get package metadata const metadataUrl = `${this.npmRegistry}/${this.packageName}/${version}`; const response = await axios.get(metadataUrl, { timeout: 10000, headers: { 'User-Agent': 'SSHBridge-Updater/1.0' } }); if (!response.data || !response.data.dist) { logger.error('Invalid package metadata received'); return false; } const packageData = response.data; const tarballUrl = packageData.dist.tarball; const integrity = packageData.dist.integrity; // Verify integrity hash if available if (integrity) { const isValid = await this.verifyIntegrityHash(tarballUrl, integrity); if (!isValid) { logger.error('Package integrity verification failed'); return false; } } // Additional security checks if (!this.validatePackageMetadata(packageData)) { logger.error('Package metadata validation failed'); return false; } logger.info('Package integrity verification passed'); return true; } catch (error) { logger.error('Package verification failed:', error.message); return false; } } /** * Validate package metadata for security * @param {Object} metadata - Package metadata * @returns {boolean} - True if metadata is valid */ validatePackageMetadata(metadata) { // Check required fields if (!metadata.name || !metadata.version || !metadata.dist) { return false; } // Verify package name matches expected if (metadata.name !== this.packageName) { return false; } // Check version format if (!ValidationUtils.validateVersion(metadata.version)) { return false; } // Check for suspicious patterns in description or keywords const suspiciousPatterns = [ /eval\s*\(/i, /exec\s*\(/i, /require\s*\(/i, /process\.env/i, /child_process/i, /fs\.writeFile/i ]; const description = (metadata.description || '') + (metadata.keywords || []).join(' '); for (const pattern of suspiciousPatterns) { if (pattern.test(description)) { logger.warn('Suspicious pattern detected in package metadata'); return false; } } return true; } /** * Verify integrity hash of package tarball * @param {string} tarballUrl - Tarball URL * @param {string} expectedIntegrity - Expected integrity hash * @returns {Promise<boolean>} - True if hash matches */ async verifyIntegrityHash(tarballUrl, expectedIntegrity) { try { // Parse integrity string (format: sha512-<hash>) const integrityParts = expectedIntegrity.split('-'); if (integrityParts.length !== 2) { logger.error('Invalid integrity format'); return false; } const algorithm = integrityParts[0]; const expectedHash = integrityParts[1]; // Download and hash tarball const response = await axios.get(tarballUrl, { responseType: 'arraybuffer', timeout: 30000, headers: { 'User-Agent': 'SSHBridge-Updater/1.0' } }); const actualHash = crypto.createHash(algorithm).update(response.data).digest('base64'); if (actualHash !== expectedHash) { logger.error(`Integrity hash mismatch. Expected: ${expectedHash}, Got: ${actualHash}`); return false; } return true; } catch (error) { logger.error('Integrity verification failed:', error.message); return false; } } } class AutoUpdater { constructor() { this.config = new Config(); this.packageName = 'ssh-bridge-ai'; this.currentVersion = this.getInstalledVersion(); // SECURITY: Use centralized constants for update endpoints this.npmRegistry = process.env.NPM_REGISTRY_URL || API.REGISTRY_URL; // SECURITY: Validate registry URL try { const url = new URL(this.npmRegistry); if (!['http:', 'https:'].includes(url.protocol)) { throw new Error('NPM registry URL must use HTTP or HTTPS protocol'); } } catch (error) { throw new Error(`Invalid NPM registry URL: ${this.npmRegistry}`); } // Use centralized update intervals this.criticalUpdateInterval = UPDATE.CRITICAL_UPDATE_INTERVAL; this.regularUpdateInterval = UPDATE.REGULAR_UPDATE_INTERVAL; this.firstTimeInterval = UPDATE.FIRST_TIME_INTERVAL; } getInstalledVersion() { try { // SECURITY: Use spawn instead of execSync for better security const { spawnSync } = require('child_process'); // SECURITY: Validate package name to prevent command injection const safePackageName = this.sanitizePackageName(this.packageName); const result = spawnSync('npm', ['list', '-g', safePackageName, '--depth=0', '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }); if (result.error) { throw result.error; } const parsed = JSON.parse(result.stdout); const installedVersion = parsed.dependencies?.[safePackageName]?.version; if (installedVersion) { return installedVersion; } } catch (error) { // Fallback to package.json if npm command fails } // Fallback to local package.json version return require('../package.json').version; } // SECURITY: Package name sanitization to prevent command injection sanitizePackageName(packageName) { if (!packageName || typeof packageName !== 'string') { throw new Error('Invalid package name'); } // Only allow alphanumeric, hyphens, and underscores const sanitized = packageName.replace(/[^a-zA-Z0-9\-_]/g, ''); if (sanitized !== packageName) { throw new Error('Package name contains invalid characters'); } return sanitized; } // SECURITY: Package spec sanitization to prevent command injection sanitizePackageSpec(packageSpec) { if (!packageSpec || typeof packageSpec !== 'string') { throw new Error('Invalid package spec'); } // Allow package name with version (e.g., "package@1.0.0") const sanitized = packageSpec.replace(/[^a-zA-Z0-9\-_@.]/g, ''); if (sanitized !== packageSpec) { throw new Error('Package spec contains invalid characters'); } return sanitized; } async checkForUpdates(silent = false) { try { // Prevent duplicate messages when user is manually checking if (process.env.SSHBRIDGE_MANUAL_UPDATE_CHECK && !silent) { silent = true; } const response = await axios.get(`${this.npmRegistry}/${this.packageName}`, { timeout: API.HTTP_TIMEOUT }); const latestVersion = response.data['dist-tags'].latest; const currentVersion = this.currentVersion; if (this.isNewerVersion(latestVersion, currentVersion)) { const updateInfo = { currentVersion, latestVersion, releaseNotes: await this.getReleaseNotes(latestVersion), isCritical: await this.isCriticalUpdate(latestVersion), checkedAt: new Date().toISOString() }; if (!silent) { await this.promptForUpdate(updateInfo); } return updateInfo; } if (!silent) { console.log(chalk.green('โœ… You have the latest version!')); } return null; } catch (error) { if (!silent) { console.log(chalk.yellow('โš ๏ธ Could not check for updates')); } return null; } } async promptForUpdate(updateInfo) { const { currentVersion, latestVersion, isCritical, releaseNotes } = updateInfo; console.log(chalk.blue(`\n๐Ÿ”„ SSHBridge Update Available`)); console.log(chalk.gray(`Current: ${currentVersion} โ†’ Latest: ${latestVersion}`)); if (isCritical) { console.log(chalk.red('๐Ÿšจ CRITICAL SECURITY UPDATE - Highly recommended!')); } if (releaseNotes.length > 0) { console.log(chalk.blue('\n๐Ÿ“‹ What\'s New:')); releaseNotes.forEach(note => { console.log(chalk.gray(` โ€ข ${note}`)); }); } console.log(chalk.blue('\n๐Ÿ’ก To update manually:')); console.log(chalk.gray(` sudo npm install -g ${this.packageName}@${latestVersion}`)); if (isCritical) { console.log(chalk.yellow('\nโš ๏ธ Critical updates contain important security fixes.')); } // Store update notification (don't auto-install for security) this.config.setUpdateNotification({ version: latestVersion, critical: isCritical, notifiedAt: new Date().toISOString() }); } async performUpdate(targetVersion) { console.log(chalk.blue(`\n๐Ÿš€ Updating SSH Bridge to v${targetVersion}...`)); try { // Step 0: SECURITY - Verify package integrity before download let progressBar = ora('๐Ÿ”’ Verifying package integrity...').start(); const secureUpdater = new SecureUpdater(); const integrityVerified = await secureUpdater.verifyPackageIntegrity(targetVersion); if (!integrityVerified) { progressBar.fail(chalk.red('โŒ Package integrity verification failed')); throw new SecurityError('Package integrity verification failed. Update aborted for security reasons.'); } progressBar.succeed(chalk.green('โœ… Package integrity verified')); // Step 1: Backup current installation progressBar = ora('๐Ÿ“ฆ Creating backup of current version...').start(); const backupInfo = { version: this.currentVersion, installedAt: new Date().toISOString() }; this.config.setBackupInfo(backupInfo); progressBar.succeed(chalk.green('โœ… Backup created')); // Step 2: Get sudo password before starting console.log(chalk.yellow('\n๐Ÿ” This update requires administrator privileges.')); const inquirer = require('inquirer'); const { sudoPassword } = await inquirer.prompt([{ type: 'password', name: 'sudoPassword', message: 'Enter your password for sudo access:', mask: '*' }]); // SECURITY: Create a function to securely handle password cleanup const securePasswordCleanup = () => { // Clear password from memory after use if (typeof sudoPassword === 'string') { const passwordArray = sudoPassword.split(''); passwordArray.fill('*'); } }; // Step 3: Download and install new version progressBar = ora('โฌ‡๏ธ Downloading new version...').start(); // Use sudo directly for reliability (most global npm installs need it) const packageSpec = `${this.packageName}@${targetVersion}`; await new Promise((resolve, reject) => { const { spawn } = require('child_process'); // SECURITY: Use sudo with the provided password progressBar.text = '๐Ÿ” Installing with admin permissions...'; // SECURITY: Validate package spec to prevent command injection const safePackageSpec = this.sanitizePackageSpec(packageSpec); let npmProcess = spawn('sudo', ['-S', 'npm', 'install', '-g', safePackageSpec], { stdio: ['pipe', 'pipe', 'pipe'] }); // SECURITY: Secure password handling with immediate cleanup const passwordBuffer = Buffer.from(sudoPassword); npmProcess.stdin.write(passwordBuffer); npmProcess.stdin.end(); // SECURITY: Immediately clear password from memory passwordBuffer.fill(0); let output = ''; let hasStartedDownload = false; let hasStartedInstall = false; npmProcess.stdout.on('data', (data) => { output += data.toString(); const text = data.toString().toLowerCase(); if (text.includes('http fetch') || text.includes('downloading')) { if (!hasStartedDownload) { progressBar.text = 'โฌ‡๏ธ Downloading package...'; hasStartedDownload = true; } } else if (text.includes('extract') || text.includes('installing')) { if (!hasStartedInstall) { progressBar.text = '๐Ÿ“ฆ Installing package...'; hasStartedInstall = true; } } else if (text.includes('linking') || text.includes('binary')) { progressBar.text = '๐Ÿ”— Setting up CLI command...'; } }); npmProcess.stderr.on('data', (data) => { const text = data.toString().toLowerCase(); // Check for sudo authentication failure if (text.includes('incorrect password') || text.includes('authentication failure') || text.includes('sorry, try again')) { reject(new Error('Incorrect sudo password. Please try again.')); return; } // npm sometimes sends progress info to stderr if (text.includes('download') || text.includes('extract')) { progressBar.text = '๐Ÿ“ฆ Processing package...'; } }); npmProcess.on('close', (code) => { // SECURITY: Clean up password from memory securePasswordCleanup(); if (code === 0) { resolve(output); } else { reject(new Error(`Update failed with exit code ${code}. Try manual install: sudo npm install -g ${packageSpec}`)); } }); npmProcess.on('error', (error) => { // SECURITY: Clean up password from memory on error securePasswordCleanup(); reject(new Error(`Update failed: ${error.message}`)); }); }); progressBar.succeed(chalk.green('โœ… Package installed')); // Step 4: Verify installation (more forgiving since npm install succeeded) progressBar = ora('๐Ÿ” Verifying installation...').start(); const verification = await this.verifyUpdate(targetVersion); if (!verification.success) { // Don't fail the update if npm install succeeded - just warn progressBar.succeed(chalk.yellow('โš ๏ธ Installation complete (verification uncertain)')); console.log(chalk.yellow('๐Ÿ’ก Package installed successfully, but version verification inconclusive')); } else { progressBar.succeed(chalk.green('โœ… Installation verified')); } // Step 5: Cleanup progressBar = ora('๐Ÿงน Cleaning up...').start(); this.config.clearUpdateNotification(); progressBar.succeed(chalk.green('โœ… Update complete')); console.log(chalk.green.bold('\n๐ŸŽ‰ Successfully updated to v' + targetVersion + '!')); console.log(chalk.blue('๐Ÿ’ก Restart your terminal to use the new version.')); console.log(chalk.gray(`๐Ÿ’พ Previous version (v${this.currentVersion}) backed up for rollback.`)); return { success: true, version: targetVersion }; } catch (error) { console.log(chalk.red('\nโŒ Update failed: ' + error.message)); console.log(chalk.yellow('๐Ÿ’ก You can update manually with:')); console.log(chalk.gray(` sudo npm install -g ${this.packageName}@${targetVersion}`)); return { success: false, error: error.message }; } } async verifyUpdate(expectedVersion) { try { return new Promise((resolve) => { // Give npm a moment to update the global link setTimeout(() => { // Try npm list first (more reliable after install) exec('npm list -g ssh-bridge-ai --depth=0', (listError, listOutput) => { if (!listError && listOutput.includes(`ssh-bridge-ai@${expectedVersion}`)) { // npm list shows correct version - success! resolve({ success: true, actualVersion: expectedVersion, expectedVersion, error: null }); return; } // npm list failed or wrong version - try sshbridge --version exec('sshbridge --version', (versionError, versionOutput) => { if (!versionError) { const actualVersion = versionOutput.trim(); const success = actualVersion === expectedVersion || actualVersion.includes(expectedVersion); resolve({ success, actualVersion, expectedVersion, error: success ? null : `Version mismatch: expected ${expectedVersion}, got ${actualVersion}` }); return; } // Both methods failed - but npm install succeeded, so be optimistic resolve({ success: true, // Assume success since npm install worked actualVersion: 'unknown', expectedVersion, error: 'Version verification inconclusive but npm install succeeded' }); }); }); }, 3000); // Wait 3 seconds for npm to update global links }); } catch (error) { return { success: false, error: error.message }; } } async rollbackUpdate() { const backupInfo = this.config.getBackupInfo(); if (!backupInfo) { throw new Error('No backup information found'); } const spinner = ora(`Rolling back to v${backupInfo.version}...`).start(); try { const rollbackCommand = `sudo npm install -g ${this.packageName}@${backupInfo.version}`; await new Promise((resolve, reject) => { exec(rollbackCommand, (error, stdout, stderr) => { if (error) { reject(new Error(`Rollback failed: ${error.message}`)); return; } resolve(stdout); }); }); spinner.succeed(chalk.green(`โœ… Rolled back to v${backupInfo.version}`)); // Clear backup info after successful rollback this.config.clearBackupInfo(); return { success: true, version: backupInfo.version }; } catch (error) { spinner.fail(chalk.red('โŒ Rollback failed')); throw error; } } async getReleaseNotes(version) { try { // Get release notes from GitHub API or npm const response = await axios.get(`${this.npmRegistry}/${this.packageName}/${version}`, { timeout: 3000 }); const description = response.data.description || ''; // Extract key features from description or return generic notes const notes = []; if (description.includes('security')) { notes.push('Security improvements and fixes'); } if (description.includes('performance')) { notes.push('Performance optimizations'); } if (description.includes('hybrid')) { notes.push('Hybrid SSH execution enhancements'); } if (version.includes('1.4')) { notes.push('Revolutionary hybrid SSH system'); notes.push('20x faster command execution'); notes.push('Private key protection'); notes.push('Unbreakable rate limiting'); } return notes.length > 0 ? notes : ['Bug fixes and improvements']; } catch (error) { return ['Bug fixes and improvements']; } } async isCriticalUpdate(version) { // Define critical update patterns const criticalKeywords = ['security', 'critical', 'vulnerability', 'fix']; try { const response = await axios.get(`${this.npmRegistry}/${this.packageName}/${version}`, { timeout: 3000 }); const description = (response.data.description || '').toLowerCase(); // Check if this is a critical update const isCritical = criticalKeywords.some(keyword => description.includes(keyword) ); // Major version changes are considered critical const [currentMajor] = this.currentVersion.split('.'); const [latestMajor] = version.split('.'); return isCritical || (parseInt(latestMajor) > parseInt(currentMajor)); } catch (error) { // Assume critical if we can't determine return false; } } isNewerVersion(latest, current) { const parseVersion = (version) => { return version.split('.').map(num => parseInt(num, 10)); }; const latestParts = parseVersion(latest); const currentParts = parseVersion(current); for (let i = 0; i < Math.max(latestParts.length, currentParts.length); i++) { const latestPart = latestParts[i] || 0; const currentPart = currentParts[i] || 0; if (latestPart > currentPart) return true; if (latestPart < currentPart) return false; } return false; } shouldCheckForUpdates() { const lastCheck = this.config.getLastUpdateCheck(); const pendingNotification = this.config.getUpdateNotification(); // Always check if no previous check if (!lastCheck) return true; const timeSinceLastCheck = Date.now() - new Date(lastCheck).getTime(); // If we have a pending critical update, check more frequently if (pendingNotification && pendingNotification.critical) { return timeSinceLastCheck > this.criticalUpdateInterval; } // For very new installations (first week), check more often const installAge = this.getInstallationAge(); if (installAge < 7 * 24 * 60 * 60 * 1000) { // 7 days return timeSinceLastCheck > this.firstTimeInterval; } // Regular interval for established installations return timeSinceLastCheck > this.regularUpdateInterval; } getInstallationAge() { // Estimate installation age from first config creation const configPath = require('path').join(require('os').homedir(), '.sshbridge', 'config.json'); try { const stats = require('fs').statSync(configPath); return Date.now() - stats.birthtime.getTime(); } catch (error) { // If no config yet, treat as brand new return 0; } } async backgroundUpdateCheck() { // Special handling for v1.x users upgrading to auto-update system if (this.isLegacyVersion()) { console.log(chalk.yellow('\n๐Ÿ”„ Auto-update system now available!')); console.log(chalk.blue('Future updates will be automatic. Run ') + chalk.white.bold('sshbridge u') + chalk.blue(' to upgrade.')); } if (this.shouldCheckForUpdates()) { const updateInfo = await this.checkForUpdates(true); if (updateInfo) { // Store update info for next command this.config.setUpdateNotification({ version: updateInfo.latestVersion, critical: updateInfo.isCritical, checkedAt: new Date().toISOString() }); } // Update last check time this.config.setLastUpdateCheck(new Date().toISOString()); } } isLegacyVersion() { // Check if this is a v1.x version without auto-update const [major, minor] = this.currentVersion.split('.').map(Number); const hasAutoUpdate = this.config.getLastUpdateCheck(); // v1.5.0+ has auto-update, but only if they've used it before return (major === 1 && minor < 5) || (major === 1 && minor >= 5 && !hasAutoUpdate); } async showPendingUpdateNotification() { const notification = this.config.getUpdateNotification(); if (!notification) return; // Don't spam - only show once per session if (process.env.SSHBRIDGE_NOTIFICATION_SHOWN) return; // Also check if we've shown this specific version already today const lastNotification = this.config.getLastNotificationShown(); const today = new Date().toDateString(); if (lastNotification === `${notification.version}-${today}`) return; process.env.SSHBRIDGE_NOTIFICATION_SHOWN = 'true'; this.config.setLastNotificationShown(`${notification.version}-${today}`); const { version, critical, checkedAt } = notification; // Calculate how long ago the update was found const daysSinceUpdate = Math.floor((Date.now() - new Date(checkedAt).getTime()) / (1000 * 60 * 60 * 24)); if (critical) { console.log(chalk.red(`\n๐Ÿšจ CRITICAL SECURITY UPDATE AVAILABLE: v${version}`)); console.log(chalk.yellow('โš ๏ธ Your current version may have security vulnerabilities')); if (daysSinceUpdate > 7) { console.log(chalk.red.bold('โ— URGENT: Security update pending for over a week!')); } else if (daysSinceUpdate > 3) { console.log(chalk.yellow('โฐ Security update available for several days')); } console.log(chalk.green('๐Ÿ›ก๏ธ Update now for enhanced protection: ') + chalk.white.bold('sshbridge u')); console.log(chalk.gray(' Security updates include latest threat protection')); } else { console.log(chalk.blue(`\n๐Ÿš€ NEW VERSION AVAILABLE: v${version}`)); console.log(chalk.cyan('โœจ Latest features, performance improvements & bug fixes')); if (daysSinceUpdate > 14) { console.log(chalk.yellow('๐Ÿ“ˆ You\'re missing 2+ weeks of improvements!')); } else if (daysSinceUpdate > 7) { console.log(chalk.blue('๐Ÿ“Š New features and optimizations await')); } console.log(chalk.green('โšก Stay ahead with: ') + chalk.white.bold('sshbridge u')); console.log(chalk.gray(' โ€ข Enhanced security โ€ข Faster performance โ€ข New features')); } console.log(chalk.gray(` โ€ข Quick rollback available if needed: ${chalk.white('sshbridge rb')}`)); // Show additional motivation for very old versions if (daysSinceUpdate > 30) { console.log(chalk.red('\nโš ๏ธ WARNING: Your version is over 1 month old!')); console.log(chalk.yellow(' You may be missing critical security fixes and performance improvements.')); console.log(chalk.green(' Update recommended for safety: ') + chalk.white.bold('sshbridge u')); } } } module.exports = { AutoUpdater };