fix(#829): guard prefixed runtime variable launches
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
This commit is contained in:
@@ -79,7 +79,7 @@ DIRECT_PATTERNS: Final = (
|
||||
re.compile(r"\beval\s+[\"'](?:claude|pi)(?:\s|[\"'])"),
|
||||
)
|
||||
RUNTIME_ASSIGNMENT: Final = re.compile(
|
||||
r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*[\"']?(?:claude|pi)(?:\s|[\"']|$)"
|
||||
r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*[\"']?(?:claude|pi)(?:\s|[\"']|[;&|]|$)"
|
||||
)
|
||||
TYPESCRIPT_WRAPPER_INVOCATION: Final = re.compile(
|
||||
r"(?m)^\s*execRuntime\s*\(\s*[\"']python3[\"']\s*,\s*"
|
||||
@@ -226,44 +226,71 @@ def is_gated_line(path: Path, line: str) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def is_shell_direct_invocation(path: Path, line: str) -> bool:
|
||||
def shell_command_tokens(path: Path, line: str) -> list[str]:
|
||||
if path.suffix.lower() not in SHELL_SUFFIXES:
|
||||
return False
|
||||
return []
|
||||
assignment = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
|
||||
case_arm = re.match(r"^\s*[^;&()]+\)\s*", line)
|
||||
command_source = line[case_arm.end() :] if case_arm is not None else line
|
||||
resolved: list[str] = []
|
||||
for command in shell_commands(command_source):
|
||||
index = 0
|
||||
while index < len(command) and assignment.match(command[index]):
|
||||
index += 1
|
||||
while index < len(command) and command[index] in {"command", "exec", "nohup"}:
|
||||
index += 1
|
||||
if index < len(command) and command[index] == "env":
|
||||
index += 1
|
||||
while index < len(command) and (
|
||||
command[index].startswith("-") or assignment.match(command[index])
|
||||
):
|
||||
# Resolve command position once for every literal and variable caller.
|
||||
# Prefixes may be nested in either order (for example `exec env A=1`).
|
||||
while index < len(command):
|
||||
if command[index] in {"command", "exec", "nohup"}:
|
||||
index += 1
|
||||
while index < len(command) and command[index] in {"command", "exec", "nohup"}:
|
||||
continue
|
||||
if command[index] == "env":
|
||||
index += 1
|
||||
if index < len(command) and Path(command[index]).name in {"claude", "pi"}:
|
||||
return True
|
||||
return False
|
||||
while index < len(command) and (
|
||||
command[index].startswith("-") or assignment.match(command[index])
|
||||
):
|
||||
index += 1
|
||||
continue
|
||||
break
|
||||
if index < len(command):
|
||||
resolved.append(command[index])
|
||||
return resolved
|
||||
|
||||
|
||||
def is_direct_line(path: Path, line: str) -> bool:
|
||||
return is_shell_direct_invocation(path, line) or any(
|
||||
def runtime_variable_reference(token: str, runtime_variables: set[str]) -> bool:
|
||||
match = re.fullmatch(r"\$(?:([A-Za-z_][A-Za-z0-9_]*)|\{([A-Za-z_][A-Za-z0-9_]*)\})", token)
|
||||
if match is None:
|
||||
return False
|
||||
return (match.group(1) or match.group(2)) in runtime_variables
|
||||
|
||||
|
||||
def is_shell_direct_invocation(
|
||||
path: Path, line: str, runtime_variables: set[str]
|
||||
) -> bool:
|
||||
return any(
|
||||
Path(token).name in {"claude", "pi"}
|
||||
or runtime_variable_reference(token, runtime_variables)
|
||||
for token in shell_command_tokens(path, line)
|
||||
)
|
||||
|
||||
|
||||
def is_direct_line(path: Path, line: str, runtime_variables: set[str]) -> bool:
|
||||
return is_shell_direct_invocation(path, line, runtime_variables) or any(
|
||||
pattern.search(line) for pattern in DIRECT_PATTERNS
|
||||
)
|
||||
|
||||
|
||||
def executes_runtime_variable(line: str, runtime_variables: set[str]) -> bool:
|
||||
def executes_runtime_variable(
|
||||
path: Path, line: str, runtime_variables: set[str]
|
||||
) -> bool:
|
||||
if any(
|
||||
runtime_variable_reference(token, runtime_variables)
|
||||
for token in shell_command_tokens(path, line)
|
||||
):
|
||||
return True
|
||||
for variable in runtime_variables:
|
||||
reference = rf"\$(?:{re.escape(variable)}|\{{{re.escape(variable)}\}})"
|
||||
if re.search(rf"\beval\s+[\"']?{reference}", line):
|
||||
return True
|
||||
if re.match(rf"^\s*[\"']?{reference}[\"']?(?:\s|$)", line):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -293,8 +320,8 @@ def classify_text(path: Path, source: str) -> list[LaunchSite]:
|
||||
if assignment is not None:
|
||||
runtime_variables.add(assignment.group(1))
|
||||
primitive_violation = DANGEROUS_PRIMITIVE in line and not is_choke_point(path)
|
||||
line_is_direct = is_direct_line(path, line) or executes_runtime_variable(
|
||||
line, runtime_variables
|
||||
line_is_direct = is_direct_line(path, line, runtime_variables) or executes_runtime_variable(
|
||||
path, line, runtime_variables
|
||||
)
|
||||
line_is_gated = line_number in validated_typescript_wrappers or is_gated_line(path, line)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user