claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
292 lines (256 loc) • 6.72 kB
text/typescript
/**
* Secure Execution Utilities for MDAP
*
* Provides secure command execution with whitelisting and input validation
* to prevent command injection vulnerabilities.
*
* @module secure-execution
* @version 1.0.0
*/
import { spawn } from 'child_process';
import { promisify } from 'util';
// =============================================
// Types
// =============================================
export interface SecureExecutionOptions {
/** Working directory for command execution */
cwd?: string;
/** Maximum buffer size for output (default: 50MB) */
maxBuffer?: number;
/** Timeout in milliseconds (default: 30000) */
timeout?: number;
/** Environment variables (default: process.env) */
env?: Record<string, string>;
}
export interface SecureExecutionResult {
/** Exit code */
code: number | null;
/** Standard output */
stdout: string;
/** Standard error */
stderr: string;
/** Whether execution timed out */
timedOut: boolean;
/** Execution error if any */
error?: Error;
}
// =============================================
// Constants
// =============================================
/** Whitelist of allowed compiler commands */
const ALLOWED_COMMANDS = new Set([
'tsc',
'tsc-watch',
'rustc',
'cargo',
'gcc',
'g++',
'clang',
'clang++',
'javac',
'go',
'python',
'python3',
'node',
'npm',
'yarn',
'pnpm',
'pytest',
'jest',
'mocha',
'eslint',
'prettier',
]);
/** Dangerous patterns in command arguments */
const DANGEROUS_PATTERNS = [
/;/g,
/&&/g,
/\|\|/g,
/\|/g,
/>/g,
/</g,
/`/g,
/\$/g,
/\${/g,
/\(/g,
/\)/g,
/&/g,
];
// =============================================
// Security Functions
// =============================================
/**
* Validates if a command is whitelisted
* @param command Command to validate
* @returns Whether command is allowed
*/
export function isCommandAllowed(command: string): boolean {
// Extract base command (first word)
const baseCommand = command.trim().split(/\s+/)[0];
// Check against whitelist
return ALLOWED_COMMANDS.has(baseCommand);
}
/**
* Sanitizes command arguments to prevent injection
* @param args Command arguments
* @returns Sanitized arguments
*/
export function sanitizeArgs(args: string[]): string[] {
return args.map(arg => {
// Remove dangerous patterns
let sanitized = arg;
DANGEROUS_PATTERNS.forEach(pattern => {
sanitized = sanitized.replace(pattern, '');
});
return sanitized;
});
}
/**
* Validates and parses a command string
* @param commandString Full command string
* @returns Parsed and validated command parts
* @throws Error if command is not allowed or contains dangerous patterns
*/
export function validateCommand(commandString: string): { command: string; args: string[] } {
// Parse command into parts
const parts = commandString.trim().split(/\s+/);
if (parts.length === 0) {
throw new Error('Empty command');
}
const command = parts[0];
const args = parts.slice(1);
// Check if command is whitelisted
if (!isCommandAllowed(command)) {
throw new Error(`Command not allowed: ${command}`);
}
// Sanitize arguments
const sanitizedArgs = sanitizeArgs(args);
return { command, args: sanitizedArgs };
}
// =============================================
// Secure Execution Functions
// =============================================
/**
* Executes a command securely using spawn
* @param command Command to execute
* @param options Execution options
* @returns Promise resolving to execution result
*/
export async function secureSpawn(
command: string,
options: SecureExecutionOptions = {}
): Promise<SecureExecutionResult> {
// Validate command
const { command: validatedCommand, args } = validateCommand(command);
// Set default options
const {
cwd = process.cwd(),
maxBuffer = 50 * 1024 * 1024, // 50MB
timeout = 30000, // 30 seconds
env = process.env as Record<string, string>,
} = options;
return new Promise((resolve) => {
let stdout = '';
let stderr = '';
let timedOut = false;
// Spawn the process
const child = spawn(validatedCommand, args, {
cwd,
env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: false,
});
// Collect output
child.stdout?.on('data', (data) => {
stdout += data.toString();
if (stdout.length > maxBuffer) {
child.kill();
timedOut = true;
}
});
child.stderr?.on('data', (data) => {
stderr += data.toString();
if (stderr.length > maxBuffer) {
child.kill();
timedOut = true;
}
});
// Set timeout
const timeoutId = setTimeout(() => {
child.kill();
timedOut = true;
}, timeout);
// Handle process completion
child.on('close', (code) => {
clearTimeout(timeoutId);
resolve({
code,
stdout: stdout.slice(0, maxBuffer),
stderr: stderr.slice(0, maxBuffer),
timedOut,
});
});
// Handle errors
child.on('error', (error) => {
clearTimeout(timeoutId);
resolve({
code: null,
stdout: '',
stderr: '',
timedOut,
error,
});
});
});
}
/**
* Executes a command synchronously (for backward compatibility)
* Note: This still uses spawn under the hood for security
* @param command Command to execute
* @param options Execution options
* @returns Execution result
*/
export async function secureExecSync(
command: string,
options: SecureExecutionOptions = {}
): Promise<{ stdout: string; stderr: string }> {
const result = await secureSpawn(command, options);
if (result.error) {
throw result.error;
}
if (result.timedOut) {
throw new Error(`Command timed out: ${command}`);
}
if (result.code !== 0 && result.code !== null) {
const error = new Error(`Command failed with exit code ${result.code}: ${command}`) as any;
error.stdout = result.stdout;
error.stderr = result.stderr;
error.code = result.code;
throw error;
}
return {
stdout: result.stdout,
stderr: result.stderr,
};
}
/**
* Adds a command to the whitelist
* @param command Command to add
*/
export function addAllowedCommand(command: string): void {
ALLOWED_COMMANDS.add(command);
}
/**
* Removes a command from the whitelist
* @param command Command to remove
*/
export function removeAllowedCommand(command: string): void {
ALLOWED_COMMANDS.delete(command);
}
/**
* Gets the current whitelist of allowed commands
* @returns Array of allowed commands
*/
export function getAllowedCommands(): string[] {
return Array.from(ALLOWED_COMMANDS);
}