/ src / utils / command-validation.ts
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  }