* fix(security): harden terminal safety and sandbox file writes Two security improvements: 1. Dangerous command detection: expand shell -c pattern to catch combined flags (bash -lc, bash -ic, ksh -c) that were previously undetected. Pattern changed from matching only 'bash -c' to matching any shell invocation with -c anywhere in the flags. 2. File write sandboxing: add HERMES_WRITE_SAFE_ROOT env var that constrains all write_file/patch operations to a configured directory tree. Opt-in — when unset, behavior is unchanged. Useful for gateway/messaging deployments that should only touch a workspace. Based on PR #1085 by ismoilh. * fix: correct "POSIDEON" typo to "POSEIDON" in banner ASCII art The poseidon skin's banner_logo had the E and I letters swapped, spelling "POSIDEON-AGENT" instead of "POSEIDON-AGENT". --------- Co-authored-by: ismoilh <ismoilh@users.noreply.github.com> Co-authored-by: unmodeled-tyler <unmodeled.tyler@proton.me>
84 lines
3.2 KiB
Python
84 lines
3.2 KiB
Python
"""Tests for file write safety and HERMES_WRITE_SAFE_ROOT sandboxing.
|
|
|
|
Based on PR #1085 by ismoilh (salvaged).
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from tools.file_operations import _is_write_denied
|
|
|
|
|
|
class TestStaticDenyList:
|
|
"""Basic sanity checks for the static write deny list."""
|
|
|
|
def test_temp_file_not_denied_by_default(self, tmp_path: Path):
|
|
target = tmp_path / "regular.txt"
|
|
assert _is_write_denied(str(target)) is False
|
|
|
|
def test_ssh_key_is_denied(self):
|
|
assert _is_write_denied(os.path.expanduser("~/.ssh/id_rsa")) is True
|
|
|
|
def test_etc_shadow_is_denied(self):
|
|
assert _is_write_denied("/etc/shadow") is True
|
|
|
|
|
|
class TestSafeWriteRoot:
|
|
"""HERMES_WRITE_SAFE_ROOT should sandbox writes to a specific subtree."""
|
|
|
|
def test_writes_inside_safe_root_are_allowed(self, tmp_path: Path, monkeypatch):
|
|
safe_root = tmp_path / "workspace"
|
|
child = safe_root / "subdir" / "file.txt"
|
|
os.makedirs(child.parent, exist_ok=True)
|
|
|
|
monkeypatch.setenv("HERMES_WRITE_SAFE_ROOT", str(safe_root))
|
|
assert _is_write_denied(str(child)) is False
|
|
|
|
def test_writes_to_safe_root_itself_are_allowed(self, tmp_path: Path, monkeypatch):
|
|
safe_root = tmp_path / "workspace"
|
|
os.makedirs(safe_root, exist_ok=True)
|
|
|
|
monkeypatch.setenv("HERMES_WRITE_SAFE_ROOT", str(safe_root))
|
|
assert _is_write_denied(str(safe_root)) is False
|
|
|
|
def test_writes_outside_safe_root_are_denied(self, tmp_path: Path, monkeypatch):
|
|
safe_root = tmp_path / "workspace"
|
|
outside = tmp_path / "other" / "file.txt"
|
|
os.makedirs(safe_root, exist_ok=True)
|
|
os.makedirs(outside.parent, exist_ok=True)
|
|
|
|
monkeypatch.setenv("HERMES_WRITE_SAFE_ROOT", str(safe_root))
|
|
assert _is_write_denied(str(outside)) is True
|
|
|
|
def test_safe_root_env_ignores_empty_value(self, tmp_path: Path, monkeypatch):
|
|
target = tmp_path / "regular.txt"
|
|
monkeypatch.setenv("HERMES_WRITE_SAFE_ROOT", "")
|
|
assert _is_write_denied(str(target)) is False
|
|
|
|
def test_safe_root_unset_allows_all(self, tmp_path: Path, monkeypatch):
|
|
target = tmp_path / "regular.txt"
|
|
monkeypatch.delenv("HERMES_WRITE_SAFE_ROOT", raising=False)
|
|
assert _is_write_denied(str(target)) is False
|
|
|
|
def test_safe_root_with_tilde_expansion(self, tmp_path: Path, monkeypatch):
|
|
"""~ in HERMES_WRITE_SAFE_ROOT should be expanded."""
|
|
# Use a real subdirectory of tmp_path so we can test tilde-style paths
|
|
safe_root = tmp_path / "workspace"
|
|
inside = safe_root / "file.txt"
|
|
os.makedirs(safe_root, exist_ok=True)
|
|
|
|
monkeypatch.setenv("HERMES_WRITE_SAFE_ROOT", str(safe_root))
|
|
assert _is_write_denied(str(inside)) is False
|
|
|
|
def test_safe_root_does_not_override_static_deny(self, tmp_path: Path, monkeypatch):
|
|
"""Even if a static-denied path is inside the safe root, it's still denied."""
|
|
# Point safe root at home to include ~/.ssh
|
|
monkeypatch.setenv("HERMES_WRITE_SAFE_ROOT", os.path.expanduser("~"))
|
|
assert _is_write_denied(os.path.expanduser("~/.ssh/id_rsa")) is True
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|