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
JavaScript
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 };