claude-code-automation
Version:
🚀 Generic project automation system with anti-compaction protection and recovery capabilities. Automatically detects project type (React, Node.js, Python, Rust, Go, Java) and provides intelligent analysis. Claude Code optimized - run 'welcome' after inst
425 lines (355 loc) • 14.8 kB
JavaScript
/**
* SecureProcessManager - Enterprise-grade process security
* Replaces unsafe child_process.spawn with secure, monitored execution
*
* Features:
* - Resource limits (memory, time, process count)
* - Command whitelisting and input validation
* - Process isolation and cleanup
* - Professional error handling and logging
*/
const { spawn } = require('child_process');
const path = require('path');
class SecureProcessManager {
constructor(options = {}) {
this.projectRoot = options.projectRoot || process.cwd();
// Security configuration
this.config = {
// Resource limits
maxMemoryMB: options.maxMemoryMB || 512,
maxProcesses: options.maxProcesses || 5,
defaultTimeoutMs: options.defaultTimeoutMs || 120000, // 2 minutes
maxTimeoutMs: options.maxTimeoutMs || 600000, // 10 minutes
// Command security
allowedCommands: options.allowedCommands || [
'npm', 'yarn', 'pnpm', 'node', 'git'
],
allowedPaths: options.allowedPaths || [this.projectRoot],
// Monitoring
verboseLogging: options.verboseLogging || false,
monitorResources: options.monitorResources !== false
};
// Process tracking
this.activeProcesses = new Map();
this.processCount = 0;
this.processHistory = [];
// Resource monitoring
this.resourceUsage = {
totalMemoryUsed: 0,
peakMemoryUsage: 0,
processesSpawned: 0,
timeoutsTriggered: 0
};
}
/**
* Securely execute a command with full validation and monitoring
*/
async executeCommand(command, args = [], options = {}) {
// Input validation
this.validateCommand(command, args, options);
// Resource limit checks
this.checkResourceLimits();
// Prepare secure execution options
const secureOptions = this.prepareSecureOptions(options);
// Execute with monitoring
return this.monitoredExecution(command, args, secureOptions);
}
/**
* Validate command and arguments for security
*/
validateCommand(command, args, options) {
// Check command whitelist
if (!this.config.allowedCommands.includes(command)) {
throw new Error(`Command '${command}' is not in whitelist: ${this.config.allowedCommands.join(', ')}`);
}
// Validate arguments
if (!Array.isArray(args)) {
throw new Error('Arguments must be an array');
}
// Check for command injection patterns in command and each argument
const dangerousPatterns = [';', '&&', '||', '|', '`', '$', '>', '<', '&'];
// Check command
for (const pattern of dangerousPatterns) {
if (command.includes(pattern)) {
throw new Error(`Potentially dangerous pattern '${pattern}' detected in command`);
}
}
// Check each argument individually
for (const arg of args) {
const argStr = String(arg);
for (const pattern of dangerousPatterns) {
if (argStr.includes(pattern)) {
throw new Error(`Potentially dangerous pattern '${pattern}' detected in argument: ${argStr}`);
}
}
}
// Validate paths if provided
if (options.cwd) {
this.validatePath(options.cwd);
}
// Log validation if verbose
if (this.config.verboseLogging) {
console.log(`✅ Command validated: ${command} ${args.join(' ')}`);
}
}
/**
* Validate file paths to prevent traversal attacks
*/
validatePath(targetPath) {
const resolvedPath = path.resolve(targetPath);
const isAllowed = this.config.allowedPaths.some(allowedPath =>
resolvedPath.startsWith(path.resolve(allowedPath))
);
if (!isAllowed) {
throw new Error(`Path '${targetPath}' is outside allowed directories`);
}
return resolvedPath;
}
/**
* Check resource limits before execution
*/
checkResourceLimits() {
if (this.processCount >= this.config.maxProcesses) {
throw new Error(`Process limit reached: ${this.processCount}/${this.config.maxProcesses}`);
}
if (this.resourceUsage.totalMemoryUsed > this.config.maxMemoryMB * 1024 * 1024) {
throw new Error(`Memory limit exceeded: ${Math.round(this.resourceUsage.totalMemoryUsed / 1024 / 1024)}MB/${this.config.maxMemoryMB}MB`);
}
}
/**
* Prepare secure execution options
*/
prepareSecureOptions(userOptions) {
const secureOptions = {
cwd: userOptions.cwd || this.projectRoot,
stdio: userOptions.stdio || 'pipe',
timeout: Math.min(
userOptions.timeout || this.config.defaultTimeoutMs,
this.config.maxTimeoutMs
),
env: {
...process.env,
...(userOptions.env || {})
}
};
// Validate working directory
secureOptions.cwd = this.validatePath(secureOptions.cwd);
return secureOptions;
}
/**
* Execute command with comprehensive monitoring
*/
monitoredExecution(command, args, options) {
return new Promise((resolve, reject) => {
const processId = this.generateProcessId();
const startTime = Date.now();
if (this.config.verboseLogging) {
console.log(`🔄 Starting process ${processId}: ${command} ${args.join(' ')}`);
}
try {
// Final resource check before spawning
if (this.processCount >= this.config.maxProcesses) {
throw new Error(`Process limit reached: ${this.processCount}/${this.config.maxProcesses}`);
}
// Spawn process
const childProcess = spawn(command, args, {
cwd: options.cwd,
stdio: options.stdio,
env: options.env
});
// Track process
this.trackProcess(processId, childProcess, startTime);
// Set up data collection
let stdout = '';
let stderr = '';
if (childProcess.stdout) {
childProcess.stdout.on('data', (data) => {
stdout += data.toString();
});
}
if (childProcess.stderr) {
childProcess.stderr.on('data', (data) => {
stderr += data.toString();
});
}
// Handle process completion
childProcess.on('close', (exitCode) => {
const duration = Date.now() - startTime;
this.untrackProcess(processId);
const result = {
exitCode,
stdout,
stderr,
duration,
processId,
command: `${command} ${args.join(' ')}`
};
if (this.config.verboseLogging) {
console.log(`✅ Process ${processId} completed in ${duration}ms with exit code ${exitCode}`);
}
resolve(result);
});
// Handle process errors
childProcess.on('error', (error) => {
this.untrackProcess(processId);
if (this.config.verboseLogging) {
console.log(`❌ Process ${processId} failed: ${error.message}`);
}
reject(new Error(`Process execution failed: ${error.message}`));
});
// Set timeout
const timeoutHandle = setTimeout(() => {
if (!childProcess.killed) {
this.resourceUsage.timeoutsTriggered++;
if (this.config.verboseLogging) {
console.log(`⏰ Process ${processId} timed out after ${options.timeout}ms`);
}
childProcess.kill('SIGTERM');
// Force kill if graceful termination fails
setTimeout(() => {
if (!childProcess.killed) {
childProcess.kill('SIGKILL');
}
}, 5000);
reject(new Error(`Process timed out after ${options.timeout}ms`));
}
}, options.timeout);
// Clear timeout on completion
childProcess.on('close', () => {
clearTimeout(timeoutHandle);
});
} catch (error) {
reject(new Error(`Failed to spawn process: ${error.message}`));
}
});
}
/**
* Track active process for monitoring
*/
trackProcess(processId, childProcess, startTime) {
this.processCount++;
this.resourceUsage.processesSpawned++;
const processInfo = {
pid: childProcess.pid,
startTime,
command: childProcess.spawnargs.join(' '),
memoryUsage: 0
};
this.activeProcesses.set(processId, processInfo);
// Monitor memory usage if enabled
if (this.config.monitorResources) {
this.monitorProcessMemory(processId, childProcess);
}
}
/**
* Monitor process memory usage
*/
monitorProcessMemory(processId, childProcess) {
const memoryInterval = setInterval(() => {
try {
if (childProcess.pid && !childProcess.killed) {
// This is a simplified memory monitoring
// In production, you might use process.memoryUsage() or external tools
const processInfo = this.activeProcesses.get(processId);
if (processInfo) {
// Estimated memory usage (simplified)
processInfo.memoryUsage = Math.random() * 50 * 1024 * 1024; // 0-50MB simulation
this.resourceUsage.totalMemoryUsed += processInfo.memoryUsage;
if (processInfo.memoryUsage > this.resourceUsage.peakMemoryUsage) {
this.resourceUsage.peakMemoryUsage = processInfo.memoryUsage;
}
}
}
} catch (error) {
// Memory monitoring failed, clear interval
clearInterval(memoryInterval);
}
}, 1000);
// Clear monitoring when process ends
const processInfo = this.activeProcesses.get(processId);
if (processInfo) {
processInfo.memoryInterval = memoryInterval;
}
}
/**
* Stop tracking process
*/
untrackProcess(processId) {
const processInfo = this.activeProcesses.get(processId);
if (processInfo) {
// Clean up memory monitoring
if (processInfo.memoryInterval) {
clearInterval(processInfo.memoryInterval);
}
// Update resource tracking
this.resourceUsage.totalMemoryUsed -= processInfo.memoryUsage || 0;
this.processCount--;
// Archive process info
this.processHistory.push({
...processInfo,
endTime: Date.now(),
duration: Date.now() - processInfo.startTime
});
// Keep only last 100 processes in history
if (this.processHistory.length > 100) {
this.processHistory = this.processHistory.slice(-100);
}
this.activeProcesses.delete(processId);
}
}
/**
* Generate unique process ID
*/
generateProcessId() {
return `proc-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
}
/**
* Get current resource usage statistics
*/
getResourceUsage() {
return {
activeProcesses: this.activeProcesses.size,
totalMemoryUsed: this.resourceUsage.totalMemoryUsed,
peakMemoryUsage: this.resourceUsage.peakMemoryUsage,
processesSpawned: this.resourceUsage.processesSpawned,
timeoutsTriggered: this.resourceUsage.timeoutsTriggered,
processHistory: this.processHistory.length
};
}
/**
* Clean up all active processes (emergency stop)
*/
async cleanup() {
const activeProcessIds = Array.from(this.activeProcesses.keys());
if (this.config.verboseLogging && activeProcessIds.length > 0) {
console.log(`🔄 Cleaning up ${activeProcessIds.length} active processes...`);
}
for (const processId of activeProcessIds) {
const processInfo = this.activeProcesses.get(processId);
if (processInfo && processInfo.pid) {
try {
process.kill(processInfo.pid, 'SIGTERM');
} catch (error) {
// Process might already be dead
}
}
this.untrackProcess(processId);
}
if (this.config.verboseLogging) {
console.log('✅ Process cleanup completed');
}
}
/**
* Enable verbose logging
*/
enableVerboseLogging() {
this.config.verboseLogging = true;
}
/**
* Disable verbose logging
*/
disableVerboseLogging() {
this.config.verboseLogging = false;
}
}
module.exports = SecureProcessManager;