Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions tests/tools/test_hardline_blocklist.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,3 +447,40 @@ def test_container_bypasses_self_host_kill(clean_session):
# container namespace are not the host gateway's.
result = check_all_command_guards(f"kill {_os.getpid()}", "docker")
assert result["approved"] is True


def test_self_host_kill_blocks_command_and_builtin_wrappers(clean_session):
# `command kill` and `builtin kill` are plain shell spellings that
# still execute kill; they must not slip past the command-position
# anchor.
pid = _os.getpid()
assert _check_self_host_kill(f"command kill {pid}")[0] is True
assert _check_self_host_kill(f"builtin kill {pid}")[0] is True
assert _check_self_host_kill("command -p kill $$")[0] is True
assert _check_self_host_kill(f"command builtin kill {pid}")[0] is True


def test_self_host_kill_blocks_standard_wrapper_prefixes(clean_session):
pid = _os.getpid()
assert _check_self_host_kill(f"sudo kill {pid}")[0] is True
assert _check_self_host_kill(f"env kill {pid}")[0] is True
assert _check_self_host_kill("exec kill $PPID")[0] is True
assert _check_self_host_kill(f"nohup setsid kill -9 {pid}")[0] is True


def test_self_host_kill_allows_foreign_pid_via_wrappers(clean_session):
assert _check_self_host_kill("command kill 999999999")[0] is False
assert _check_self_host_kill("builtin kill -9 999999998")[0] is False


def test_self_host_kill_allows_command_v_lookup(clean_session):
# `command -v kill` resolves the name without executing kill, so it
# is not a wrapper; only `command [-p]` executes its operand.
assert _check_self_host_kill(f"command -v kill {_os.getpid()}")[0] is False


def test_check_all_command_guards_blocks_wrapped_self_host_kill(clean_session):
for cmd in (f"command kill {_os.getpid()}", f"builtin kill {_os.getpid()}"):
result = check_all_command_guards(cmd, "local")
assert result["approved"] is False, cmd
assert result.get("hardline") is True, cmd
18 changes: 12 additions & 6 deletions tools/approval.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,13 +236,15 @@ def _is_gateway_approval_context() -> bool:
# patterns so they don't fire on "echo reboot" or "grep 'shutdown' log".
# Matches: start of string, after command separators (; && || | newline),
# after subshell openers ( `$(` or backtick ), optionally consuming
# leading wrapper commands (sudo, env VAR=VAL, exec, nohup, setsid).
# leading wrapper commands (sudo, env VAR=VAL, exec, nohup, setsid,
# `command [-p]`, builtin). Only `command -p` is treated as a wrapper:
# `command -v`/`-V` resolve a name without executing it.
_CMDPOS = (
r'(?:^|[;&|\n`]|\$\()' # start position
r'\s*' # optional whitespace
r'(?:sudo\s+(?:-[^\s]+\s+)*)?' # optional sudo with flags
r'(?:env\s+(?:\w+=\S*\s+)*)?' # optional env with VAR=VAL pairs
r'(?:(?:exec|nohup|setsid|time)\s+)*' # optional wrapper commands
r'(?:(?:exec|nohup|setsid|time|builtin|command(?:\s+-p)?)\s+)*' # optional wrapper commands
r'\s*'
)

Expand Down Expand Up @@ -343,13 +345,17 @@ def detect_hardline_command(command: str) -> tuple:
# (desktop app, launchd, `hermes gateway restart`), not the hosted agent.
#
# Static patterns can't express "our PID", so this is a function guard
# like the sudo-stdin one: it extracts kill/pkill numeric targets and the
# like the sudo-stdin one: it extracts numeric ``kill`` targets and the
# shell self-tokens ``$$`` / ``$PPID`` and compares against this process
# and its parent. Process-group kills (negative PIDs) are intentionally
# out of scope here — ``kill -1`` is already hardline-blocked above.
# and its parent. ``kill`` is anchored at command position via _CMDPOS,
# so wrapper spellings that still execute it (``command kill``,
# ``builtin kill``, ``sudo``/``env``/``exec``/``nohup``/``setsid``/
# ``time`` prefixes, and chains of those) are recognized too.
# Process-group kills (negative PIDs) are intentionally out of scope
# here — ``kill -1`` is already hardline-blocked above.

_KILL_CMD_RE = re.compile(
r'(?:^|[;&|`\n]|&&|\|\||\$\()\s*kill\s+(?P<args>[^;&|`\n]*)',
_CMDPOS + r'kill\s+(?P<args>[^;&|`\n]*)',
re.IGNORECASE)
_KILL_SELF_TOKEN_RE = re.compile(r'\$\$|\$\{?PPID\}?\b')

Expand Down
Loading