command-validation.ts
1 import os from "os"; 2 3 /** 4 * Dangerous command patterns that should trigger approval 5 */ 6 const DANGEROUS_PATTERNS = [ 7 // Destructive operations 8 /\brm\b.*-rf?\b/i, 9 /\bdel\b.*\/s\b/i, 10 /\bformat\b/i, 11 /\bmkfs\b/i, 12 13 // System modifications 14 /\bsudo\b/i, 15 /\bsu\b/i, 16 /\bchmod\b.*777/i, 17 18 // Package operations 19 /\b(apt|yum|dnf|brew)\s+(install|remove|purge)/i, 20 /\bnpm\s+(install|uninstall)\s+-g/i, 21 /\bpip\s+install/i, 22 23 // Network operations 24 /\bcurl\b.*\|\s*(bash|sh)/i, 25 /\bwget\b.*\|\s*(bash|sh)/i, 26 27 // Process operations 28 /\bkill\s+-9/i, 29 /\bkillall/i, 30 ]; 31 32 /** 33 * Command substitution patterns (security risk) 34 */ 35 const COMMAND_SUBSTITUTION_PATTERNS = [ 36 /\$\([^)]*\)/, // $(command) 37 /`[^`]*`/, // `command` 38 /<\([^)]*\)/, // <(command) 39 />\([^)]*\)/, // >(command) 40 ]; 41 42 /** 43 * Extract root command from shell command string 44 * Examples: 45 * "ls -la" -> "ls" 46 * "npm install && npm start" -> ["npm", "npm"] 47 * "sudo apt install" -> ["sudo", "apt"] 48 */ 49 export function extractRootCommands(command: string): string[] { 50 const roots: string[] = []; 51 52 // Split by common shell operators 53 const segments = command 54 .split(/[;&|]/) 55 .map((s) => s.trim()) 56 .filter(Boolean); 57 58 for (const segment of segments) { 59 // Remove leading/trailing whitespace and extract first word 60 const firstWord = segment.trim().split(/\s+/)[0]; 61 if (firstWord && !roots.includes(firstWord)) { 62 roots.push(firstWord); 63 } 64 } 65 66 return roots; 67 } 68 69 /** 70 * Check if command contains dangerous patterns 71 */ 72 export function isDangerousCommand(command: string): boolean { 73 return DANGEROUS_PATTERNS.some((pattern) => pattern.test(command)); 74 } 75 76 /** 77 * Check if command contains command substitution 78 */ 79 export function hasCommandSubstitution(command: string): boolean { 80 return COMMAND_SUBSTITUTION_PATTERNS.some((pattern) => pattern.test(command)); 81 } 82 83 /** 84 * Validate command against security policies 85 */ 86 export function validateCommand( 87 command: string, 88 allowCommandSubstitution: boolean = false 89 ): { allowed: boolean; reason?: string } { 90 if (!command || !command.trim()) { 91 return { allowed: false, reason: "Command cannot be empty" }; 92 } 93 94 // Check for command substitution 95 if (!allowCommandSubstitution && hasCommandSubstitution(command)) { 96 return { 97 allowed: false, 98 reason: 99 "Command substitution using $(), ``, <(), or >() is not allowed for security reasons", 100 }; 101 } 102 103 // Extract root commands for approval checking 104 const roots = extractRootCommands(command); 105 if (roots.length === 0) { 106 return { 107 allowed: false, 108 reason: "Could not identify command root for security validation", 109 }; 110 } 111 112 return { allowed: true }; 113 } 114 115 /** 116 * Check if command is in approved list 117 */ 118 export function isCommandApproved( 119 command: string, 120 approvedCommands: Set<string> 121 ): boolean { 122 const roots = extractRootCommands(command); 123 return roots.every((root) => approvedCommands.has(root)); 124 } 125 126 /** 127 * Get platform-specific shell configuration 128 */ 129 export function getShellConfig(): { 130 shell: string; 131 args: string[]; 132 platform: string; 133 } { 134 const platform = os.platform(); 135 136 if (platform === "win32") { 137 return { 138 shell: "powershell.exe", 139 args: ["-NoProfile", "-NonInteractive", "-Command"], 140 platform: "Windows", 141 }; 142 } else { 143 return { 144 shell: "bash", 145 args: ["-c"], 146 platform: "Unix/Mac", 147 }; 148 } 149 }