Enforce owner-only permissions on files and directories that contain secrets or sensitive data: - cron/jobs.py: jobs.json (0600), cron dirs (0700), job output files (0600) - hermes_cli/config.py: config.yaml (0600), .env (0600), ~/.hermes/* dirs (0700) - cli.py: config.yaml via save_config_value (0600) All chmod calls use try/except for Windows compatibility. Includes _secure_file() and _secure_dir() helpers with graceful fallback. 8 new tests verify permissions on all file types. Inspired by openclaw v2026.3.7 file permission enforcement.
136 lines
4.9 KiB
Python
136 lines
4.9 KiB
Python
"""Tests for file permissions hardening on sensitive files."""
|
|
|
|
import json
|
|
import os
|
|
import stat
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
|
|
class TestCronFilePermissions(unittest.TestCase):
|
|
"""Verify cron files get secure permissions."""
|
|
|
|
def setUp(self):
|
|
self.tmpdir = tempfile.mkdtemp()
|
|
self.cron_dir = Path(self.tmpdir) / "cron"
|
|
self.output_dir = self.cron_dir / "output"
|
|
|
|
def tearDown(self):
|
|
import shutil
|
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
|
|
|
@patch("cron.jobs.CRON_DIR")
|
|
@patch("cron.jobs.OUTPUT_DIR")
|
|
@patch("cron.jobs.JOBS_FILE")
|
|
def test_ensure_dirs_sets_0700(self, mock_jobs_file, mock_output, mock_cron):
|
|
mock_cron.__class__ = Path
|
|
# Use real paths
|
|
cron_dir = Path(self.tmpdir) / "cron"
|
|
output_dir = cron_dir / "output"
|
|
|
|
with patch("cron.jobs.CRON_DIR", cron_dir), \
|
|
patch("cron.jobs.OUTPUT_DIR", output_dir):
|
|
from cron.jobs import ensure_dirs
|
|
ensure_dirs()
|
|
|
|
cron_mode = stat.S_IMODE(os.stat(cron_dir).st_mode)
|
|
output_mode = stat.S_IMODE(os.stat(output_dir).st_mode)
|
|
self.assertEqual(cron_mode, 0o700)
|
|
self.assertEqual(output_mode, 0o700)
|
|
|
|
@patch("cron.jobs.CRON_DIR")
|
|
@patch("cron.jobs.OUTPUT_DIR")
|
|
@patch("cron.jobs.JOBS_FILE")
|
|
def test_save_jobs_sets_0600(self, mock_jobs_file, mock_output, mock_cron):
|
|
cron_dir = Path(self.tmpdir) / "cron"
|
|
output_dir = cron_dir / "output"
|
|
jobs_file = cron_dir / "jobs.json"
|
|
|
|
with patch("cron.jobs.CRON_DIR", cron_dir), \
|
|
patch("cron.jobs.OUTPUT_DIR", output_dir), \
|
|
patch("cron.jobs.JOBS_FILE", jobs_file):
|
|
from cron.jobs import save_jobs
|
|
save_jobs([{"id": "test", "prompt": "hello"}])
|
|
|
|
file_mode = stat.S_IMODE(os.stat(jobs_file).st_mode)
|
|
self.assertEqual(file_mode, 0o600)
|
|
|
|
def test_save_job_output_sets_0600(self):
|
|
output_dir = Path(self.tmpdir) / "output"
|
|
with patch("cron.jobs.OUTPUT_DIR", output_dir), \
|
|
patch("cron.jobs.CRON_DIR", Path(self.tmpdir)), \
|
|
patch("cron.jobs.ensure_dirs"):
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
from cron.jobs import save_job_output
|
|
output_file = save_job_output("test-job", "test output content")
|
|
|
|
file_mode = stat.S_IMODE(os.stat(output_file).st_mode)
|
|
self.assertEqual(file_mode, 0o600)
|
|
|
|
# Job output dir should also be 0700
|
|
job_dir = output_dir / "test-job"
|
|
dir_mode = stat.S_IMODE(os.stat(job_dir).st_mode)
|
|
self.assertEqual(dir_mode, 0o700)
|
|
|
|
|
|
class TestConfigFilePermissions(unittest.TestCase):
|
|
"""Verify config files get secure permissions."""
|
|
|
|
def setUp(self):
|
|
self.tmpdir = tempfile.mkdtemp()
|
|
|
|
def tearDown(self):
|
|
import shutil
|
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
|
|
|
def test_save_config_sets_0600(self):
|
|
config_path = Path(self.tmpdir) / "config.yaml"
|
|
with patch("hermes_cli.config.get_config_path", return_value=config_path), \
|
|
patch("hermes_cli.config.ensure_hermes_home"):
|
|
from hermes_cli.config import save_config
|
|
save_config({"model": "test/model"})
|
|
|
|
file_mode = stat.S_IMODE(os.stat(config_path).st_mode)
|
|
self.assertEqual(file_mode, 0o600)
|
|
|
|
def test_save_env_value_sets_0600(self):
|
|
env_path = Path(self.tmpdir) / ".env"
|
|
with patch("hermes_cli.config.get_env_path", return_value=env_path), \
|
|
patch("hermes_cli.config.ensure_hermes_home"):
|
|
from hermes_cli.config import save_env_value
|
|
save_env_value("TEST_KEY", "test_value")
|
|
|
|
file_mode = stat.S_IMODE(os.stat(env_path).st_mode)
|
|
self.assertEqual(file_mode, 0o600)
|
|
|
|
def test_ensure_hermes_home_sets_0700(self):
|
|
home = Path(self.tmpdir) / ".hermes"
|
|
with patch("hermes_cli.config.get_hermes_home", return_value=home):
|
|
from hermes_cli.config import ensure_hermes_home
|
|
ensure_hermes_home()
|
|
|
|
home_mode = stat.S_IMODE(os.stat(home).st_mode)
|
|
self.assertEqual(home_mode, 0o700)
|
|
|
|
for subdir in ("cron", "sessions", "logs", "memories"):
|
|
subdir_mode = stat.S_IMODE(os.stat(home / subdir).st_mode)
|
|
self.assertEqual(subdir_mode, 0o700, f"{subdir} should be 0700")
|
|
|
|
|
|
class TestSecureHelpers(unittest.TestCase):
|
|
"""Test the _secure_file and _secure_dir helpers."""
|
|
|
|
def test_secure_file_nonexistent_no_error(self):
|
|
from cron.jobs import _secure_file
|
|
_secure_file(Path("/nonexistent/path/file.json")) # Should not raise
|
|
|
|
def test_secure_dir_nonexistent_no_error(self):
|
|
from cron.jobs import _secure_dir
|
|
_secure_dir(Path("/nonexistent/path")) # Should not raise
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|