xsecuremcp / tests /reflexive /test_monitors.py
svkrishna
🤖 Implement Story 4: Reflexive Core
7b7b417
Raw
History Blame Contribute Delete
16.8 kB
"""Tests for the reflexive monitors."""
import pytest
from datetime import datetime, timedelta
from unittest.mock import Mock
from fastmcp.reflexive.monitor import PolicyMonitor, LedgerMonitor, AnomalyDetector
from fastmcp.reflexive.engine import ActionContext
class TestPolicyMonitor:
"""Test the PolicyMonitor class."""
@pytest.fixture
def policy_monitor(self):
"""Create a policy monitor for testing."""
return PolicyMonitor()
@pytest.fixture
def action_context(self):
"""Create an action context for testing."""
return ActionContext(
action_id="test_action",
actor_id="test_user",
action_type="tool_call",
resource_id="test_resource"
)
async def test_monitor_no_violations(self, policy_monitor, action_context):
"""Test monitoring with no violations."""
result = await policy_monitor(action_context)
assert result is None
async def test_monitor_admin_access_violation(self, policy_monitor, action_context):
"""Test monitoring admin access violation."""
# Create action context with admin access by guest user
admin_context = ActionContext(
action_id="admin_action",
actor_id="guest_user",
action_type="admin_access",
resource_id="admin_panel"
)
result = await policy_monitor(admin_context)
assert result is not None
assert result["type"] == "violation"
assert result["severity"] == "high"
assert len(result["violations"]) == 1
assert result["violations"][0]["rule"] == "admin_access_restriction"
async def test_monitor_rate_limit_violation(self, policy_monitor, action_context):
"""Test monitoring rate limit violation."""
# Add multiple violations for the same actor
for i in range(5):
violation_context = ActionContext(
action_id=f"action_{i}",
actor_id="rate_limit_user",
action_type="api_call"
)
# Manually add violations to history
policy_monitor.violation_history.append({
"type": "violation",
"severity": "low",
"actor_id": "rate_limit_user",
"action_id": f"action_{i}",
"timestamp": datetime.utcnow().isoformat()
})
# Test rate limit violation
result = await policy_monitor(action_context)
# Should not trigger rate limit for different actor
assert result is None
# Test with same actor
rate_limit_context = ActionContext(
action_id="rate_limit_action",
actor_id="rate_limit_user",
action_type="api_call"
)
result = await policy_monitor(rate_limit_context)
assert result is not None
assert result["type"] == "violation"
assert result["severity"] == "medium"
assert len(result["violations"]) == 1
assert result["violations"][0]["rule"] == "rate_limit_exceeded"
async def test_monitor_sensitive_resource_violation(self, policy_monitor, action_context):
"""Test monitoring sensitive resource access violation."""
sensitive_context = ActionContext(
action_id="sensitive_action",
actor_id="unauthorized_user",
action_type="data_access",
resource_id="sensitive_data",
metadata={"authorized": False}
)
result = await policy_monitor(sensitive_context)
assert result is not None
assert result["type"] == "violation"
assert result["severity"] == "critical"
assert len(result["violations"]) == 1
assert result["violations"][0]["rule"] == "unauthorized_sensitive_access"
def test_assess_violation_severity(self, policy_monitor):
"""Test violation severity assessment."""
# Test critical severity
critical_violations = [{"severity": "critical"}]
assert policy_monitor._assess_violation_severity(critical_violations) == "critical"
# Test high severity
high_violations = [{"severity": "high"}]
assert policy_monitor._assess_violation_severity(high_violations) == "high"
# Test medium severity
medium_violations = [{"severity": "medium"}]
assert policy_monitor._assess_violation_severity(medium_violations) == "medium"
# Test low severity
low_violations = [{"severity": "low"}]
assert policy_monitor._assess_violation_severity(low_violations) == "low"
# Test mixed severities
mixed_violations = [{"severity": "low"}, {"severity": "high"}]
assert policy_monitor._assess_violation_severity(mixed_violations) == "high"
def test_get_violation_stats(self, policy_monitor):
"""Test getting violation statistics."""
# Add some violations
policy_monitor.violation_history.append({
"type": "violation",
"severity": "high",
"actor_id": "user1",
"action_id": "action1",
"timestamp": datetime.utcnow().isoformat()
})
policy_monitor.actor_violations["user1"] = 1
policy_monitor.actor_violations["user2"] = 2
stats = policy_monitor.get_violation_stats()
assert stats["total_violations"] == 1
assert stats["actor_violations"]["user1"] == 1
assert stats["actor_violations"]["user2"] == 2
assert "recent_violations" in stats
class TestLedgerMonitor:
"""Test the LedgerMonitor class."""
@pytest.fixture
def ledger_monitor(self):
"""Create a ledger monitor for testing."""
return LedgerMonitor()
@pytest.fixture
def mock_ledger(self):
"""Create a mock ledger for testing."""
mock_ledger = Mock()
mock_ledger.verify_chain_integrity.return_value = True
mock_ledger.get_ledger_statistics.return_value = {
"total_entries": 10,
"total_blocks": 2
}
return mock_ledger
@pytest.fixture
def action_context(self):
"""Create an action context for testing."""
return ActionContext(
action_id="test_action",
actor_id="test_user",
action_type="tool_call"
)
async def test_monitor_no_issues(self, ledger_monitor, mock_ledger, action_context):
"""Test monitoring with no ledger issues."""
ledger_monitor.ledger = mock_ledger
result = await ledger_monitor(action_context)
assert result is None
async def test_monitor_chain_integrity_issue(self, ledger_monitor, mock_ledger, action_context):
"""Test monitoring chain integrity issue."""
# Mock chain integrity failure
mock_ledger.verify_chain_integrity.return_value = False
ledger_monitor.ledger = mock_ledger
result = await ledger_monitor(action_context)
assert result is not None
assert result["type"] == "anomaly"
assert result["severity"] == "critical"
assert len(result["issues"]) == 1
assert result["issues"][0]["type"] == "chain_integrity"
async def test_monitor_missing_blocks_issue(self, ledger_monitor, mock_ledger, action_context):
"""Test monitoring missing blocks issue."""
# Mock missing blocks scenario
mock_ledger.get_ledger_statistics.return_value = {
"total_entries": 10,
"total_blocks": 0
}
ledger_monitor.ledger = mock_ledger
result = await ledger_monitor(action_context)
assert result is not None
assert result["type"] == "anomaly"
assert result["severity"] == "high"
assert len(result["issues"]) == 1
assert result["issues"][0]["type"] == "missing_blocks"
async def test_monitor_integrity_check_error(self, ledger_monitor, action_context):
"""Test monitoring with integrity check error."""
# Mock ledger that raises exception
mock_ledger = Mock()
mock_ledger.verify_chain_integrity.side_effect = Exception("Database error")
ledger_monitor.ledger = mock_ledger
result = await ledger_monitor(action_context)
assert result is not None
assert result["type"] == "anomaly"
assert result["severity"] == "medium"
assert len(result["issues"]) == 1
assert result["issues"][0]["type"] == "integrity_check_error"
def test_assess_integrity_severity(self, ledger_monitor):
"""Test integrity severity assessment."""
# Test critical severity
critical_issues = [{"severity": "critical"}]
assert ledger_monitor._assess_integrity_severity(critical_issues) == "critical"
# Test high severity
high_issues = [{"severity": "high"}]
assert ledger_monitor._assess_integrity_severity(high_issues) == "high"
# Test medium severity
medium_issues = [{"severity": "medium"}]
assert ledger_monitor._assess_integrity_severity(medium_issues) == "medium"
# Test low severity
low_issues = [{"severity": "low"}]
assert ledger_monitor._assess_integrity_severity(low_issues) == "low"
def test_get_integrity_stats(self, ledger_monitor):
"""Test getting integrity statistics."""
# Add some integrity checks
ledger_monitor.integrity_checks.append({
"type": "anomaly",
"severity": "high",
"timestamp": datetime.utcnow().isoformat()
})
stats = ledger_monitor.get_integrity_stats()
assert stats["total_checks"] == 1
assert "recent_issues" in stats
class TestAnomalyDetector:
"""Test the AnomalyDetector class."""
@pytest.fixture
def anomaly_detector(self):
"""Create an anomaly detector for testing."""
return AnomalyDetector()
@pytest.fixture
def action_context(self):
"""Create an action context for testing."""
return ActionContext(
action_id="test_action",
actor_id="test_user",
action_type="tool_call",
resource_id="test_resource"
)
async def test_detector_no_anomalies(self, anomaly_detector, action_context):
"""Test detection with no anomalies."""
# First access to a resource will be flagged as new resource access
# So we need to access the resource twice to avoid the "new resource" anomaly
await anomaly_detector(action_context)
result = await anomaly_detector(action_context)
assert result is None
async def test_detector_high_frequency_anomaly(self, anomaly_detector, action_context):
"""Test detection of high frequency anomaly."""
# Add many recent actions for the same actor
for i in range(25):
recent_context = ActionContext(
action_id=f"action_{i}",
actor_id="high_frequency_user",
action_type="api_call"
)
anomaly_detector._update_patterns(recent_context)
# Test high frequency detection
high_freq_context = ActionContext(
action_id="high_freq_action",
actor_id="high_frequency_user",
action_type="api_call"
)
result = await anomaly_detector(high_freq_context)
assert result is not None
assert result["type"] == "anomaly"
assert result["severity"] == "medium"
assert len(result["anomalies"]) == 1
assert result["anomalies"][0]["type"] == "high_frequency"
async def test_detector_unusual_timing_anomaly(self, anomaly_detector, action_context):
"""Test detection of unusual timing anomaly."""
# Create action at unusual hour with new action type
unusual_context = ActionContext(
action_id="unusual_action",
actor_id="test_user",
action_type="new_action_type" # New action type
)
unusual_context.timestamp = datetime.utcnow().replace(hour=3) # 3 AM
result = await anomaly_detector(unusual_context)
assert result is not None
assert result["type"] == "anomaly"
assert result["severity"] == "low"
assert len(result["anomalies"]) == 1
assert result["anomalies"][0]["type"] == "unusual_timing"
async def test_detector_new_resource_access_anomaly(self, anomaly_detector, action_context):
"""Test detection of new resource access anomaly."""
# First access to a resource
new_resource_context = ActionContext(
action_id="new_resource_action",
actor_id="test_user",
action_type="data_access",
resource_id="new_resource"
)
result = await anomaly_detector(new_resource_context)
assert result is not None
assert result["type"] == "anomaly"
assert result["severity"] == "low"
assert len(result["anomalies"]) == 1
assert result["anomalies"][0]["type"] == "new_resource_access"
async def test_detector_privilege_escalation_anomaly(self, anomaly_detector, action_context):
"""Test detection of privilege escalation anomaly."""
# First time performing privileged action
privilege_context = ActionContext(
action_id="privilege_action",
actor_id="test_user",
action_type="admin_access",
resource_id="admin_panel"
)
result = await anomaly_detector(privilege_context)
assert result is not None
assert result["type"] == "anomaly"
assert result["severity"] == "high"
# Should detect both new resource access and privilege escalation
assert len(result["anomalies"]) >= 1
# Check that privilege escalation is detected
privilege_anomalies = [a for a in result["anomalies"] if a["type"] == "privilege_escalation"]
assert len(privilege_anomalies) == 1
assert privilege_anomalies[0]["type"] == "privilege_escalation"
def test_update_patterns(self, anomaly_detector, action_context):
"""Test pattern updating."""
# Update patterns
anomaly_detector._update_patterns(action_context)
# Check that patterns were updated
actor_data = anomaly_detector.actor_patterns[action_context.actor_id]
assert actor_data["action_counts"][action_context.action_type] == 1
assert actor_data["resource_access"][action_context.resource_id] == 1
assert len(actor_data["session_times"]) == 1
assert actor_data["last_seen"] == action_context.timestamp
# Check global patterns
assert anomaly_detector.global_patterns["action_frequency"][action_context.action_type] == 1
assert anomaly_detector.global_patterns["resource_access"][action_context.resource_id] == 1
def test_assess_anomaly_severity(self, anomaly_detector):
"""Test anomaly severity assessment."""
# Test high severity
high_anomalies = [{"severity": "high"}]
assert anomaly_detector._assess_anomaly_severity(high_anomalies) == "high"
# Test medium severity
medium_anomalies = [{"severity": "medium"}]
assert anomaly_detector._assess_anomaly_severity(medium_anomalies) == "medium"
# Test low severity
low_anomalies = [{"severity": "low"}]
assert anomaly_detector._assess_anomaly_severity(low_anomalies) == "low"
# Test mixed severities
mixed_anomalies = [{"severity": "low"}, {"severity": "high"}]
assert anomaly_detector._assess_anomaly_severity(mixed_anomalies) == "high"
def test_get_anomaly_stats(self, anomaly_detector, action_context):
"""Test getting anomaly statistics."""
# Update patterns for some actors
anomaly_detector._update_patterns(action_context)
another_context = ActionContext(
action_id="another_action",
actor_id="another_user",
action_type="another_type"
)
anomaly_detector._update_patterns(another_context)
stats = anomaly_detector.get_anomaly_stats()
assert stats["tracked_actors"] == 2
assert stats["global_action_types"] == 2
assert stats["global_resources"] == 1