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()
|