/** * V3 Bash Safety Hook * * TypeScript conversion of V2 bash-hook.sh. * Provides command safety analysis, dangerous command detection, * secret detection, and safe alternatives. * * @module v3/shared/hooks/safety/bash-safety */ import { HookEvent, HookPriority, } from '../types.js'; /** * Dangerous command patterns */ const DANGEROUS_PATTERNS = [ // Critical - Always block { pattern: /rm\s+(-[rRf]+\s+)*\//, type: 'destructive', severity: 'critical', description: 'Recursive deletion from root directory', block: true, }, { pattern: /rm\s+-rf\s+\/\*/, type: 'destructive', severity: 'critical', description: 'Recursive deletion of all root files', block: true, }, { pattern: /dd\s+if=.*of=\/dev\/(sd|hd|nvme)/, type: 'destructive', severity: 'critical', description: 'Direct disk write that can destroy data', block: true, }, { pattern: /mkfs\./, type: 'destructive', severity: 'critical', description: 'Filesystem formatting command', block: true, }, { pattern: />\s*\/dev\/sd[a-z]/, type: 'destructive', severity: 'critical', description: 'Direct write to disk device', block: true, }, { // Fork bomb patterns - various formats (with flexible spacing) pattern: /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:|bomb\s*\(\)|while\s+true.*fork/, type: 'resource', severity: 'critical', description: 'Fork bomb detected', block: true, }, { pattern: /chmod\s+(-R\s+)?777\s+\//, type: 'privilege', severity: 'critical', description: 'Setting dangerous permissions on root', block: true, }, // High - Block but offer alternatives { pattern: /rm\s+-rf\s+\*/, type: 'destructive', severity: 'high', description: 'Recursive deletion of all files in directory', block: true, }, { pattern: /rm\s+-rf\s+\.\//, type: 'destructive', severity: 'high', description: 'Recursive deletion of current directory', block: true, }, { pattern: /rm\s+-rf\s+~/, type: 'destructive', severity: 'high', description: 'Recursive deletion of home directory', block: true, }, { pattern: /curl.*\|\s*(bash|sh|zsh)/, type: 'dangerous', severity: 'high', description: 'Piping remote content directly to shell', block: true, }, { pattern: /wget.*-O-\s*\|\s*(bash|sh|zsh)/, type: 'dangerous', severity: 'high', description: 'Piping remote content directly to shell', block: true, }, { pattern: /eval\s+.*\$\(/, type: 'dangerous', severity: 'high', description: 'Dynamic code execution with command substitution', block: true, }, // Medium - Warn { pattern: /rm\s+(?!.*-i)/, type: 'destructive', severity: 'medium', description: 'Remove command without interactive flag', block: false, }, { pattern: /sudo\s+rm/, type: 'privilege', severity: 'medium', description: 'Privileged file deletion', block: false, }, { pattern: /sudo\s+chmod/, type: 'privilege', severity: 'medium', description: 'Privileged permission change', block: false, }, { pattern: /git\s+push\s+.*--force/, type: 'destructive', severity: 'medium', description: 'Force push can overwrite remote history', block: false, }, { pattern: /git\s+reset\s+--hard/, type: 'destructive', severity: 'medium', description: 'Hard reset discards uncommitted changes', block: false, }, { pattern: /DROP\s+(DATABASE|TABLE)/i, type: 'destructive', severity: 'high', description: 'Database/table deletion command', block: false, }, { pattern: /TRUNCATE\s+TABLE/i, type: 'destructive', severity: 'medium', description: 'Table truncation command', block: false, }, // Low - Informational { pattern: /kill\s+-9/, type: 'dangerous', severity: 'low', description: 'Force kill signal prevents graceful shutdown', block: false, }, { pattern: /killall/, type: 'dangerous', severity: 'low', description: 'Kills all processes by name', block: false, }, ]; /** * Secret patterns to detect and redact */ const SECRET_PATTERNS = [ { pattern: /(password|passwd|pwd)\s*[=:]\s*['"]?([^\s'"]+)/i, name: 'password', redactGroup: 2 }, { pattern: /(api[_-]?key)\s*[=:]\s*['"]?([^\s'"]+)/i, name: 'API key', redactGroup: 2 }, { pattern: /(secret[_-]?key)\s*[=:]\s*['"]?([^\s'"]+)/i, name: 'secret key', redactGroup: 2 }, { pattern: /(access[_-]?token)\s*[=:]\s*['"]?([^\s'"]+)/i, name: 'access token', redactGroup: 2 }, { pattern: /(auth[_-]?token)\s*[=:]\s*['"]?([^\s'"]+)/i, name: 'auth token', redactGroup: 2 }, { pattern: /(bearer)\s+([a-zA-Z0-9._-]+)/i, name: 'bearer token', redactGroup: 2 }, { pattern: /(private[_-]?key)\s*[=:]\s*['"]?([^\s'"]+)/i, name: 'private key', redactGroup: 2 }, { pattern: /(\bsk-[a-zA-Z0-9]{20,})/i, name: 'OpenAI API key' }, { pattern: /(\bghp_[a-zA-Z0-9]{36,})/i, name: 'GitHub token' }, { pattern: /(\bnpm_[a-zA-Z0-9]{36,})/i, name: 'npm token' }, { pattern: /(AKIA[0-9A-Z]{16})/i, name: 'AWS access key' }, ]; /** * Common dependencies to check */ const DEPENDENCY_CHECKS = [ { command: /\bjq\b/, dependency: 'jq' }, { command: /\byq\b/, dependency: 'yq' }, { command: /\bawk\b/, dependency: 'awk' }, { command: /\bsed\b/, dependency: 'sed' }, { command: /\bcurl\b/, dependency: 'curl' }, { command: /\bwget\b/, dependency: 'wget' }, { command: /\bgit\b/, dependency: 'git' }, { command: /\bdocker\b/, dependency: 'docker' }, { command: /\bkubectl\b/, dependency: 'kubectl' }, { command: /\bpython3?\b/, dependency: 'python' }, { command: /\bnode\b/, dependency: 'node' }, { command: /\bnpm\b/, dependency: 'npm' }, { command: /\byarn\b/, dependency: 'yarn' }, { command: /\bpnpm\b/, dependency: 'pnpm' }, ]; /** * Safe alternatives for dangerous commands (with patterns for matching) */ const SAFE_ALTERNATIVES = [ { pattern: /rm\s+-rf\s+\*/, alternatives: [ 'rm -ri * (interactive mode)', 'find . -maxdepth 1 -type f -delete (only files)', 'git clean -fd (for git repositories)', ], }, { pattern: /rm\s+-rf/, alternatives: [ 'rm -ri (interactive mode)', 'trash-cli (move to trash instead)', 'mv to backup directory first', ], }, { pattern: /kill\s+-9/, alternatives: [ 'kill (graceful termination first)', 'kill -15 (SIGTERM)', 'systemctl stop (for services)', ], }, { pattern: /curl.*\|\s*(bash|sh|zsh)/, alternatives: [ 'Download script first, review, then execute', 'Use package managers when available', 'Verify script hash before execution', ], }, { pattern: /wget.*\|\s*(bash|sh|zsh)/, alternatives: [ 'Download script first, review, then execute', 'Use package managers when available', 'Verify script hash before execution', ], }, { pattern: /git\s+push.*--force/, alternatives: [ 'git push --force-with-lease (safer)', 'Create backup branch first', 'git push --force-if-includes', ], }, { pattern: /git\s+reset\s+--hard/, alternatives: [ 'git stash (save changes first)', 'git reset --soft (keep changes staged)', 'Create backup branch first', ], }, ]; /** * Bash Safety Hook Manager */ export class BashSafetyHook { registry; blockedCommands = new Set(); availableDependencies = new Set(); constructor(registry) { this.registry = registry; this.registerHooks(); this.detectAvailableDependencies(); } /** * Register bash safety hooks */ registerHooks() { this.registry.register(HookEvent.PreCommand, this.analyzeCommand.bind(this), HookPriority.Critical, { name: 'bash-safety:pre-command' }); } /** * Detect available dependencies */ async detectAvailableDependencies() { // In a real implementation, this would check which commands are available // For now, assume common ones are available const commonDeps = ['git', 'node', 'npm', 'curl', 'sed', 'awk']; commonDeps.forEach(dep => this.availableDependencies.add(dep)); } /** * Analyze a command for safety */ async analyzeCommand(context) { const commandInfo = context.command; if (!commandInfo) { return this.createResult('low', false, []); } const command = commandInfo.command; const risks = []; const warnings = []; let blocked = false; let blockReason; let modifiedCommand; let safeAlternatives; // Check for dangerous patterns for (const pattern of DANGEROUS_PATTERNS) { if (pattern.pattern.test(command)) { risks.push({ type: pattern.type, severity: pattern.severity, description: pattern.description, pattern: pattern.pattern.toString(), }); if (pattern.block) { blocked = true; blockReason = pattern.description; } // Find safe alternatives using pattern matching for (const { pattern: altPattern, alternatives } of SAFE_ALTERNATIVES) { if (altPattern.test(command)) { safeAlternatives = alternatives; break; } } } } // Check for secrets const { secrets, redactedCommand } = this.detectSecrets(command); for (const secret of secrets) { risks.push({ type: 'secret', severity: 'high', description: `Potential ${secret.name} detected in command`, }); warnings.push(`Detected potential secret: ${secret.name}`); } // Check for missing dependencies const missingDependencies = this.checkDependencies(command); // Add -i flag to rm commands if not present if (/\brm\s+/.test(command) && !/-i\b/.test(command) && !blocked) { modifiedCommand = command.replace(/\brm\s+/, 'rm -i '); warnings.push('Added -i flag for interactive confirmation'); } // Calculate overall risk level const riskLevel = this.calculateRiskLevel(risks); // Determine if we should proceed const shouldProceed = !blocked; return { success: true, riskLevel, blocked, blockReason, modifiedCommand, risks, safeAlternatives, warnings: warnings.length > 0 ? warnings : undefined, missingDependencies: missingDependencies.length > 0 ? missingDependencies : undefined, redactedCommand: secrets.length > 0 ? redactedCommand : undefined, abort: blocked, data: blocked ? undefined : { command: { ...commandInfo, command: modifiedCommand || command, isDestructive: risks.some(r => r.type === 'destructive'), }, }, }; } /** * Detect secrets in command */ detectSecrets(command) { const secrets = []; let redactedCommand = command; for (const { pattern, name, redactGroup } of SECRET_PATTERNS) { const match = pattern.exec(command); if (match) { secrets.push({ name, position: match.index }); // Redact the secret value if (redactGroup && match[redactGroup]) { redactedCommand = redactedCommand.replace(match[redactGroup], '[REDACTED]'); } else { redactedCommand = redactedCommand.replace(match[0], `[REDACTED_${name.toUpperCase().replace(/\s/g, '_')}]`); } } } return { secrets, redactedCommand }; } /** * Check for missing dependencies */ checkDependencies(command) { const missing = []; for (const { command: pattern, dependency } of DEPENDENCY_CHECKS) { if (pattern.test(command) && !this.availableDependencies.has(dependency)) { missing.push(dependency); } } return missing; } /** * Calculate overall risk level */ calculateRiskLevel(risks) { if (risks.length === 0) { return 'low'; } const severities = risks.map(r => r.severity); if (severities.includes('critical')) { return 'critical'; } if (severities.includes('high')) { return 'high'; } if (severities.includes('medium')) { return 'medium'; } return 'low'; } /** * Create a result object */ createResult(riskLevel, blocked, risks) { return { success: true, riskLevel, blocked, risks, }; } /** * Manually analyze a command */ async analyze(command) { const context = { event: HookEvent.PreCommand, timestamp: new Date(), command: { command }, }; return this.analyzeCommand(context); } /** * Add a custom dangerous pattern */ addDangerousPattern(pattern, type, severity, description, block = true) { DANGEROUS_PATTERNS.push({ pattern, type, severity, description, block }); } /** * Mark a dependency as available */ markDependencyAvailable(dependency) { this.availableDependencies.add(dependency); } /** * Check if a command would be blocked */ wouldBlock(command) { for (const pattern of DANGEROUS_PATTERNS) { if (pattern.block && pattern.pattern.test(command)) { return true; } } return false; } } /** * Create bash safety hook */ export function createBashSafetyHook(registry) { return new BashSafetyHook(registry); } //# sourceMappingURL=bash-safety.js.map