UNPKG

ssh-bridge-ai

Version:

One Command Magic SSH with Invisible Analytics - Connect to any server instantly with 'sshbridge user@server'. Zero setup, zero friction, pure magic. Industry-standard security with behind-the-scenes business intelligence.

352 lines (310 loc) 10.3 kB
const Docker = require('dockerode'); const { EventEmitter } = require('events'); const logger = require('../utils/logger'); /** * Sandbox Manager - Provides isolated execution environment for commands * Similar to Cursor's sandbox approach for safe AI command testing */ class SandboxManager extends EventEmitter { constructor(options = {}) { super(); this.docker = null; this.sandboxImage = options.image || 'ubuntu:22.04'; this.sandboxTimeout = options.timeout || 30000; // 30 seconds this.maxContainerSize = options.maxSize || '100m'; this.enabled = options.enabled !== false; this.initializeDocker(); } /** * Initialize Docker connection */ async initializeDocker() { try { this.docker = new Docker(); // Test Docker connection await this.docker.ping(); logger.info('✅ Docker sandbox initialized successfully'); // Ensure sandbox image is available await this.ensureImageExists(); } catch (error) { logger.warn('⚠️ Docker not available, falling back to local sandbox'); this.enabled = false; this.docker = null; } } /** * Ensure the sandbox image exists */ async ensureImageExists() { try { const images = await this.docker.listImages(); const imageExists = images.some(img => img.RepoTags && img.RepoTags.includes(this.sandboxImage) ); if (!imageExists) { logger.info(`📥 Pulling sandbox image: ${this.sandboxImage}`); await this.docker.pull(this.sandboxImage); logger.info('✅ Sandbox image ready'); } } catch (error) { logger.error('Failed to ensure sandbox image:', error.message); throw error; } } /** * Create a new sandbox container */ async createSandbox(workingDir = '/workspace') { if (!this.enabled || !this.docker) { return this.createLocalSandbox(workingDir); } try { const container = await this.docker.createContainer({ Image: this.sandboxImage, Cmd: ['/bin/bash'], WorkingDir: workingDir, HostConfig: { Memory: 512 * 1024 * 1024, // 512MB RAM limit MemorySwap: 0, DiskQuota: 100 * 1024 * 1024, // 100MB disk limit SecurityOpt: ['no-new-privileges'], CapDrop: ['ALL'], ReadonlyRootfs: false, Binds: [`${process.cwd()}:${workingDir}`] }, Env: [ 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', 'TERM=xterm-256color' ] }); await container.start(); logger.debug(`🔒 Sandbox container created: ${container.id}`); return { type: 'docker', container, id: container.id, workingDir }; } catch (error) { logger.warn('Docker sandbox failed, falling back to local:', error.message); return this.createLocalSandbox(workingDir); } } /** * Create a local sandbox (fallback when Docker unavailable) */ createLocalSandbox(workingDir) { const fs = require('fs-extra'); const path = require('path'); const os = require('os'); const tempDir = path.join(os.tmpdir(), `sshbridge-sandbox-${Date.now()}`); fs.ensureDirSync(tempDir); logger.debug(`📁 Local sandbox created: ${tempDir}`); return { type: 'local', path: tempDir, workingDir: tempDir }; } /** * Execute a command in the sandbox */ async executeInSandbox(command, sandbox) { this.emit('sandbox:execute', { command, sandbox }); if (sandbox.type === 'docker') { return this.executeInDockerSandbox(command, sandbox); } else { return this.executeInLocalSandbox(command, sandbox); } } /** * Execute command in Docker sandbox */ async executeInDockerSandbox(command, sandbox) { try { const exec = await sandbox.container.exec({ Cmd: ['/bin/bash', '-c', command], AttachStdout: true, AttachStderr: true, AttachStdin: false, Tty: false, WorkingDir: sandbox.workingDir }); const stream = await exec.start(); return new Promise((resolve, reject) => { let stdout = ''; let stderr = ''; stream.on('data', (chunk) => { stdout += chunk.toString(); }); stream.on('error', (error) => { stderr += error.toString(); }); stream.on('end', async () => { try { const inspect = await exec.inspect(); resolve({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode: inspect.ExitCode || 0, success: inspect.ExitCode === 0, sandboxType: 'docker', containerId: sandbox.id }); } catch (error) { reject(error); } }); }); } catch (error) { logger.error('Docker sandbox execution failed:', error.message); throw error; } } /** * Execute command in local sandbox */ async executeInLocalSandbox(command, sandbox) { const { spawn } = require('child_process'); const path = require('path'); return new Promise((resolve, reject) => { const childProcess = spawn('/bin/bash', ['-c', command], { cwd: sandbox.path, env: { ...process.env, PATH: process.env.PATH }, stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; childProcess.stdout.on('data', (data) => { stdout += data.toString(); }); childProcess.stderr.on('data', (data) => { stderr += data.toString(); }); childProcess.on('close', (code) => { resolve({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code, success: code === 0, sandboxType: 'local', path: sandbox.path }); }); childProcess.on('error', (error) => { reject(error); }); // Set timeout setTimeout(() => { childProcess.kill(); reject(new Error('Sandbox execution timeout')); }, this.sandboxTimeout); }); } /** * Clean up sandbox resources */ async cleanupSandbox(sandbox) { try { if (sandbox.type === 'docker' && sandbox.container) { await sandbox.container.stop(); await sandbox.container.remove(); logger.debug(`🗑️ Docker sandbox cleaned up: ${sandbox.id}`); } else if (sandbox.type === 'local' && sandbox.path) { const fs = require('fs-extra'); await fs.remove(sandbox.path); logger.debug(`🗑️ Local sandbox cleaned up: ${sandbox.path}`); } } catch (error) { logger.warn('Sandbox cleanup failed:', error.message); } } /** * Test if a command is safe to execute */ isCommandSafe(command, sandboxResult) { // Check for dangerous patterns const dangerousPatterns = [ /rm\s+-rf\s+\//, // rm -rf / /rm\s+-rf\s+\/.*/, // rm -rf /something /dd\s+if=/, // dd if= /mkfs\..*/, // mkfs.ext4, mkfs.xfs, etc. /fdisk\s+/, // fdisk /parted\s+/, // parted /reboot/, // reboot /shutdown/, // shutdown /init\s+[06]/, // init 0, init 6 /killall/, // killall /kill\s+-9/, // kill -9 /chmod\s+777/, // chmod 777 /chown\s+root/, // chown root /mount\s+/, // mount /umount\s+/, // umount /crontab\s+/, // crontab /at\s+/, // at /nohup\s+.*&/, // nohup ... & /screen\s+/, // screen /tmux\s+/, // tmux /history\s+-c/, // history -c /unset\s+HISTFILE/, // unset HISTFILE /curl\s+.*\|\s*sh/, // curl ... | sh /wget\s+.*\|\s*sh/, // wget ... | sh /nc\s+.*-[le]/, // netcat with listen/execute /bash\s+-i\s+.*>&/, // bash -i with redirection /python\s+-c\s+.*['"]/, // python -c with code execution /eval\s*\(/, // eval( /exec\s*\(/, // exec( /\$\([^)]*\)/, // Command substitution /`[^`]*`/, // Backtick command substitution />\s*\/dev\/(null|zero|random)/, // Redirection to /dev /\|\s*sh\s*$/, // Pipe to shell /;\s*rm\s+-rf/, // Semicolon followed by rm -rf /&&\s*rm\s+-rf/, // AND operator with rm -rf /\|\s*rm\s+-rf/, // Pipe to rm -rf /\/dev\/tcp\//, // /dev/tcp/ for network connections ]; // Check if command contains dangerous patterns const commandLower = command.toLowerCase(); for (const pattern of dangerousPatterns) { if (pattern.test(commandLower)) { return { safe: false, reason: `Command blocked: matches dangerous pattern`, pattern: pattern.toString() }; } } // Check sandbox execution result if (sandboxResult && sandboxResult.exitCode !== 0) { return { safe: false, reason: `Command failed in sandbox (exit code: ${sandboxResult.exitCode})`, sandboxOutput: sandboxResult.stderr }; } // Check for excessive output (potential DoS) if (sandboxResult && sandboxResult.stdout.length > 1000000) { // 1MB limit return { safe: false, reason: `Command output too large (${sandboxResult.stdout.length} bytes)`, limit: '1MB' }; } return { safe: true, reason: 'Command passed safety checks' }; } /** * Get sandbox status */ getStatus() { return { enabled: this.enabled, dockerAvailable: !!this.docker, sandboxImage: this.sandboxImage, timeout: this.sandboxTimeout }; } } module.exports = SandboxManager;