fix: create the audit log 0600 in a 0700 directory and refuse symlinks (SEC-11)
This commit is contained in:
1 parent
fd5e4b71f3
commit
7307214ab6
4 files changed
+138
-3
No files matched your search
@@ -0,0 +1,101 @@
|
||||
"""The audit file is private and is never reached through a symlink (SEC-11)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
import os
|
||||
import stat
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pytest_httpx import HTTPXMock
|
||||
|
||||
from aegis_gitea_mcp import transports
|
||||
from aegis_gitea_mcp.audit import AuditLogger, get_audit_logger, validate_audit_log_integrity
|
||||
from aegis_gitea_mcp.transports import run_http
|
||||
from tests.test_server_helpers import API, connected, mock_user, server_settings
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _env(mock_env: None, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""The audit logger reads the process-wide settings; a startup that passes must not serve."""
|
||||
|
||||
def refuse(*args: object, **kwargs: object) -> None:
|
||||
raise AssertionError("uvicorn.run must not be reached")
|
||||
|
||||
monkeypatch.setattr(transports.uvicorn, "run", refuse)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def open_umask() -> object:
|
||||
"""A permissive umask: what an unprepared host or container would hand out."""
|
||||
previous = os.umask(0)
|
||||
yield None
|
||||
os.umask(previous)
|
||||
|
||||
|
||||
def mode_of(path: Path) -> int:
|
||||
return stat.S_IMODE(path.stat().st_mode)
|
||||
|
||||
|
||||
async def test_audit_log_is_0600_and_a_new_state_directory_is_0700(
|
||||
httpx_mock: HTTPXMock, open_umask: object
|
||||
) -> None:
|
||||
mock_user(httpx_mock)
|
||||
httpx_mock.add_response(url=f"{API}/repos/octo/demo", json={})
|
||||
async with connected() as client:
|
||||
await client.call_tool("gitea_repo_get", {"owner": "octo", "repo": "demo"})
|
||||
|
||||
path = get_audit_logger().log_path
|
||||
assert mode_of(path) == 0o600
|
||||
assert mode_of(path.parent) == 0o700
|
||||
assert validate_audit_log_integrity(path) == (True, [])
|
||||
|
||||
|
||||
def test_an_older_world_readable_log_is_tightened(tmp_path: Path) -> None:
|
||||
path = tmp_path / "audit.log"
|
||||
path.write_text("")
|
||||
path.chmod(0o644)
|
||||
|
||||
AuditLogger(path).close()
|
||||
|
||||
assert mode_of(path) == 0o600
|
||||
|
||||
|
||||
def test_a_symlink_at_the_audit_path_is_not_followed(tmp_path: Path) -> None:
|
||||
target = tmp_path / "elsewhere.log"
|
||||
target.write_text("")
|
||||
link = tmp_path / "audit.log"
|
||||
link.symlink_to(target)
|
||||
|
||||
with pytest.raises(OSError) as caught:
|
||||
AuditLogger(link)
|
||||
assert caught.value.errno == errno.ELOOP
|
||||
assert target.read_text() == ""
|
||||
|
||||
|
||||
def test_startup_exits_2_for_a_symlinked_audit_path(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
target = tmp_path / "elsewhere.log"
|
||||
target.write_text("")
|
||||
link = tmp_path / "state" / "audit.log"
|
||||
link.parent.mkdir()
|
||||
link.symlink_to(target)
|
||||
monkeypatch.setenv("AUDIT_LOG_PATH", str(link))
|
||||
|
||||
assert run_http(server_settings(audit_log_path=link)) == 2
|
||||
|
||||
assert "symbolic link" in capsys.readouterr().err
|
||||
assert target.read_text() == ""
|
||||
|
||||
|
||||
def test_startup_exits_2_for_an_audit_path_that_is_a_directory(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
directory = tmp_path / "audit.log"
|
||||
directory.mkdir()
|
||||
monkeypatch.setenv("AUDIT_LOG_PATH", str(directory))
|
||||
|
||||
assert run_http(server_settings(audit_log_path=directory)) == 2
|
||||
assert "audit log" in capsys.readouterr().err
|
||||
Reference in new issue
Block a user