fix: stop extracting patterns at command flags and revert command-validation changes

This commit is contained in:
Daniel Riccio 2025-07-24 11:56:55 -05:00
parent 8b4150e017
commit 627ea8462d
No known key found for this signature in database
GPG key ID: FFD5FD825F8E8209
2 changed files with 174 additions and 27 deletions

View file

@ -41,7 +41,7 @@ export function extractPatternsFromCommand(command: string): string[] {
}
function isValidToken(token: string): boolean {
return !!token && !token.match(/[/\\~:]/) && token !== "." && !token.match(/\.\w+$/)
return !!token && !token.startsWith("-") && !token.match(/[/\\~:]/) && token !== "." && !token.match(/\.\w+$/)
}
function extractFromTokens(tokens: string[], patterns: Set<string>): void {

View file

@ -1,5 +1,7 @@
import { parse } from "shell-quote"
type ShellToken = string | { op: string } | { command: string }
/**
* # Command Denylist Feature - Longest Prefix Match Strategy
*
@ -70,38 +72,183 @@ import { parse } from "shell-quote"
export function parseCommand(command: string): string[] {
if (!command?.trim()) return []
try {
const parsed = parse(command)
const commands: string[] = []
let currentCommand: string[] = []
// Split by newlines first (handle different line ending formats)
// This regex splits on \r\n (Windows), \n (Unix), or \r (old Mac)
const lines = command.split(/\r\n|\r|\n/)
const allCommands: string[] = []
for (const token of parsed) {
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)
for (const line of lines) {
// Skip empty lines
if (!line.trim()) continue
// Process each line through the existing parsing logic
const lineCommands = parseCommandLine(line)
allCommands.push(...lineCommands)
}
return allCommands
}
/**
* Parse a single line of commands (internal helper function)
*/
function parseCommandLine(command: string): string[] {
if (!command?.trim()) return []
// Storage for replaced content
const redirections: string[] = []
const subshells: string[] = []
const quotes: string[] = []
const arrayIndexing: string[] = []
const arithmeticExpressions: string[] = []
const variables: string[] = []
const parameterExpansions: string[] = []
const processSubstitutions: string[] = []
// First handle PowerShell redirections by temporarily replacing them
let processedCommand = command.replace(/\d*>&\d*/g, (match) => {
redirections.push(match)
return `__REDIR_${redirections.length - 1}__`
})
// Handle arithmetic expressions: $((...)) pattern
// Match the entire arithmetic expression including nested parentheses
processedCommand = processedCommand.replace(/\$\(\([^)]*(?:\)[^)]*)*\)\)/g, (match) => {
arithmeticExpressions.push(match)
return `__ARITH_${arithmeticExpressions.length - 1}__`
})
// Handle parameter expansions: ${...} patterns (including array indexing)
// This covers ${var}, ${var:-default}, ${var:+alt}, ${#var}, ${var%pattern}, etc.
processedCommand = processedCommand.replace(/\$\{[^}]+\}/g, (match) => {
parameterExpansions.push(match)
return `__PARAM_${parameterExpansions.length - 1}__`
})
// Handle process substitutions: <(...) and >(...)
processedCommand = processedCommand.replace(/[<>]\([^)]+\)/g, (match) => {
processSubstitutions.push(match)
return `__PROCSUB_${processSubstitutions.length - 1}__`
})
// Handle simple variable references: $varname pattern
// This prevents shell-quote from splitting $count into separate tokens
processedCommand = processedCommand.replace(/\$[a-zA-Z_][a-zA-Z0-9_]*/g, (match) => {
variables.push(match)
return `__VAR_${variables.length - 1}__`
})
// Handle special bash variables: $?, $!, $#, $$, $@, $*, $-, $0-$9
processedCommand = processedCommand.replace(/\$[?!#$@*\-0-9]/g, (match) => {
variables.push(match)
return `__VAR_${variables.length - 1}__`
})
// Then handle subshell commands
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
processedCommand = processedCommand.replace(/"[^"]*"/g, (match) => {
quotes.push(match)
return `__QUOTE_${quotes.length - 1}__`
})
let tokens: ShellToken[]
try {
tokens = parse(processedCommand) as ShellToken[]
} catch (error: any) {
// If shell-quote fails to parse, fall back to simple splitting
console.warn("shell-quote parse error:", error.message, "for command:", processedCommand)
// Simple fallback: split by common operators
const fallbackCommands = processedCommand
.split(/(?:&&|\|\||;|\|)/)
.map((cmd) => cmd.trim())
.filter((cmd) => cmd.length > 0)
// Restore all placeholders for each command
return fallbackCommands.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)])
// Restore array indexing expressions
result = result.replace(/__ARRAY_(\d+)__/g, (_, i) => arrayIndexing[parseInt(i)])
// Restore arithmetic expressions
result = result.replace(/__ARITH_(\d+)__/g, (_, i) => arithmeticExpressions[parseInt(i)])
// Restore parameter expansions
result = result.replace(/__PARAM_(\d+)__/g, (_, i) => parameterExpansions[parseInt(i)])
// Restore process substitutions
result = result.replace(/__PROCSUB_(\d+)__/g, (_, i) => processSubstitutions[parseInt(i)])
// Restore variable references
result = result.replace(/__VAR_(\d+)__/g, (_, i) => variables[parseInt(i)])
return result
})
}
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 if (typeof token === "string") {
} 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(" "))
}
return commands
} catch (_error) {
// If shell-quote fails, fall back to simple splitting
return [command]
}
// 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)])
// Restore array indexing expressions
result = result.replace(/__ARRAY_(\d+)__/g, (_, i) => arrayIndexing[parseInt(i)])
// Restore arithmetic expressions
result = result.replace(/__ARITH_(\d+)__/g, (_, i) => arithmeticExpressions[parseInt(i)])
// Restore parameter expansions
result = result.replace(/__PARAM_(\d+)__/g, (_, i) => parameterExpansions[parseInt(i)])
// Restore process substitutions
result = result.replace(/__PROCSUB_(\d+)__/g, (_, i) => processSubstitutions[parseInt(i)])
// Restore variable references
result = result.replace(/__VAR_(\d+)__/g, (_, i) => variables[parseInt(i)])
return result
})
}
/**