mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 07:29:33 +00:00
harden(bash): don't treat a $VAR/path command word as an interpreter
Follow-up on #4007: is_interpreter_variable fired for any bare-$VAR command word, so `"$DIR/run.sh" stage.py` could mint a spurious invokes edge to the argument on top of the real .sh calls edge. A bare interpreter variable ($PYTHON) has no path separator; gate on `"/" not in cmd_word`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
e402b87b73
commit
0110d962b9
2 files changed
+26
-1
No files matched your search
@@ -531,8 +531,14 @@ def extract_bash(path: Path) -> dict:
|
||||
cmd_word = text(cmd_name_node).strip()
|
||||
if cmd_word[0:1] in {"'", '"'} and cmd_word[-1:] == cmd_word[0]:
|
||||
cmd_word = cmd_word[1:-1]
|
||||
# A bare variable standing in for an interpreter (`$PYTHON x.py`)
|
||||
# has no path separator; `"$DIR/run.sh" x.py` runs run.sh with
|
||||
# x.py as its ARGUMENT, so the `/` guard keeps us from minting a
|
||||
# spurious invokes edge to the argument on top of the real
|
||||
# `calls` edge the .sh runner path already emits.
|
||||
is_interpreter_variable = (
|
||||
cmd is None and cmd_word.startswith("$") and "$(" not in cmd_word
|
||||
cmd is None and cmd_word.startswith("$")
|
||||
and "$(" not in cmd_word and "/" not in cmd_word
|
||||
)
|
||||
if (is_known_interpreter or is_interpreter_variable) and args:
|
||||
invoked_raw = literal(args[0])
|
||||
|
||||
@@ -3291,6 +3291,25 @@ def test_extract_bash_skips_invokes_for_a_non_interpreter_command(tmp_path):
|
||||
assert not any(edge.get("relation") == "invokes" for edge in result["edges"])
|
||||
|
||||
|
||||
def test_extract_bash_variable_path_command_does_not_invoke_its_argument(tmp_path):
|
||||
"""A variable-built PATH command (`"$DIR/run.sh" stage.py`) runs run.sh with
|
||||
stage.py as its argument; the `/` in the command word means it is not a bare
|
||||
interpreter variable, so no spurious `invokes` edge to the argument is minted
|
||||
(only the real .sh `calls` edge the runner path already emits)."""
|
||||
stage = tmp_path / "stage.py"
|
||||
stage.write_text("print('stage')\n", encoding="utf-8")
|
||||
runner = tmp_path / "run.sh"
|
||||
runner.write_text("#!/bin/bash\necho run\n", encoding="utf-8")
|
||||
script = tmp_path / "caller.sh"
|
||||
script.write_text('#!/bin/bash\nDIR="."\n"$DIR/run.sh" stage.py\n', encoding="utf-8")
|
||||
|
||||
result = extract_bash(script)
|
||||
assert not any(
|
||||
edge.get("relation") == "invokes" and edge.get("target") == _make_id(str(stage.resolve()))
|
||||
for edge in result["edges"]
|
||||
)
|
||||
|
||||
|
||||
def test_extract_bash_skips_invokes_for_a_missing_script(tmp_path):
|
||||
script = tmp_path / "runner.sh"
|
||||
script.write_text("#!/bin/bash\npython3 does_not_exist.py\n", encoding="utf-8")
|
||||
|
||||
Reference in new issue
Block a user