Files
Enterprise-MCP-Service-Bus/RI/tests/unit/test_audit.py
2026-09-14 08:18:53 -03:00

140 lines
4.6 KiB
Python

"""Audit events, sinks and correlation (EP-08-T02/T03, README.md §27/§28)."""
from __future__ import annotations
import json
from pathlib import Path
from emcp_bus.audit import events
from emcp_bus.audit.correlation import (
DecisionCorrelation,
compute_policy_decision_id,
new_decision_id,
)
from emcp_bus.audit.models import EventCategory
from emcp_bus.audit.sink import InMemoryAuditSink, JSONLFileAuditSink
def test_tool_call_denied_is_classified_as_grl() -> None:
event = events.tool_call_denied(
client_id="sales-read-agent",
profile_name="Sales.Read",
tool="finance.payment.execute",
reason_code="NOT_IN_ENTITLEMENT",
)
assert event.category is EventCategory.GRL
assert event.reason_code == "NOT_IN_ENTITLEMENT"
def test_tool_call_allowed_is_classified_as_ic() -> None:
correlation = DecisionCorrelation(
decision_id="d1",
policy_decision_id="p1",
entitlement_version="e1",
policy_version="v1",
mcp_request_id="1",
)
event = events.tool_call_allowed(
client_id="sales-read-agent",
profile_name="Sales.Read",
tool="sales.customer.get",
correlation=correlation,
)
assert event.category is EventCategory.IC
assert event.decision_id == "d1"
assert event.policy_decision_id == "p1"
assert event.entitlement_version == "e1"
def test_pdp_unavailable_is_classified_as_noc() -> None:
event = events.pdp_unavailable(client_id="sales-read-agent", tool="sales.customer.get")
assert event.category is EventCategory.NOC
def test_bypass_attempt_detected_is_classified_as_grl() -> None:
event = events.bypass_attempt_detected(backend="sales-domain", reason="missing_credential")
assert event.category is EventCategory.GRL
assert event.payload == {"backend": "sales-domain", "reason": "missing_credential"}
def test_event_payload_never_contains_a_raw_token_value() -> None:
"""README.md §27, 'Não registrar': a payload key that looks like a secret
is replaced by a hash reference, never left as the raw value."""
event = events.tool_call_allowed(
client_id="c1",
profile_name="Sales.Read",
tool="sales.customer.get",
correlation=DecisionCorrelation(
decision_id="d1",
policy_decision_id="p1",
entitlement_version=None,
policy_version=None,
mcp_request_id=None,
),
)
event.payload = {"backend_token": "super-secret-value", "customer_id": "cust-001"}
# Re-trigger sanitization the way construction does (model_post_init runs
# once at construction; this simulates a caller building payload then
# assigning it, which the real call sites never do -- included as an
# explicit regression guard on the sanitizer itself).
from emcp_bus.audit.models import _sanitize
sanitized = _sanitize(event.payload)
assert sanitized["backend_token"].startswith("sha256:")
assert sanitized["backend_token"] != "super-secret-value"
assert sanitized["customer_id"] == "cust-001"
def test_in_memory_sink_collects_emitted_events() -> None:
sink = InMemoryAuditSink()
event = events.client_authenticated(client_id="c1", mcp_request_id="1")
sink.emit(event)
assert sink.events == [event]
def test_jsonl_file_sink_appends_one_line_per_event(tmp_path: Path) -> None:
path = tmp_path / "audit" / "events.jsonl"
sink = JSONLFileAuditSink(path)
sink.emit(events.client_authenticated(client_id="c1", mcp_request_id="1"))
sink.emit(events.client_authenticated(client_id="c2", mcp_request_id="2"))
lines = path.read_text(encoding="utf-8").splitlines()
assert len(lines) == 2
assert json.loads(lines[0])["client_id"] == "c1"
assert json.loads(lines[1])["client_id"] == "c2"
def test_new_decision_id_is_unique_per_call() -> None:
assert new_decision_id() != new_decision_id()
def test_policy_decision_id_is_deterministic_for_identical_inputs() -> None:
def _compute() -> str:
return compute_policy_decision_id(
policy_version="v1",
client_profile="Sales.Write",
tool="sales.quote.create",
arguments={"amount": 100, "region": "BR-SP"},
)
assert _compute() == _compute()
def test_policy_decision_id_differs_for_different_arguments() -> None:
def _compute(amount: int) -> str:
return compute_policy_decision_id(
policy_version="v1",
client_profile="Sales.Write",
tool="sales.quote.create",
arguments={"amount": amount},
)
assert _compute(100) != _compute(200)