termcode
Version:
Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative
753 lines (752 loc) • 28.8 kB
JavaScript
import { promises as fs } from "node:fs";
import path from "node:path";
import { log } from "../util/logging.js";
import { hookManager } from "../hooks/manager.js";
/**
* Enhanced Security Sandbox with dynamic validation, policies, and AI-powered detection
* Far superior to Claude Code's basic validation
*/
export class EnhancedSecuritySandbox {
policies = new Map();
ruleCache = new Map();
violationHistory = [];
configPath;
learningMode = true;
constructor(configDir = path.join(process.env.HOME || "~", ".termcode", "security")) {
this.configPath = configDir;
}
/**
* Initialize enhanced sandbox with dynamic policies
*/
async initialize() {
try {
await fs.mkdir(this.configPath, { recursive: true });
await this.loadPolicies();
await this.loadDefaultRules();
log.info(`Enhanced sandbox initialized with ${this.policies.size} policies`);
}
catch (error) {
log.error("Failed to initialize enhanced sandbox:", error);
throw error;
}
}
/**
* Execute command with comprehensive security validation
*/
async validateAndExecute(command, context, options = {}) {
const startTime = Date.now();
const commandArray = Array.isArray(command) ? command : [command];
const commandString = commandArray.join(' ');
// Select applicable policy
const policy = await this.selectPolicy(context, options.policyId);
// Get applicable rules
const rules = await this.getApplicableRules(policy, context);
const result = {
success: true,
blocked: false,
violations: [],
warnings: [],
metadata: {
executionTime: 0,
rulesEvaluated: rules.length,
policyApplied: policy.id
}
};
// Execute hook-based validation first
const hookResults = await hookManager.executeHooks('PreToolUse', {
repoPath: context.repoPath,
currentBranch: 'current',
provider: context.provider,
model: context.model,
sessionId: context.session,
timestamp: Date.now(),
environment: process.env
}, {
toolName: 'shell',
toolInput: { command: commandArray },
originalCommand: commandArray
});
// Process hook results
for (const hookResult of hookResults) {
if (!hookResult.success && hookResult.result.error?.includes('CRITICAL')) {
result.blocked = true;
result.violations.push({
ruleId: hookResult.hookId,
ruleName: 'Hook Validation',
severity: 'critical',
message: hookResult.result.error,
suggestion: hookResult.result.suggestions?.[0],
input: commandString,
context,
timestamp: Date.now()
});
}
if (hookResult.result.suggestions) {
result.warnings.push(...hookResult.result.suggestions);
}
if (hookResult.result.transformedInput) {
result.transformedInput = hookResult.result.transformedInput;
}
}
// Apply security rules
for (const rule of rules) {
const violation = await this.evaluateRule(rule, commandString, context);
if (violation) {
result.violations.push(violation);
switch (rule.action) {
case 'block':
result.blocked = true;
result.success = false;
break;
case 'transform':
if (rule.replacement) {
result.transformedInput = {
command: commandArray.map(cmd => cmd.replace(new RegExp(rule.pattern), rule.replacement))
};
}
break;
case 'warn':
result.warnings.push(violation.message);
break;
}
}
}
// AI-powered anomaly detection
const anomalies = await this.detectAnomalies(commandString, context);
result.warnings.push(...anomalies);
// If blocked, don't execute
if (result.blocked) {
result.metadata.executionTime = Date.now() - startTime;
await this.logViolations(result.violations);
return result;
}
// Execute if not blocked and not dry run
if (!options.dryRun) {
try {
const execResult = await this.executeSecure(result.transformedInput?.command || commandArray, context, options.timeout || 30000);
result.data = execResult;
}
catch (error) {
result.success = false;
result.warnings.push(`Execution failed: ${error}`);
}
}
result.metadata.executionTime = Date.now() - startTime;
await this.logViolations(result.violations);
// Learn from this execution
if (this.learningMode) {
await this.learnFromExecution(commandString, context, result);
}
return result;
}
/**
* Load security policies from configuration
*/
async loadPolicies() {
const policiesPath = path.join(this.configPath, "policies.json");
try {
const data = await fs.readFile(policiesPath, 'utf8');
const policies = JSON.parse(data);
for (const policy of policies) {
this.policies.set(policy.id, policy);
}
}
catch (error) {
if (error.code === 'ENOENT') {
await this.createDefaultPolicies();
}
else {
throw error;
}
}
}
/**
* Create comprehensive default security policies
*/
async createDefaultPolicies() {
const policies = [
{
id: 'development',
name: 'Development Environment',
description: 'Relaxed security for development work',
contexts: ['development'],
strictMode: false,
allowOverrides: true,
rules: []
},
{
id: 'production',
name: 'Production Environment',
description: 'Strict security for production environments',
contexts: ['production', 'staging'],
strictMode: true,
allowOverrides: false,
rules: []
},
{
id: 'ai-assisted',
name: 'AI-Assisted Development',
description: 'Enhanced security for AI-generated code',
contexts: ['ai', 'assistant'],
strictMode: true,
allowOverrides: true,
rules: []
}
];
for (const policy of policies) {
this.policies.set(policy.id, policy);
}
await this.savePolicies();
}
/**
* Load comprehensive default security rules
*/
async loadDefaultRules() {
const defaultRules = [
// Critical system security
{
id: 'critical_system_delete',
name: 'Critical System Delete',
description: 'Prevents deletion of critical system files',
type: 'command',
pattern: /rm\s+-rf\s+\/(bin|boot|dev|etc|lib|proc|root|sbin|sys|usr\/bin|usr\/sbin)/,
severity: 'critical',
action: 'block',
suggestion: 'This command could damage your system. Use specific file paths instead.',
enabled: true
},
{
id: 'sudo_privilege_escalation',
name: 'Sudo Privilege Escalation',
description: 'Monitors sudo usage for security',
type: 'command',
pattern: /sudo\s+/,
severity: 'high',
action: 'warn',
suggestion: 'Verify this command requires elevated privileges',
enabled: true
},
{
id: 'remote_code_execution',
name: 'Remote Code Execution',
description: 'Blocks downloading and executing scripts',
type: 'command',
pattern: /(curl|wget)\s+.*?\|\s*(sh|bash|python|node)/,
severity: 'critical',
action: 'block',
suggestion: 'Download and review scripts before execution',
enabled: true
},
// Network security
{
id: 'network_listener',
name: 'Network Listener',
description: 'Monitors creation of network listeners',
type: 'command',
pattern: /nc\s+-l|netcat\s+-l|python.*SimpleHTTPServer|python.*http\.server/,
severity: 'medium',
action: 'warn',
suggestion: 'Review network listener configuration for security',
enabled: true
},
{
id: 'data_exfiltration',
name: 'Data Exfiltration',
description: 'Monitors potential data exfiltration',
type: 'command',
pattern: /(scp|rsync|curl\s+-X\s+POST)\s+.*?[^@]+@/,
severity: 'high',
action: 'warn',
suggestion: 'Verify data transfer destination and content',
enabled: true
},
// Code injection and execution
{
id: 'code_injection',
name: 'Code Injection',
description: 'Detects potential code injection patterns',
type: 'command',
pattern: /eval\s*\(|exec\s*\(|system\s*\(|shell_exec\s*\(/,
severity: 'high',
action: 'warn',
suggestion: 'Avoid dynamic code execution when possible',
enabled: true
},
{
id: 'dangerous_interpreters',
name: 'Dangerous Interpreter Usage',
description: 'Monitors usage of interpreters with user input',
type: 'command',
pattern: /(python|node|ruby)\s+-c\s+["'].*\$.*["']/,
severity: 'medium',
action: 'warn',
suggestion: 'Validate and sanitize any user input in interpreter commands',
enabled: true
},
// File system security
{
id: 'sensitive_file_access',
name: 'Sensitive File Access',
description: 'Monitors access to sensitive files',
type: 'path',
pattern: /\/(etc\/passwd|etc\/shadow|\.ssh\/|\.aws\/|\.kube\/)/,
severity: 'high',
action: 'warn',
suggestion: 'Accessing sensitive files - ensure proper authorization',
enabled: true
},
{
id: 'config_modification',
name: 'Configuration Modification',
description: 'Monitors changes to configuration files',
type: 'command',
pattern: />\s*(\/etc\/|\/usr\/|\/var\/|~\/\.(bash|zsh|ssh))/,
severity: 'medium',
action: 'warn',
suggestion: 'Review configuration changes carefully',
enabled: true
},
// Package manager security
{
id: 'package_install_sudo',
name: 'Package Install with Sudo',
description: 'Monitors privileged package installations',
type: 'command',
pattern: /sudo\s+(npm|pip|apt|yum|brew|pacman)\s+install/,
severity: 'medium',
action: 'warn',
suggestion: 'Use user-level package managers when possible',
enabled: true
},
{
id: 'npm_audit_bypass',
name: 'NPM Audit Bypass',
description: 'Detects attempts to bypass npm security',
type: 'command',
pattern: /npm.*--audit\s+(false|off)|npm.*--no-audit/,
severity: 'medium',
action: 'warn',
suggestion: 'Avoid bypassing security audits',
enabled: true
},
// Git security
{
id: 'git_credential_exposure',
name: 'Git Credential Exposure',
description: 'Prevents committing credentials',
type: 'content',
pattern: /(password|secret|key|token)\s*[:=]\s*["'][^"']{8,}["']/i,
severity: 'high',
action: 'block',
suggestion: 'Remove credentials from code - use environment variables',
enabled: true
},
{
id: 'git_force_push',
name: 'Git Force Push',
description: 'Monitors dangerous git operations',
type: 'command',
pattern: /git\s+push\s+.*--force(-with-lease)?/,
severity: 'medium',
action: 'warn',
suggestion: 'Force push can overwrite history - ensure this is intended',
enabled: true
},
// AI/LLM specific security
{
id: 'prompt_injection',
name: 'Prompt Injection',
description: 'Detects potential prompt injection attempts',
type: 'content',
pattern: /(ignore previous|forget instructions|new instructions:|system:|assistant:|human:)/i,
severity: 'medium',
action: 'warn',
suggestion: 'Possible prompt injection attempt detected',
enabled: true
},
{
id: 'model_extraction',
name: 'Model Extraction',
description: 'Detects attempts to extract model information',
type: 'content',
pattern: /(what model are you|your training data|your weights|export model)/i,
severity: 'low',
action: 'warn',
suggestion: 'Potential model probing detected',
enabled: true
},
// Performance and resource security
{
id: 'resource_exhaustion',
name: 'Resource Exhaustion',
description: 'Prevents resource exhaustion attacks',
type: 'command',
pattern: /:(){ :|:&}|while true|for\s+i in \{1\.\.[0-9]{6,}\}/,
severity: 'critical',
action: 'block',
suggestion: 'This could exhaust system resources',
enabled: true
},
{
id: 'large_file_operations',
name: 'Large File Operations',
description: 'Monitors operations on large files',
type: 'command',
pattern: /dd\s+.*bs=[0-9]+[MG]|fallocate.*[0-9]+[MG]/,
severity: 'medium',
action: 'warn',
suggestion: 'Large file operation detected - verify disk space',
enabled: true
}
];
// Assign rules to policies
const devPolicy = this.policies.get('development');
const prodPolicy = this.policies.get('production');
const aiPolicy = this.policies.get('ai-assisted');
if (devPolicy) {
devPolicy.rules = defaultRules.filter(rule => rule.severity === 'critical' || rule.severity === 'high');
}
if (prodPolicy) {
prodPolicy.rules = defaultRules; // All rules for production
}
if (aiPolicy) {
aiPolicy.rules = defaultRules.filter(rule => rule.type === 'command' || rule.id.includes('injection') ||
rule.id.includes('ai') || rule.id.includes('model'));
}
await this.savePolicies();
}
/**
* Select appropriate security policy based on context
*/
async selectPolicy(context, policyId) {
if (policyId && this.policies.has(policyId)) {
return this.policies.get(policyId);
}
// Auto-select based on context
if (context.environment === 'production') {
return this.policies.get('production');
}
// AI context detection
if (context.provider || context.model) {
return this.policies.get('ai-assisted');
}
return this.policies.get('development');
}
/**
* Get applicable rules for context
*/
async getApplicableRules(policy, context) {
const cacheKey = `${policy.id}-${context.environment}-${context.projectType}`;
if (this.ruleCache.has(cacheKey)) {
return this.ruleCache.get(cacheKey);
}
let rules = policy.rules.filter(rule => rule.enabled);
// Context-specific rule filtering
if (context.projectType === 'javascript' || context.framework?.includes('node')) {
rules = rules.filter(rule => !rule.id.includes('python') || rule.severity === 'critical');
}
if (context.environment === 'development') {
rules = rules.filter(rule => rule.severity !== 'low');
}
this.ruleCache.set(cacheKey, rules);
return rules;
}
/**
* Evaluate a security rule against input
*/
async evaluateRule(rule, input, context) {
let matches = false;
try {
if (rule.pattern instanceof RegExp) {
matches = rule.pattern.test(input);
}
else {
matches = input.includes(rule.pattern);
}
if (matches) {
return {
ruleId: rule.id,
ruleName: rule.name,
severity: rule.severity,
message: rule.description,
suggestion: rule.suggestion,
input,
context,
timestamp: Date.now()
};
}
}
catch (error) {
log.warn(`Error evaluating rule ${rule.id}:`, error);
}
return null;
}
/**
* AI-powered anomaly detection
*/
async detectAnomalies(command, context) {
const anomalies = [];
// Pattern-based anomaly detection
const suspiciousPatterns = [
// Unusual character combinations
/[^\x20-\x7E]{3,}/, // Non-printable characters
/\$\{[^}]+\}/g, // Variable substitution
/`[^`]+`/g, // Command substitution
/\|\s*\|/, // Double pipe
/;\s*;/, // Double semicolon
/&&\s*&&/, // Double AND
/\\\\/, // Escaped backslash
];
for (const pattern of suspiciousPatterns) {
if (pattern.test(command)) {
anomalies.push(`Suspicious pattern detected in command: ${pattern.source}`);
}
}
// Statistical anomaly detection
const stats = this.analyzeCommandStatistics(command);
if (stats.complexity > 0.8) {
anomalies.push('Command has unusually high complexity');
}
if (stats.entropy > 5.0) {
anomalies.push('Command has high entropy (possible obfuscation)');
}
// Context-based anomaly detection
if (context.provider && !this.isTypicalAICommand(command)) {
const similarity = await this.calculateCommandSimilarity(command, context);
if (similarity < 0.3) {
anomalies.push('Command is unusual for AI-assisted development');
}
}
return anomalies;
}
/**
* Analyze command complexity and entropy
*/
analyzeCommandStatistics(command) {
// Complexity based on special characters, nesting, etc.
const specialChars = (command.match(/[|&;<>(){}[\]$`\\]/g) || []).length;
const complexity = Math.min(specialChars / command.length, 1);
// Shannon entropy calculation
const chars = new Map();
for (const char of command) {
chars.set(char, (chars.get(char) || 0) + 1);
}
let entropy = 0;
for (const count of chars.values()) {
const p = count / command.length;
entropy -= p * Math.log2(p);
}
return {
complexity,
entropy,
length: command.length
};
}
/**
* Check if command is typical for AI development
*/
isTypicalAICommand(command) {
const typicalPatterns = [
/npm\s+(install|run|test)/,
/git\s+(add|commit|push|pull|status)/,
/(cd|ls|mkdir|touch|cat|grep|find)/,
/python\s+(.*\.py|test|setup\.py)/,
/node\s+(.*\.js|test)/,
/(cargo|go)\s+(build|test|run)/,
/docker\s+(build|run|ps|images)/
];
return typicalPatterns.some(pattern => pattern.test(command));
}
/**
* Calculate similarity to typical commands in context
*/
async calculateCommandSimilarity(command, context) {
// Simple similarity based on common words and patterns
// In production, this could use more sophisticated NLP
const commandWords = command.toLowerCase().split(/\s+/);
const commonWords = [
'npm', 'install', 'run', 'test', 'build',
'git', 'add', 'commit', 'push', 'pull',
'cd', 'ls', 'mkdir', 'cat', 'grep',
'python', 'node', 'cargo', 'go'
];
const matches = commandWords.filter(word => commonWords.includes(word));
return matches.length / Math.max(commandWords.length, 1);
}
/**
* Execute command with enhanced security
*/
async executeSecure(command, context, timeout) {
const { spawn } = await import("node:child_process");
// Create restricted environment
const env = this.createRestrictedEnvironment(context);
return new Promise((resolve, reject) => {
const child = spawn(command[0], command.slice(1), {
cwd: context.repoPath,
env,
stdio: ['pipe', 'pipe', 'pipe'],
timeout
});
let stdout = '';
let stderr = '';
child.stdout?.on('data', (data) => {
stdout += data.toString();
});
child.stderr?.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
resolve({ stdout, stderr, code: code || 0 });
});
child.on('error', (error) => {
reject(error);
});
});
}
/**
* Create restricted environment for command execution
*/
createRestrictedEnvironment(context) {
const safeEnvVars = [
'PATH', 'HOME', 'USER', 'PWD', 'TERM',
'NODE_ENV', 'NODE_PATH', 'PYTHON_PATH',
'LANG', 'LC_ALL', 'TZ'
];
const env = {};
for (const key of safeEnvVars) {
if (process.env[key]) {
env[key] = process.env[key];
}
}
// Add context-specific variables
env.TERMCODE_PROVIDER = context.provider;
env.TERMCODE_MODEL = context.model;
env.TERMCODE_SESSION = context.session;
env.TERMCODE_ENVIRONMENT = context.environment;
return env;
}
/**
* Log security violations
*/
async logViolations(violations) {
if (violations.length === 0)
return;
this.violationHistory.push(...violations);
// Keep only recent violations (last 1000)
if (this.violationHistory.length > 1000) {
this.violationHistory = this.violationHistory.slice(-1000);
}
// Log to file
const logPath = path.join(this.configPath, 'violations.log');
const logEntries = violations.map(v => JSON.stringify(v)).join('\n') + '\n';
try {
await fs.appendFile(logPath, logEntries);
}
catch (error) {
log.warn('Failed to write violation log:', error);
}
}
/**
* Learn from execution patterns (ML-style adaptation)
*/
async learnFromExecution(command, context, result) {
// Simple learning: adjust rule sensitivity based on false positives
if (result.success && result.violations.length > 0) {
// If execution succeeded despite violations, maybe rules are too strict
for (const violation of result.violations) {
if (violation.severity === 'low') {
// Consider reducing sensitivity of this rule
log.debug(`Learning: Command '${command}' succeeded despite violation ${violation.ruleId}`);
}
}
}
// Track command patterns for anomaly detection baseline
const stats = this.analyzeCommandStatistics(command);
// In a full implementation, this would update ML models
}
/**
* Save policies to disk
*/
async savePolicies() {
const policiesPath = path.join(this.configPath, "policies.json");
const policies = Array.from(this.policies.values());
try {
await fs.writeFile(policiesPath, JSON.stringify(policies, null, 2));
}
catch (error) {
log.error('Failed to save security policies:', error);
}
}
/**
* Public API methods
*/
/**
* Add custom security rule
*/
async addRule(policyId, rule) {
const policy = this.policies.get(policyId);
if (!policy) {
throw new Error(`Policy ${policyId} not found`);
}
policy.rules.push(rule);
this.ruleCache.clear(); // Clear cache
await this.savePolicies();
log.info(`Security rule ${rule.id} added to policy ${policyId}`);
}
/**
* Get security statistics
*/
getSecurityStats() {
const violationsBySeverity = {};
const ruleFrequency = new Map();
for (const violation of this.violationHistory) {
violationsBySeverity[violation.severity] =
(violationsBySeverity[violation.severity] || 0) + 1;
ruleFrequency.set(violation.ruleId, (ruleFrequency.get(violation.ruleId) || 0) + 1);
}
const mostTriggeredRules = Array.from(ruleFrequency.entries())
.map(([ruleId, count]) => ({ ruleId, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 10);
// Calculate recent trends (last 7 days)
const now = Date.now();
const oneDayMs = 24 * 60 * 60 * 1000;
const recentTrends = [];
for (let i = 6; i >= 0; i--) {
const dayStart = now - (i * oneDayMs);
const dayEnd = dayStart + oneDayMs;
const dayViolations = this.violationHistory.filter(v => v.timestamp >= dayStart && v.timestamp < dayEnd).length;
recentTrends.push({
date: new Date(dayStart).toISOString().split('T')[0],
violations: dayViolations
});
}
return {
totalViolations: this.violationHistory.length,
violationsBySeverity,
mostTriggeredRules,
recentTrends
};
}
/**
* Enable/disable learning mode
*/
setLearningMode(enabled) {
this.learningMode = enabled;
log.info(`Security learning mode ${enabled ? 'enabled' : 'disabled'}`);
}
/**
* Get policy by ID
*/
getPolicy(policyId) {
return this.policies.get(policyId);
}
/**
* List all policies
*/
listPolicies() {
return Array.from(this.policies.values());
}
}
// Export singleton instance
export const enhancedSandbox = new EnhancedSecuritySandbox();