stage-11: security hardening

- Prompt injection protection: external messages wrapped in [EXTERNAL CONTENT]
  markers, system prompt instructs Claude to never follow external instructions
- Invocation logging: all Claude CLI calls logged with channel, model, duration,
  token counts to echo-core.invoke logger
- Security logging: separate echo-core.security logger for unauthorized access
  attempts (DMs from non-admins, unauthorized admin/owner commands)
- Security log routed to logs/security.log in addition to main log
- Extended echo doctor: Claude CLI functional check, config.json secret scan,
  .gitignore completeness, file permissions, Ollama reachability, bot process
- Subprocess env stripping logged at debug level

373 tests pass (10 new security tests).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MoltBot Service
2026-02-13 18:01:31 +00:00
parent 85c72e4b3d
commit d1bb67abc1
6 changed files with 326 additions and 12 deletions

View File

@@ -100,15 +100,40 @@ class TestDoctor:
def _run_doctor(self, iso, capsys, *, token="tok",
claude="/usr/bin/claude",
disk_bavail=1_000_000, disk_frsize=4096):
disk_bavail=1_000_000, disk_frsize=4096,
setup_full=False):
"""Run cmd_doctor with mocked externals, return (stdout, exit_code)."""
import os as _os
stat = MagicMock(f_bavail=disk_bavail, f_frsize=disk_frsize)
# Mock subprocess.run for claude --version
mock_proc = MagicMock(returncode=0, stdout="1.0.0", stderr="")
# Mock urllib for Ollama reachability
mock_resp = MagicMock(status=200)
patches = [
patch("cli.get_secret", return_value=token),
patch("keyring.get_password", return_value=None),
patch("shutil.which", return_value=claude),
patch("os.statvfs", return_value=stat),
patch("subprocess.run", return_value=mock_proc),
patch("urllib.request.urlopen", return_value=mock_resp),
]
if setup_full:
# Create .gitignore with required entries
gi_path = cli.PROJECT_ROOT / ".gitignore"
gi_path.write_text("sessions/\nlogs/\n.env\n*.sqlite\n")
# Create PID file with current PID
iso["pid_file"].write_text(str(_os.getpid()))
# Set config.json not world-readable
iso["config_file"].chmod(0o600)
# Create sessions dir not world-readable
sessions_dir = cli.PROJECT_ROOT / "sessions"
sessions_dir.mkdir(exist_ok=True)
sessions_dir.chmod(0o700)
with ExitStack() as stack:
for p in patches:
stack.enter_context(p)
@@ -120,7 +145,7 @@ class TestDoctor:
def test_all_pass(self, iso, capsys):
iso["config_file"].write_text('{"bot":{}}')
out, code = self._run_doctor(iso, capsys)
out, code = self._run_doctor(iso, capsys, setup_full=True)
assert "All checks passed" in out
assert "[FAIL]" not in out
assert code == 0
@@ -150,6 +175,29 @@ class TestDoctor:
assert "Disk space" in out
assert code == 1
def test_config_with_token_fails(self, iso, capsys):
iso["config_file"].write_text('{"discord_token": "sk-abcdefghijklmnopqrstuvwxyz"}')
out, code = self._run_doctor(iso, capsys)
assert "[FAIL] config.json no plain text secrets" in out
assert code == 1
def test_gitignore_check(self, iso, capsys):
iso["config_file"].write_text('{"bot":{}}')
# No .gitignore → FAIL
out, code = self._run_doctor(iso, capsys)
assert "[FAIL] .gitignore" in out
assert code == 1
def test_ollama_check(self, iso, capsys):
iso["config_file"].write_text('{"bot":{}}')
out, code = self._run_doctor(iso, capsys, setup_full=True)
assert "Ollama reachable" in out
def test_claude_functional_check(self, iso, capsys):
iso["config_file"].write_text('{"bot":{}}')
out, code = self._run_doctor(iso, capsys, setup_full=True)
assert "Claude CLI functional" in out
# ---------------------------------------------------------------------------
# cmd_restart