mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-29 07:21:37 +08:00
Compare commits
1 Commits
fix/plugin
...
claude-cod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9055f91a4 |
@@ -821,3 +821,206 @@ class TestChmodExecuteCombo:
|
|||||||
assert dangerous is False
|
assert dangerous is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestMacOSPrivateSystemPaths:
|
||||||
|
"""Inspired by Claude Code 2.1.113 "dangerous path protection".
|
||||||
|
|
||||||
|
On macOS, /etc, /var, /tmp, /home are symlinks to
|
||||||
|
/private/{etc,var,tmp,home}. A command that writes to
|
||||||
|
/private/etc/sudoers works identically to /etc/sudoers but bypasses
|
||||||
|
a plain "/etc/" pattern check. These tests guard the shared
|
||||||
|
_SYSTEM_CONFIG_PATH fragment used across redirect / tee / cp / mv /
|
||||||
|
install / sed -i patterns.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_private_etc_redirect(self):
|
||||||
|
dangerous, _, desc = detect_dangerous_command(
|
||||||
|
"echo 'root ALL=NOPASSWD: ALL' > /private/etc/sudoers"
|
||||||
|
)
|
||||||
|
assert dangerous is True
|
||||||
|
assert "system config" in desc.lower()
|
||||||
|
|
||||||
|
def test_private_var_redirect(self):
|
||||||
|
dangerous, _, _ = detect_dangerous_command(
|
||||||
|
"echo payload > /private/var/db/dslocal/nodes/x"
|
||||||
|
)
|
||||||
|
assert dangerous is True
|
||||||
|
|
||||||
|
def test_private_etc_via_tee(self):
|
||||||
|
dangerous, _, desc = detect_dangerous_command(
|
||||||
|
"echo malicious | tee /private/etc/hosts"
|
||||||
|
)
|
||||||
|
assert dangerous is True
|
||||||
|
assert "tee" in desc.lower() or "system" in desc.lower()
|
||||||
|
|
||||||
|
def test_private_etc_cp(self):
|
||||||
|
dangerous, _, desc = detect_dangerous_command(
|
||||||
|
"cp malicious.conf /private/etc/hosts"
|
||||||
|
)
|
||||||
|
assert dangerous is True
|
||||||
|
assert "copy" in desc.lower() or "system config" in desc.lower()
|
||||||
|
|
||||||
|
def test_private_etc_mv(self):
|
||||||
|
dangerous, _, _ = detect_dangerous_command(
|
||||||
|
"mv evil /private/etc/ssh/sshd_config"
|
||||||
|
)
|
||||||
|
assert dangerous is True
|
||||||
|
|
||||||
|
def test_private_etc_install(self):
|
||||||
|
dangerous, _, _ = detect_dangerous_command(
|
||||||
|
"install -m 600 key /private/etc/ssh/keys"
|
||||||
|
)
|
||||||
|
assert dangerous is True
|
||||||
|
|
||||||
|
def test_private_etc_sed_in_place(self):
|
||||||
|
dangerous, _, desc = detect_dangerous_command(
|
||||||
|
"sed -i 's/root/pwned/' /private/etc/passwd"
|
||||||
|
)
|
||||||
|
assert dangerous is True
|
||||||
|
assert "in-place" in desc.lower() or "system config" in desc.lower()
|
||||||
|
|
||||||
|
def test_private_var_sed_long_flag(self):
|
||||||
|
dangerous, _, _ = detect_dangerous_command(
|
||||||
|
"sed --in-place 's/x/y/' /private/var/log/wtmp"
|
||||||
|
)
|
||||||
|
assert dangerous is True
|
||||||
|
|
||||||
|
def test_private_tmp_cp(self):
|
||||||
|
dangerous, _, _ = detect_dangerous_command(
|
||||||
|
"cp rootkit /private/tmp/payload"
|
||||||
|
)
|
||||||
|
assert dangerous is True
|
||||||
|
|
||||||
|
def test_ls_private_is_safe(self):
|
||||||
|
"""Reading under /private/ must not trigger approval."""
|
||||||
|
dangerous, _, _ = detect_dangerous_command("ls /private")
|
||||||
|
assert dangerous is False
|
||||||
|
|
||||||
|
def test_echo_mentioning_private_path_is_safe(self):
|
||||||
|
"""Literal mention of /private/etc in an echo string must not fire."""
|
||||||
|
dangerous, _, _ = detect_dangerous_command(
|
||||||
|
"echo 'the macOS path is /private/etc on disk'"
|
||||||
|
)
|
||||||
|
assert dangerous is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestKillallKillSignals:
|
||||||
|
"""Inspired by Claude Code 2.1.113 expanded deny rules.
|
||||||
|
|
||||||
|
The existing pattern caught `pkill -9` but not the equivalent
|
||||||
|
`killall -9` / `-KILL` / `-s KILL` / `-r <regex>` broad sweeps that
|
||||||
|
can wipe out unrelated processes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_killall_dash_9(self):
|
||||||
|
dangerous, _, desc = detect_dangerous_command("killall -9 firefox")
|
||||||
|
assert dangerous is True
|
||||||
|
assert "kill" in desc.lower()
|
||||||
|
|
||||||
|
def test_killall_dash_kill(self):
|
||||||
|
dangerous, _, _ = detect_dangerous_command("killall -KILL firefox")
|
||||||
|
assert dangerous is True
|
||||||
|
|
||||||
|
def test_killall_dash_sigkill(self):
|
||||||
|
dangerous, _, _ = detect_dangerous_command("killall -SIGKILL firefox")
|
||||||
|
assert dangerous is True
|
||||||
|
|
||||||
|
def test_killall_dash_s_kill(self):
|
||||||
|
dangerous, _, _ = detect_dangerous_command("killall -s KILL firefox")
|
||||||
|
assert dangerous is True
|
||||||
|
|
||||||
|
def test_killall_dash_s_signum(self):
|
||||||
|
dangerous, _, _ = detect_dangerous_command("killall -s 9 firefox")
|
||||||
|
assert dangerous is True
|
||||||
|
|
||||||
|
def test_killall_regex(self):
|
||||||
|
"""killall -r <regex> is a broad sweep; require approval."""
|
||||||
|
dangerous, _, desc = detect_dangerous_command("killall -r 'fire.*'")
|
||||||
|
assert dangerous is True
|
||||||
|
assert "regex" in desc.lower() or "kill" in desc.lower()
|
||||||
|
|
||||||
|
def test_killall_combined_flags(self):
|
||||||
|
dangerous, _, _ = detect_dangerous_command("killall -9 -r 'herm.*'")
|
||||||
|
assert dangerous is True
|
||||||
|
|
||||||
|
def test_killall_list_signals_is_safe(self):
|
||||||
|
"""`killall -l` lists signals and is harmless — must not fire."""
|
||||||
|
dangerous, _, _ = detect_dangerous_command("killall -l")
|
||||||
|
assert dangerous is False
|
||||||
|
|
||||||
|
def test_killall_version_is_safe(self):
|
||||||
|
dangerous, _, _ = detect_dangerous_command("killall -V")
|
||||||
|
assert dangerous is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestFindExecdir:
|
||||||
|
"""Inspired by Claude Code 2.1.113 tightening of find rules.
|
||||||
|
|
||||||
|
`find -execdir rm` has the same destructive effect as `find -exec rm`
|
||||||
|
but ran in each match's directory. Previously missed because the
|
||||||
|
pattern required a literal `-exec ` followed by a space.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_find_execdir_rm(self):
|
||||||
|
dangerous, _, desc = detect_dangerous_command(
|
||||||
|
"find . -execdir rm {} \\;"
|
||||||
|
)
|
||||||
|
assert dangerous is True
|
||||||
|
assert "find" in desc.lower() or "rm" in desc.lower()
|
||||||
|
|
||||||
|
def test_find_execdir_with_absolute_rm(self):
|
||||||
|
dangerous, _, _ = detect_dangerous_command(
|
||||||
|
"find /var -execdir /bin/rm -rf {} \\;"
|
||||||
|
)
|
||||||
|
assert dangerous is True
|
||||||
|
|
||||||
|
def test_find_exec_rm_still_caught(self):
|
||||||
|
"""Original -exec pattern must still fire (regression guard)."""
|
||||||
|
dangerous, _, _ = detect_dangerous_command(
|
||||||
|
"find . -exec rm {} \\;"
|
||||||
|
)
|
||||||
|
assert dangerous is True
|
||||||
|
|
||||||
|
def test_find_execdir_ls_is_safe(self):
|
||||||
|
"""-execdir with a read-only command is not dangerous."""
|
||||||
|
dangerous, _, _ = detect_dangerous_command(
|
||||||
|
"find . -execdir ls {} \\;"
|
||||||
|
)
|
||||||
|
assert dangerous is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestEtcPatternsUnaffectedByRefactor:
|
||||||
|
"""Regression guard: the /etc/ patterns were refactored to share the
|
||||||
|
_SYSTEM_CONFIG_PATH fragment with the /private/ mirror. Make sure the
|
||||||
|
existing /etc/ coverage remains identical.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_etc_redirect(self):
|
||||||
|
dangerous, _, _ = detect_dangerous_command("echo x > /etc/hosts")
|
||||||
|
assert dangerous is True
|
||||||
|
|
||||||
|
def test_etc_cp(self):
|
||||||
|
dangerous, _, _ = detect_dangerous_command("cp evil /etc/hosts")
|
||||||
|
assert dangerous is True
|
||||||
|
|
||||||
|
def test_etc_sed_inline(self):
|
||||||
|
dangerous, _, _ = detect_dangerous_command(
|
||||||
|
"sed -i 's/a/b/' /etc/hosts"
|
||||||
|
)
|
||||||
|
assert dangerous is True
|
||||||
|
|
||||||
|
def test_etc_tee(self):
|
||||||
|
dangerous, _, _ = detect_dangerous_command(
|
||||||
|
"echo x | tee /etc/hosts"
|
||||||
|
)
|
||||||
|
assert dangerous is True
|
||||||
|
|
||||||
|
def test_cat_etc_hostname_is_safe(self):
|
||||||
|
"""Reading /etc/ files is safe — only writes require approval."""
|
||||||
|
dangerous, _, _ = detect_dangerous_command("cat /etc/hostname")
|
||||||
|
assert dangerous is False
|
||||||
|
|
||||||
|
def test_grep_etc_passwd_is_safe(self):
|
||||||
|
dangerous, _, _ = detect_dangerous_command("grep root /etc/passwd")
|
||||||
|
assert dangerous is False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -63,8 +63,19 @@ _HERMES_ENV_PATH = (
|
|||||||
r'(?:\$hermes_home|\$\{hermes_home\})/)'
|
r'(?:\$hermes_home|\$\{hermes_home\})/)'
|
||||||
r'\.env\b'
|
r'\.env\b'
|
||||||
)
|
)
|
||||||
|
# macOS: /etc, /var, /tmp, /home are symlinks to /private/{etc,var,tmp,home}.
|
||||||
|
# A command written to target /private/etc/sudoers works identically to
|
||||||
|
# /etc/sudoers on macOS but bypasses a plain "/etc/" pattern check. Match
|
||||||
|
# both forms. Inspired by Claude Code 2.1.113's "dangerous path protection".
|
||||||
|
_MACOS_PRIVATE_SYSTEM_PATH = r'/private/(?:etc|var|tmp|home)/'
|
||||||
|
# System-config paths that should trigger approval for any write/edit,
|
||||||
|
# collapsing /etc, its macOS /private/etc mirror, and /etc/sudoers.d/ into
|
||||||
|
# one shared fragment so new DANGEROUS_PATTERNS stay consistent.
|
||||||
|
_SYSTEM_CONFIG_PATH = (
|
||||||
|
rf'(?:/etc/|{_MACOS_PRIVATE_SYSTEM_PATH})'
|
||||||
|
)
|
||||||
_SENSITIVE_WRITE_TARGET = (
|
_SENSITIVE_WRITE_TARGET = (
|
||||||
r'(?:/etc/|/dev/sd|'
|
rf'(?:{_SYSTEM_CONFIG_PATH}|/dev/sd|'
|
||||||
rf'{_SSH_SENSITIVE_PATH}|'
|
rf'{_SSH_SENSITIVE_PATH}|'
|
||||||
rf'{_HERMES_ENV_PATH})'
|
rf'{_HERMES_ENV_PATH})'
|
||||||
)
|
)
|
||||||
@@ -87,10 +98,17 @@ DANGEROUS_PATTERNS = [
|
|||||||
(r'\bDROP\s+(TABLE|DATABASE)\b', "SQL DROP"),
|
(r'\bDROP\s+(TABLE|DATABASE)\b', "SQL DROP"),
|
||||||
(r'\bDELETE\s+FROM\b(?!.*\bWHERE\b)', "SQL DELETE without WHERE"),
|
(r'\bDELETE\s+FROM\b(?!.*\bWHERE\b)', "SQL DELETE without WHERE"),
|
||||||
(r'\bTRUNCATE\s+(TABLE)?\s*\w', "SQL TRUNCATE"),
|
(r'\bTRUNCATE\s+(TABLE)?\s*\w', "SQL TRUNCATE"),
|
||||||
(r'>\s*/etc/', "overwrite system config"),
|
(rf'>\s*{_SYSTEM_CONFIG_PATH}', "overwrite system config"),
|
||||||
(r'\bsystemctl\s+(-[^\s]+\s+)*(stop|restart|disable|mask)\b', "stop/restart system service"),
|
(r'\bsystemctl\s+(-[^\s]+\s+)*(stop|restart|disable|mask)\b', "stop/restart system service"),
|
||||||
(r'\bkill\s+-9\s+-1\b', "kill all processes"),
|
(r'\bkill\s+-9\s+-1\b', "kill all processes"),
|
||||||
(r'\bpkill\s+-9\b', "force kill processes"),
|
(r'\bpkill\s+-9\b', "force kill processes"),
|
||||||
|
# killall with SIGKILL (parallel to pkill -9). Catches -9 / -KILL /
|
||||||
|
# -s KILL / -SIGKILL forms, and also `killall -r <regex>` broad sweeps
|
||||||
|
# that can wipe out unrelated processes by accident.
|
||||||
|
# Inspired by Claude Code 2.1.113 expanded deny rules.
|
||||||
|
(r'\bkillall\s+(-[^\s]*\s+)*-(9|KILL|SIGKILL)\b', "force kill processes (killall -KILL)"),
|
||||||
|
(r'\bkillall\s+(-[^\s]*\s+)*-s\s+(KILL|SIGKILL|9)\b', "force kill processes (killall -s KILL)"),
|
||||||
|
(r'\bkillall\s+(-[^\s]*\s+)*-r\b', "kill processes by regex (killall -r)"),
|
||||||
(r':\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:', "fork bomb"),
|
(r':\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:', "fork bomb"),
|
||||||
# Any shell invocation via -c or combined flags like -lc, -ic, etc.
|
# Any shell invocation via -c or combined flags like -lc, -ic, etc.
|
||||||
(r'\b(bash|sh|zsh|ksh)\s+-[^\s]*c(\s+|$)', "shell command via -c/-lc flag"),
|
(r'\b(bash|sh|zsh|ksh)\s+-[^\s]*c(\s+|$)', "shell command via -c/-lc flag"),
|
||||||
@@ -100,7 +118,11 @@ DANGEROUS_PATTERNS = [
|
|||||||
(rf'\btee\b.*["\']?{_SENSITIVE_WRITE_TARGET}', "overwrite system file via tee"),
|
(rf'\btee\b.*["\']?{_SENSITIVE_WRITE_TARGET}', "overwrite system file via tee"),
|
||||||
(rf'>>?\s*["\']?{_SENSITIVE_WRITE_TARGET}', "overwrite system file via redirection"),
|
(rf'>>?\s*["\']?{_SENSITIVE_WRITE_TARGET}', "overwrite system file via redirection"),
|
||||||
(r'\bxargs\s+.*\brm\b', "xargs with rm"),
|
(r'\bxargs\s+.*\brm\b', "xargs with rm"),
|
||||||
(r'\bfind\b.*-exec\s+(/\S*/)?rm\b', "find -exec rm"),
|
# find -exec rm / -execdir rm — the -execdir variant (same semantics,
|
||||||
|
# runs in the directory of each match) was previously missed. Claude
|
||||||
|
# Code 2.1.113 tightened their equivalent find rule to stop auto-
|
||||||
|
# approving -exec / -delete flags.
|
||||||
|
(r'\bfind\b.*-exec(?:dir)?\s+(/\S*/)?rm\b', "find -exec/-execdir rm"),
|
||||||
(r'\bfind\b.*-delete\b', "find -delete"),
|
(r'\bfind\b.*-delete\b', "find -delete"),
|
||||||
# Gateway lifecycle protection: prevent the agent from killing its own
|
# Gateway lifecycle protection: prevent the agent from killing its own
|
||||||
# gateway process. These commands trigger a gateway restart/stop that
|
# gateway process. These commands trigger a gateway restart/stop that
|
||||||
@@ -118,10 +140,11 @@ DANGEROUS_PATTERNS = [
|
|||||||
# to regex at detection time. Catch the structural pattern instead.
|
# to regex at detection time. Catch the structural pattern instead.
|
||||||
(r'\bkill\b.*\$\(\s*pgrep\b', "kill process via pgrep expansion (self-termination)"),
|
(r'\bkill\b.*\$\(\s*pgrep\b', "kill process via pgrep expansion (self-termination)"),
|
||||||
(r'\bkill\b.*`\s*pgrep\b', "kill process via backtick pgrep expansion (self-termination)"),
|
(r'\bkill\b.*`\s*pgrep\b', "kill process via backtick pgrep expansion (self-termination)"),
|
||||||
# File copy/move/edit into sensitive system paths
|
# File copy/move/edit into sensitive system paths (/etc/ and macOS
|
||||||
(r'\b(cp|mv|install)\b.*\s/etc/', "copy/move file into /etc/"),
|
# /private/etc/ mirror).
|
||||||
(r'\bsed\s+-[^\s]*i.*\s/etc/', "in-place edit of system config"),
|
(rf'\b(cp|mv|install)\b.*\s{_SYSTEM_CONFIG_PATH}', "copy/move file into system config path"),
|
||||||
(r'\bsed\s+--in-place\b.*\s/etc/', "in-place edit of system config (long flag)"),
|
(rf'\bsed\s+-[^\s]*i.*\s{_SYSTEM_CONFIG_PATH}', "in-place edit of system config"),
|
||||||
|
(rf'\bsed\s+--in-place\b.*\s{_SYSTEM_CONFIG_PATH}', "in-place edit of system config (long flag)"),
|
||||||
# Script execution via heredoc — bypasses the -e/-c flag patterns above.
|
# Script execution via heredoc — bypasses the -e/-c flag patterns above.
|
||||||
# `python3 << 'EOF'` feeds arbitrary code via stdin without -c/-e flags.
|
# `python3 << 'EOF'` feeds arbitrary code via stdin without -c/-e flags.
|
||||||
(r'\b(python[23]?|perl|ruby|node)\s+<<', "script execution via heredoc"),
|
(r'\b(python[23]?|perl|ruby|node)\s+<<', "script execution via heredoc"),
|
||||||
|
|||||||
Reference in New Issue
Block a user