initial commit after one day coding agent session
This commit is contained in:
commit
372af75b90
88 changed files with 22694 additions and 0 deletions
369
tests/security/test_audit.py
Normal file
369
tests/security/test_audit.py
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
"""
|
||||
Tests for audit logger module.
|
||||
|
||||
Following TDD approach - these tests are written before implementation.
|
||||
Tests cover all logging requirements from todo.md section 1.2.3.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def test_log_entries_written_to_file(tmp_path):
|
||||
"""Test that log entries are written to the log file."""
|
||||
from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity
|
||||
|
||||
log_file = tmp_path / "audit.log"
|
||||
logger = AuditLogger(log_file)
|
||||
|
||||
logger.log(
|
||||
event_type=AuditEventType.CONTAINER_CREATE,
|
||||
severity=AuditSeverity.INFO,
|
||||
message="Test container created",
|
||||
details={"image": "test-image", "container_id": "abc123"}
|
||||
)
|
||||
|
||||
assert log_file.exists()
|
||||
content = log_file.read_text()
|
||||
assert len(content) > 0
|
||||
|
||||
|
||||
def test_log_entries_are_valid_json(tmp_path):
|
||||
"""Test that log entries are valid JSON."""
|
||||
from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity
|
||||
|
||||
log_file = tmp_path / "audit.log"
|
||||
logger = AuditLogger(log_file)
|
||||
|
||||
logger.log(
|
||||
event_type=AuditEventType.CONTAINER_CREATE,
|
||||
severity=AuditSeverity.INFO,
|
||||
message="Test entry",
|
||||
details={"key": "value"}
|
||||
)
|
||||
|
||||
# Each line should be valid JSON
|
||||
lines = log_file.read_text().strip().split("\n")
|
||||
for line in lines:
|
||||
data = json.loads(line) # Should not raise
|
||||
assert isinstance(data, dict)
|
||||
|
||||
|
||||
def test_log_entries_contain_required_fields(tmp_path):
|
||||
"""Test that log entries contain all required fields."""
|
||||
from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity
|
||||
|
||||
log_file = tmp_path / "audit.log"
|
||||
logger = AuditLogger(log_file)
|
||||
|
||||
logger.log(
|
||||
event_type=AuditEventType.EXECUTION_REQUEST,
|
||||
severity=AuditSeverity.INFO,
|
||||
message="Code execution requested",
|
||||
session_id="test-session-123"
|
||||
)
|
||||
|
||||
lines = log_file.read_text().strip().split("\n")
|
||||
entry = json.loads(lines[0])
|
||||
|
||||
# Required fields
|
||||
assert "timestamp" in entry
|
||||
assert "event_type" in entry
|
||||
assert "severity" in entry
|
||||
assert "message" in entry
|
||||
assert "session_id" in entry
|
||||
|
||||
|
||||
def test_timestamp_format_is_iso_8601(tmp_path):
|
||||
"""Test that timestamp is in ISO 8601 format."""
|
||||
from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity
|
||||
|
||||
log_file = tmp_path / "audit.log"
|
||||
logger = AuditLogger(log_file)
|
||||
|
||||
logger.log(
|
||||
event_type=AuditEventType.SESSION_CREATE,
|
||||
severity=AuditSeverity.INFO,
|
||||
message="Session created"
|
||||
)
|
||||
|
||||
lines = log_file.read_text().strip().split("\n")
|
||||
entry = json.loads(lines[0])
|
||||
|
||||
# Should be parseable as ISO 8601
|
||||
timestamp = entry["timestamp"]
|
||||
dt = datetime.fromisoformat(timestamp)
|
||||
assert isinstance(dt, datetime)
|
||||
|
||||
|
||||
def test_concurrent_logging_is_thread_safe(tmp_path):
|
||||
"""Test that concurrent logging from multiple threads is thread-safe."""
|
||||
from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity
|
||||
|
||||
log_file = tmp_path / "audit.log"
|
||||
logger = AuditLogger(log_file)
|
||||
|
||||
def log_entries(thread_id, count):
|
||||
for i in range(count):
|
||||
logger.log(
|
||||
event_type=AuditEventType.CONTAINER_CREATE,
|
||||
severity=AuditSeverity.INFO,
|
||||
message=f"Thread {thread_id} entry {i}"
|
||||
)
|
||||
|
||||
# Create multiple threads
|
||||
threads = []
|
||||
entries_per_thread = 10
|
||||
num_threads = 5
|
||||
|
||||
for i in range(num_threads):
|
||||
t = threading.Thread(target=log_entries, args=(i, entries_per_thread))
|
||||
threads.append(t)
|
||||
t.start()
|
||||
|
||||
# Wait for all threads
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
# Verify all entries written
|
||||
lines = log_file.read_text().strip().split("\n")
|
||||
assert len(lines) == num_threads * entries_per_thread
|
||||
|
||||
# Verify all entries are valid JSON
|
||||
for line in lines:
|
||||
json.loads(line)
|
||||
|
||||
|
||||
def test_security_violations_logged_with_correct_severity(tmp_path):
|
||||
"""Test that security violations are logged at correct severity."""
|
||||
from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity
|
||||
|
||||
log_file = tmp_path / "audit.log"
|
||||
logger = AuditLogger(log_file)
|
||||
|
||||
logger.log_security_violation(
|
||||
operation="container_create",
|
||||
reason="Privileged mode attempted",
|
||||
session_id="test-session"
|
||||
)
|
||||
|
||||
lines = log_file.read_text().strip().split("\n")
|
||||
entry = json.loads(lines[0])
|
||||
|
||||
assert entry["severity"] == "critical"
|
||||
assert entry["event_type"] == "security.violation"
|
||||
|
||||
|
||||
def test_pii_is_not_logged(tmp_path):
|
||||
"""Test that PII (code content, tokens, files) is not logged."""
|
||||
from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity
|
||||
|
||||
log_file = tmp_path / "audit.log"
|
||||
logger = AuditLogger(log_file)
|
||||
|
||||
# Log execution request - should NOT include actual code
|
||||
logger.log(
|
||||
event_type=AuditEventType.EXECUTION_REQUEST,
|
||||
severity=AuditSeverity.INFO,
|
||||
message="Code execution requested",
|
||||
details={
|
||||
"code_hash": "abc123def456", # Hash is OK
|
||||
# "code": "print('hello')" # Should NOT be logged
|
||||
}
|
||||
)
|
||||
|
||||
content = log_file.read_text()
|
||||
# Should not contain actual code
|
||||
assert "print" not in content
|
||||
assert "hello" not in content
|
||||
# Should contain hash
|
||||
assert "abc123def456" in content
|
||||
|
||||
|
||||
def test_log_container_operation(tmp_path):
|
||||
"""Test log_container_operation convenience method."""
|
||||
from mcp_forge.security.audit import AuditLogger, AuditEventType
|
||||
|
||||
log_file = tmp_path / "audit.log"
|
||||
logger = AuditLogger(log_file)
|
||||
|
||||
logger.log_container_operation(
|
||||
operation="create",
|
||||
container_id="container-123",
|
||||
image="mcp-forge/python:3.11",
|
||||
session_id="session-456"
|
||||
)
|
||||
|
||||
lines = log_file.read_text().strip().split("\n")
|
||||
entry = json.loads(lines[0])
|
||||
|
||||
assert entry["event_type"] == "container.create"
|
||||
assert entry["container_id"] == "container-123"
|
||||
assert entry["image"] == "mcp-forge/python:3.11"
|
||||
assert entry["session_id"] == "session-456"
|
||||
|
||||
|
||||
def test_log_container_operation_with_error(tmp_path):
|
||||
"""Test logging container operation with error."""
|
||||
from mcp_forge.security.audit import AuditLogger
|
||||
|
||||
log_file = tmp_path / "audit.log"
|
||||
logger = AuditLogger(log_file)
|
||||
|
||||
logger.log_container_operation(
|
||||
operation="start",
|
||||
container_id="container-123",
|
||||
image="mcp-forge/python:3.11",
|
||||
session_id="session-456",
|
||||
error="Container not found"
|
||||
)
|
||||
|
||||
lines = log_file.read_text().strip().split("\n")
|
||||
entry = json.loads(lines[0])
|
||||
|
||||
assert "error" in entry
|
||||
assert entry["error"] == "Container not found"
|
||||
assert entry["severity"] == "error"
|
||||
|
||||
|
||||
def test_log_creates_directory_if_not_exists(tmp_path):
|
||||
"""Test that logger creates log directory if it doesn't exist."""
|
||||
from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity
|
||||
|
||||
log_dir = tmp_path / "nested" / "log" / "dir"
|
||||
log_file = log_dir / "audit.log"
|
||||
|
||||
logger = AuditLogger(log_file)
|
||||
|
||||
logger.log(
|
||||
event_type=AuditEventType.SESSION_CREATE,
|
||||
severity=AuditSeverity.INFO,
|
||||
message="Test"
|
||||
)
|
||||
|
||||
assert log_file.exists()
|
||||
assert log_file.parent.exists()
|
||||
|
||||
|
||||
def test_audit_event_types():
|
||||
"""Test that all required audit event types are defined."""
|
||||
from mcp_forge.security.audit import AuditEventType
|
||||
|
||||
# Required event types from todo.md
|
||||
assert hasattr(AuditEventType, "CONTAINER_CREATE")
|
||||
assert hasattr(AuditEventType, "CONTAINER_START")
|
||||
assert hasattr(AuditEventType, "CONTAINER_STOP")
|
||||
assert hasattr(AuditEventType, "CONTAINER_REMOVE")
|
||||
assert hasattr(AuditEventType, "EXECUTION_REQUEST")
|
||||
assert hasattr(AuditEventType, "SECURITY_VIOLATION")
|
||||
assert hasattr(AuditEventType, "BUILD_REQUEST")
|
||||
assert hasattr(AuditEventType, "BUILD_COMPLETE")
|
||||
assert hasattr(AuditEventType, "SESSION_CREATE")
|
||||
assert hasattr(AuditEventType, "SESSION_DESTROY")
|
||||
|
||||
|
||||
def test_audit_severity_levels():
|
||||
"""Test that all required severity levels are defined."""
|
||||
from mcp_forge.security.audit import AuditSeverity
|
||||
|
||||
assert hasattr(AuditSeverity, "INFO")
|
||||
assert hasattr(AuditSeverity, "WARNING")
|
||||
assert hasattr(AuditSeverity, "ERROR")
|
||||
assert hasattr(AuditSeverity, "CRITICAL")
|
||||
|
||||
|
||||
def test_log_with_all_optional_parameters(tmp_path):
|
||||
"""Test logging with all optional parameters provided."""
|
||||
from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity
|
||||
|
||||
log_file = tmp_path / "audit.log"
|
||||
logger = AuditLogger(log_file)
|
||||
|
||||
logger.log(
|
||||
event_type=AuditEventType.BUILD_COMPLETE,
|
||||
severity=AuditSeverity.INFO,
|
||||
message="Build completed successfully",
|
||||
session_id="session-123",
|
||||
user_id="user-456",
|
||||
details={"image": "custom-env", "duration": 120},
|
||||
error=None
|
||||
)
|
||||
|
||||
lines = log_file.read_text().strip().split("\n")
|
||||
entry = json.loads(lines[0])
|
||||
|
||||
assert entry["session_id"] == "session-123"
|
||||
assert entry["user_id"] == "user-456"
|
||||
assert entry["details"]["image"] == "custom-env"
|
||||
assert entry["details"]["duration"] == 120
|
||||
|
||||
|
||||
def test_multiple_log_entries_on_separate_lines(tmp_path):
|
||||
"""Test that multiple log entries are written on separate lines."""
|
||||
from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity
|
||||
|
||||
log_file = tmp_path / "audit.log"
|
||||
logger = AuditLogger(log_file)
|
||||
|
||||
for i in range(5):
|
||||
logger.log(
|
||||
event_type=AuditEventType.CONTAINER_CREATE,
|
||||
severity=AuditSeverity.INFO,
|
||||
message=f"Entry {i}"
|
||||
)
|
||||
|
||||
lines = log_file.read_text().strip().split("\n")
|
||||
assert len(lines) == 5
|
||||
|
||||
# Each line should be parseable
|
||||
for line in lines:
|
||||
json.loads(line)
|
||||
|
||||
|
||||
def test_log_security_violation_parameters(tmp_path):
|
||||
"""Test log_security_violation includes all necessary information."""
|
||||
from mcp_forge.security.audit import AuditLogger
|
||||
|
||||
log_file = tmp_path / "audit.log"
|
||||
logger = AuditLogger(log_file)
|
||||
|
||||
logger.log_security_violation(
|
||||
operation="volume_mount",
|
||||
reason="Attempted to mount /etc",
|
||||
session_id="session-789"
|
||||
)
|
||||
|
||||
lines = log_file.read_text().strip().split("\n")
|
||||
entry = json.loads(lines[0])
|
||||
|
||||
assert entry["event_type"] == "security.violation"
|
||||
assert entry["severity"] == "critical"
|
||||
assert entry["operation"] == "volume_mount"
|
||||
assert entry["reason"] == "Attempted to mount /etc"
|
||||
assert entry["session_id"] == "session-789"
|
||||
|
||||
|
||||
def test_details_can_be_none(tmp_path):
|
||||
"""Test that details parameter can be None."""
|
||||
from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity
|
||||
|
||||
log_file = tmp_path / "audit.log"
|
||||
logger = AuditLogger(log_file)
|
||||
|
||||
logger.log(
|
||||
event_type=AuditEventType.SESSION_CREATE,
|
||||
severity=AuditSeverity.INFO,
|
||||
message="Session created",
|
||||
details=None
|
||||
)
|
||||
|
||||
lines = log_file.read_text().strip().split("\n")
|
||||
entry = json.loads(lines[0])
|
||||
|
||||
# Should work without error
|
||||
assert "message" in entry
|
||||
Loading…
Add table
Add a link
Reference in a new issue