termcode
Version:
Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative
324 lines (323 loc) • 10.9 kB
JavaScript
import { log } from "../util/logging.js";
import path from "node:path";
/**
* Enhanced security sandbox for command execution
* Inspired by Claude Code's safety-first approach
*/
export class SecuritySandbox {
config;
dangerousPatterns = [
// System modification
/rm\s+-rf\s+\//,
/sudo\s+/,
/chmod\s+777/,
/chown\s+/,
// Network operations
/curl\s+.*?\|\s*sh/,
/wget\s+.*?\|\s*sh/,
/nc\s+-l/,
// Process manipulation
/kill\s+-9/,
/killall/,
/pkill/,
// Data exfiltration
/scp\s+/,
/rsync\s+.*?:/,
/ssh\s+.*?@/,
// Dangerous file operations
/>\s*\/dev\/null/,
/>\s*\/dev\/zero/,
/cat\s+\/etc\/passwd/,
/cat\s+\/etc\/shadow/,
// Command injection
/;\s*rm\s+/,
/&&\s*rm\s+/,
/\|\s*rm\s+/,
/`.*`/,
/\$\(.*\)/
];
constructor(config = {}) {
this.config = {
allowedCommands: [
// Package managers & build tools
"npm", "pnpm", "yarn", "node", "python", "pip", "pipx", "uv",
// Test runners
"pytest", "npx", "jest", "vitest", "mocha",
// Compilers & tools
"go", "cargo", "tsc", "rustc", "javac", "gcc", "clang",
// Linters & formatters
"eslint", "prettier", "ruff", "flake8", "golangci-lint", "clippy",
// Version control
"git",
// Safe utilities
"cat", "ls", "find", "grep", "head", "tail", "wc", "sort",
// Database tools (read-only)
"sqlite3"
],
blockedPaths: [
"/etc",
"/sys",
"/proc",
"/dev",
"/boot",
"/root",
"/var/log",
"/usr/bin",
"/usr/sbin",
"/sbin",
"/bin"
],
allowedPaths: [
process.cwd(),
"/tmp",
"/var/tmp"
],
timeout: 300000, // 5 minutes
maxOutputSize: 10 * 1024 * 1024, // 10MB
...config
};
}
/**
* Execute command in sandbox with security checks
*/
async execute(command, cwd, stdin) {
// Pre-execution security checks
const securityCheck = this.checkSecurity(command, cwd);
if (!securityCheck.safe) {
return {
ok: false,
blocked: securityCheck.reason
};
}
// Sanitize environment
const sanitizedEnv = this.sanitizeEnvironment();
try {
// Execute with restricted environment
const result = await this.executeRestricted(command, cwd, stdin, sanitizedEnv);
// Post-execution validation
if (result.ok) {
const validation = this.validateOutput(result.data);
if (!validation.safe) {
return {
ok: false,
error: `Output validation failed: ${validation.reason}`
};
}
}
return result;
}
catch (error) {
return {
ok: false,
error: error instanceof Error ? error.message : "Sandbox execution failed"
};
}
}
/**
* Check command security before execution
*/
checkSecurity(command, cwd) {
const [cmd, ...args] = command;
// Check if command is allowed
if (!this.config.allowedCommands.includes(cmd)) {
return {
safe: false,
reason: `Command '${cmd}' not in allowlist. Allowed: ${this.config.allowedCommands.join(", ")}`
};
}
// Check working directory
const absoluteCwd = path.resolve(cwd);
const isAllowedPath = this.config.allowedPaths.some(allowedPath => absoluteCwd.startsWith(path.resolve(allowedPath)));
if (!isAllowedPath) {
return {
safe: false,
reason: `Working directory '${cwd}' not in allowed paths`
};
}
// Check for blocked paths in arguments
for (const arg of args) {
if (typeof arg === "string") {
const resolvedArg = path.resolve(cwd, arg);
const isBlockedPath = this.config.blockedPaths.some(blockedPath => resolvedArg.startsWith(blockedPath));
if (isBlockedPath) {
return {
safe: false,
reason: `Argument '${arg}' references blocked path`
};
}
}
}
// Check for dangerous patterns
const fullCommand = command.join(" ");
for (const pattern of this.dangerousPatterns) {
if (pattern.test(fullCommand)) {
return {
safe: false,
reason: `Command contains dangerous pattern: ${pattern.source}`
};
}
}
// Additional npm/node security checks
if (cmd === "npm" || cmd === "npx" || cmd === "yarn" || cmd === "pnpm") {
return this.checkPackageManagerSecurity(args);
}
return { safe: true };
}
/**
* Additional security checks for package managers
*/
checkPackageManagerSecurity(args) {
const dangerousNpmCommands = ["publish", "adduser", "login", "logout"];
const dangerousFlags = ["--unsafe-perm", "--allow-root", "--no-audit"];
for (const arg of args) {
if (dangerousNpmCommands.includes(arg)) {
return {
safe: false,
reason: `Dangerous npm command: ${arg}`
};
}
if (dangerousFlags.includes(arg)) {
return {
safe: false,
reason: `Dangerous flag: ${arg}`
};
}
}
return { safe: true };
}
/**
* Sanitize environment variables
*/
sanitizeEnvironment() {
const safe = {
PATH: process.env.PATH || "",
HOME: process.env.HOME || "",
USER: process.env.USER || "",
NODE_ENV: process.env.NODE_ENV || "development",
// Preserve necessary Node.js variables
NODE_PATH: process.env.NODE_PATH || "",
npm_config_cache: process.env.npm_config_cache || "",
// Preserve CI variables for testing
CI: process.env.CI || "",
GITHUB_ACTIONS: process.env.GITHUB_ACTIONS || ""
};
// Remove sensitive variables
const sensitivePatterns = [/KEY/, /SECRET/, /TOKEN/, /PASSWORD/, /AUTH/];
for (const [key, value] of Object.entries(process.env)) {
if (!sensitivePatterns.some(pattern => pattern.test(key.toUpperCase()))) {
safe[key] = value || "";
}
}
return safe;
}
/**
* Execute command with restrictions
*/
async executeRestricted(command, cwd, stdin, env) {
const { spawn } = await import("node:child_process");
return new Promise((resolve) => {
const child = spawn(command[0], command.slice(1), {
cwd,
env: env || process.env,
stdio: ["pipe", "pipe", "pipe"],
timeout: this.config.timeout
});
let stdout = "";
let stderr = "";
let outputSize = 0;
child.stdout?.on("data", (data) => {
const chunk = data.toString();
outputSize += chunk.length;
if (outputSize > this.config.maxOutputSize) {
child.kill("SIGKILL");
resolve({
ok: false,
error: "Output size limit exceeded"
});
return;
}
stdout += chunk;
});
child.stderr?.on("data", (data) => {
const chunk = data.toString();
outputSize += chunk.length;
if (outputSize > this.config.maxOutputSize) {
child.kill("SIGKILL");
resolve({
ok: false,
error: "Output size limit exceeded"
});
return;
}
stderr += chunk;
});
child.on("close", (code, signal) => {
if (signal === "SIGKILL") {
resolve({
ok: false,
error: "Command killed due to timeout or resource limits"
});
}
else {
resolve({
ok: true,
data: {
stdout,
stderr,
code: code || 0
}
});
}
});
child.on("error", (error) => {
resolve({
ok: false,
error: error.message
});
});
// Send stdin if provided
if (stdin && child.stdin) {
child.stdin.write(stdin);
child.stdin.end();
}
});
}
/**
* Validate command output for security issues
*/
validateOutput(data) {
const combined = data.stdout + data.stderr;
// Check for credential leaks
const credentialPatterns = [
/(?:password|passwd|pwd)\s*[:=]\s*\S+/i,
/(?:api[_-]?key|apikey)\s*[:=]\s*\S+/i,
/(?:secret|token)\s*[:=]\s*\S+/i,
/-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/,
/ssh-rsa\s+[A-Za-z0-9+\/]+=*/
];
for (const pattern of credentialPatterns) {
if (pattern.test(combined)) {
log.warn("Potential credential leak detected in command output");
return {
safe: false,
reason: "Output contains potential credentials"
};
}
}
return { safe: true };
}
/**
* Update sandbox configuration
*/
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
}
/**
* Get current sandbox configuration
*/
getConfig() {
return { ...this.config };
}
}
// Export default sandbox instance
export const sandbox = new SecuritySandbox();