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