Initial monorepo structure
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"""Tests für den AuditLogger."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from monorepo.audit import AuditLogger, _DEFAULT_AUDIT_LOG_PATH
|
||||
from monorepo.models import SecurityEvent
|
||||
|
||||
|
||||
class TestAuditLoggerInit:
|
||||
"""Tests für die Initialisierung des AuditLoggers."""
|
||||
|
||||
def test_default_log_path(self, tmp_path: Path) -> None:
|
||||
"""AuditLogger mit Standard-Pfad nutzt _DEFAULT_AUDIT_LOG_PATH."""
|
||||
# Wir übergeben explizit einen tmp-Pfad, da der Default relativ ist
|
||||
log_path = tmp_path / ".audit" / "access.log"
|
||||
logger = AuditLogger(log_path=log_path)
|
||||
assert logger.log_path == log_path
|
||||
|
||||
def test_creates_log_directory(self, tmp_path: Path) -> None:
|
||||
"""AuditLogger erstellt das Verzeichnis, falls es nicht existiert."""
|
||||
log_path = tmp_path / "nested" / "deep" / "audit.log"
|
||||
assert not log_path.parent.exists()
|
||||
AuditLogger(log_path=log_path)
|
||||
assert log_path.parent.exists()
|
||||
|
||||
def test_existing_directory_no_error(self, tmp_path: Path) -> None:
|
||||
"""AuditLogger wirft keinen Fehler bei bereits existierendem Verzeichnis."""
|
||||
log_dir = tmp_path / ".audit"
|
||||
log_dir.mkdir()
|
||||
log_path = log_dir / "access.log"
|
||||
logger = AuditLogger(log_path=log_path)
|
||||
assert logger.log_path == log_path
|
||||
|
||||
|
||||
class TestLogViolation:
|
||||
"""Tests für die log_violation-Methode."""
|
||||
|
||||
def _make_event(
|
||||
self,
|
||||
requesting: str = "dhive",
|
||||
target: str = "privat",
|
||||
resource: str = "privat/.env",
|
||||
action: str = "read",
|
||||
outcome: str = "denied",
|
||||
ts: datetime | None = None,
|
||||
) -> SecurityEvent:
|
||||
"""Erstellt ein Test-SecurityEvent."""
|
||||
return SecurityEvent(
|
||||
timestamp=ts or datetime(2025, 1, 15, 10, 30, 0),
|
||||
requesting_context=requesting,
|
||||
target_context=target,
|
||||
resource=resource,
|
||||
action=action,
|
||||
outcome=outcome,
|
||||
)
|
||||
|
||||
def test_writes_single_entry(self, tmp_path: Path) -> None:
|
||||
"""log_violation schreibt einen Eintrag in die Logdatei."""
|
||||
log_path = tmp_path / ".audit" / "access.log"
|
||||
logger = AuditLogger(log_path=log_path)
|
||||
|
||||
event = self._make_event()
|
||||
logger.log_violation(event)
|
||||
|
||||
content = log_path.read_text(encoding="utf-8")
|
||||
assert "[2025-01-15T10:30:00] DENIED | dhive -> privat | read on privat/.env\n" == content
|
||||
|
||||
def test_appends_multiple_entries(self, tmp_path: Path) -> None:
|
||||
"""log_violation fügt Einträge an, ohne bestehende zu überschreiben."""
|
||||
log_path = tmp_path / ".audit" / "access.log"
|
||||
logger = AuditLogger(log_path=log_path)
|
||||
|
||||
event1 = self._make_event(
|
||||
requesting="bahn", target="privat", resource="privat/.env",
|
||||
action="read", outcome="denied",
|
||||
ts=datetime(2025, 1, 15, 10, 0, 0),
|
||||
)
|
||||
event2 = self._make_event(
|
||||
requesting="dhive", target="bahn", resource="bahn/.env",
|
||||
action="write", outcome="denied",
|
||||
ts=datetime(2025, 1, 15, 11, 0, 0),
|
||||
)
|
||||
|
||||
logger.log_violation(event1)
|
||||
logger.log_violation(event2)
|
||||
|
||||
lines = log_path.read_text(encoding="utf-8").splitlines()
|
||||
assert len(lines) == 2
|
||||
assert "bahn -> privat" in lines[0]
|
||||
assert "dhive -> bahn" in lines[1]
|
||||
|
||||
def test_format_contains_iso_timestamp(self, tmp_path: Path) -> None:
|
||||
"""Der Zeitstempel ist im ISO-8601-Format."""
|
||||
log_path = tmp_path / ".audit" / "access.log"
|
||||
logger = AuditLogger(log_path=log_path)
|
||||
|
||||
ts = datetime(2025, 6, 20, 14, 45, 30)
|
||||
event = self._make_event(ts=ts)
|
||||
logger.log_violation(event)
|
||||
|
||||
content = log_path.read_text(encoding="utf-8")
|
||||
assert "[2025-06-20T14:45:30]" in content
|
||||
|
||||
def test_format_outcome_uppercase(self, tmp_path: Path) -> None:
|
||||
"""Das Outcome wird in Großbuchstaben geschrieben."""
|
||||
log_path = tmp_path / ".audit" / "access.log"
|
||||
logger = AuditLogger(log_path=log_path)
|
||||
|
||||
event = self._make_event(outcome="allowed")
|
||||
logger.log_violation(event)
|
||||
|
||||
content = log_path.read_text(encoding="utf-8")
|
||||
assert "ALLOWED" in content
|
||||
|
||||
def test_format_contains_action_and_resource(self, tmp_path: Path) -> None:
|
||||
"""Die Aktion und Ressource werden korrekt formatiert."""
|
||||
log_path = tmp_path / ".audit" / "access.log"
|
||||
logger = AuditLogger(log_path=log_path)
|
||||
|
||||
event = self._make_event(action="execute", resource="shared/tools/script.sh")
|
||||
logger.log_violation(event)
|
||||
|
||||
content = log_path.read_text(encoding="utf-8")
|
||||
assert "execute on shared/tools/script.sh" in content
|
||||
|
||||
def test_thread_safety(self, tmp_path: Path) -> None:
|
||||
"""Parallele Aufrufe erzeugen keine korrupten Einträge."""
|
||||
import threading
|
||||
|
||||
log_path = tmp_path / ".audit" / "access.log"
|
||||
logger = AuditLogger(log_path=log_path)
|
||||
|
||||
events = [
|
||||
self._make_event(
|
||||
requesting=f"ctx{i}",
|
||||
target="privat",
|
||||
resource="privat/.env",
|
||||
ts=datetime(2025, 1, 15, 10, i, 0),
|
||||
)
|
||||
for i in range(20)
|
||||
]
|
||||
|
||||
threads = [
|
||||
threading.Thread(target=logger.log_violation, args=(e,))
|
||||
for e in events
|
||||
]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
lines = log_path.read_text(encoding="utf-8").splitlines()
|
||||
assert len(lines) == 20
|
||||
# Jede Zeile muss vollständig sein (beginnt mit [ und endet mit resource)
|
||||
for line in lines:
|
||||
assert line.startswith("[")
|
||||
assert "privat/.env" in line
|
||||
|
||||
|
||||
class TestDefaultPath:
|
||||
"""Tests für den Standard-Audit-Log-Pfad."""
|
||||
|
||||
def test_default_path_value(self) -> None:
|
||||
"""Der Standard-Pfad entspricht der monorepo.yaml-Konfiguration."""
|
||||
assert _DEFAULT_AUDIT_LOG_PATH == Path(".audit/access.log")
|
||||
Reference in New Issue
Block a user