| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126 |
- import { parse } from 'shell-quote'
- type ShellToken = string | { op: string } | { command: string }
- /**
- * Split a command string into individual sub-commands by
- * chaining operators (&&, ||, ;, or |).
- *
- * Uses shell-quote to properly handle:
- * - Quoted strings (preserves quotes)
- * - Subshell commands ($(cmd) or `cmd`)
- * - PowerShell redirections (2>&1)
- * - Chain operators (&&, ||, ;, |)
- */
- export function parseCommand(command: string): string[] {
- if (!command?.trim()) return []
- // First handle PowerShell redirections by temporarily replacing them
- const redirections: string[] = []
- let processedCommand = command.replace(/\d*>&\d*/g, (match) => {
- redirections.push(match)
- return `__REDIR_${redirections.length - 1}__`
- })
- // Then handle subshell commands
- const subshells: string[] = []
- processedCommand = processedCommand
- .replace(/\$\((.*?)\)/g, (_, inner) => {
- subshells.push(inner.trim())
- return `__SUBSH_${subshells.length - 1}__`
- })
- .replace(/`(.*?)`/g, (_, inner) => {
- subshells.push(inner.trim())
- return `__SUBSH_${subshells.length - 1}__`
- })
- // Then handle quoted strings
- const quotes: string[] = []
- processedCommand = processedCommand.replace(/"[^"]*"/g, (match) => {
- quotes.push(match)
- return `__QUOTE_${quotes.length - 1}__`
- })
- const tokens = parse(processedCommand) as ShellToken[]
- const commands: string[] = []
- let currentCommand: string[] = []
- for (const token of tokens) {
- if (typeof token === 'object' && 'op' in token) {
- // Chain operator - split command
- if (['&&', '||', ';', '|'].includes(token.op)) {
- if (currentCommand.length > 0) {
- commands.push(currentCommand.join(' '))
- currentCommand = []
- }
- } else {
- // Other operators (>, &) are part of the command
- currentCommand.push(token.op)
- }
- } else if (typeof token === 'string') {
- // Check if it's a subshell placeholder
- const subshellMatch = token.match(/__SUBSH_(\d+)__/)
- if (subshellMatch) {
- if (currentCommand.length > 0) {
- commands.push(currentCommand.join(' '))
- currentCommand = []
- }
- commands.push(subshells[parseInt(subshellMatch[1])])
- } else {
- currentCommand.push(token)
- }
- }
- }
- // Add any remaining command
- if (currentCommand.length > 0) {
- commands.push(currentCommand.join(' '))
- }
- // Restore quotes and redirections
- return commands.map(cmd => {
- let result = cmd
- // Restore quotes
- result = result.replace(/__QUOTE_(\d+)__/g, (_, i) => quotes[parseInt(i)])
- // Restore redirections
- result = result.replace(/__REDIR_(\d+)__/g, (_, i) => redirections[parseInt(i)])
- return result
- })
- }
- /**
- * Check if a single command is allowed based on prefix matching.
- */
- export function isAllowedSingleCommand(
- command: string,
- allowedCommands: string[]
- ): boolean {
- if (!command || !allowedCommands?.length) return false
- const trimmedCommand = command.trim().toLowerCase()
- return allowedCommands.some(prefix =>
- trimmedCommand.startsWith(prefix.toLowerCase())
- )
- }
- /**
- * Check if a command string is allowed based on the allowed command prefixes.
- * This version also blocks subshell attempts by checking for `$(` or `` ` ``.
- */
- export function validateCommand(command: string, allowedCommands: string[]): boolean {
- if (!command?.trim()) return true
- // Block subshell execution attempts
- if (command.includes('$(') || command.includes('`')) {
- return false
- }
- // Parse into sub-commands (split by &&, ||, ;, |)
- const subCommands = parseCommand(command)
- // Then ensure every sub-command starts with an allowed prefix
- return subCommands.every(cmd => {
- // Remove simple PowerShell-like redirections (e.g. 2>&1) before checking
- const cmdWithoutRedirection = cmd.replace(/\d*>&\d*/, '').trim()
- return isAllowedSingleCommand(cmdWithoutRedirection, allowedCommands)
- })
- }
|