audit-core/tests/test_emission_cadence.py

220 lines
8.2 KiB
Python
Raw Normal View History

"""AUDIT-WP-0009-T05 — declared emission cadence, evaluated by the observer."""
import copy
import json
import pytest
from audit_core.emission_cadence import (
BELOW_CADENCE,
CONTRACT_DIGEST,
duration_seconds,
evaluate,
parse_declaration,
)
from audit_core.ingestion import IngestionApplication
from audit_core.senders import SenderIdentity, SenderRegistry, _identity_from
from audit_core.sqlite_backend import SQLiteAuditBackend
from tests.test_stream_findings import emit, invoke
RATE = {
"schema_version": "0.1",
"declaration_id": "approval-engine.audit.v1",
"source": "approval-engine",
"stream_id": "approval-engine.audit",
"sources": [
{
"source_id": "approval-engine.issued",
"event_class": "approval.issued",
"form": "expected-rate",
"window": "PT1H",
"expected_min": 2,
"drop_below": "finding",
},
{
"source_id": "approval-engine.revocation",
"event_class": "approval.revocation",
"form": "heartbeat-or-reconciliation",
"heartbeat": {
"event_class": "approval-engine.heartbeat",
"interval": "24h",
"assertion": "nothing-to-report",
"missing": "finding",
},
},
],
}
# net-kingdom commit 116643f, local-identity/emission-cadence.yaml, as JSON.
# The one real source-owned declaration published so far.
NET_KINGDOM_LOCAL_IDENTITY = {
"schema_version": "0.1",
"declaration_id": "net-kingdom.local-identity.audit.v1",
"source": "net-kingdom",
"stream_id": "net-kingdom.local-identity.audit",
"extensions": {"netkingdom": {"contract_digest": "972c0b6701d1693f",
"contract_document_version": "0.2.0"}},
"sources": [
{
"source_id": f"net-kingdom.local-identity.audit.{name}",
"source_system": "local-identity",
"event_class": cls,
"form": "heartbeat-or-reconciliation",
"reconciliation": {
"compare_local": f"audit_log_counts.{cls}",
"compare_observed": f"evidence_counts.{cls}",
"divergence": "finding",
},
"extensions": {"netkingdom": {"evidence_class": "load-bearing",
"rate_monitoring": "forbidden",
"completeness_claimed": False,
"heartbeat_emitted": False}},
}
for name, cls in (("token-issued", "serve/token.token_issued"),
("token-revoked", "revoke-token"))
],
}
def _sender(declaration=RATE, sources=("approval-engine",)):
return SenderIdentity(
name="approval-engine", tokens=("approval",), sources=frozenset(sources),
tenants=frozenset({"tenant:platform"}),
emission_cadence=parse_declaration(declaration),
)
@pytest.mark.parametrize("text,seconds", [
("PT1H", 3600), ("P1D", 86400), ("P1DT30M", 88200), ("90s", 90), ("5m", 300), ("1d", 86400),
])
def test_durations_follow_the_schema_pattern(text, seconds):
assert duration_seconds(text) == seconds
@pytest.mark.parametrize("text", ["P", "PT", "PT0S", "0s", "1w", "3600", "P1H"])
def test_durations_the_schema_refuses_are_refused(text):
with pytest.raises(ValueError):
duration_seconds(text)
def test_both_forms_parse_and_only_rates_are_indexed_for_evaluation():
declaration = parse_declaration(RATE)
assert [(r.event_class, r.window_seconds, r.expected_min) for r in declaration.rates] == [
("approval.issued", 3600, 2)
]
assert declaration.summary()["heartbeat_or_reconciliation_classes"] == ["approval.revocation"]
assert declaration.summary()["contract_digest"] == CONTRACT_DIGEST
def test_the_published_net_kingdom_declaration_is_accepted():
"""Observer evaluation of the one real declaration: it fits the contract."""
declaration = parse_declaration(NET_KINGDOM_LOCAL_IDENTITY)
assert declaration.rates == ()
assert len(declaration.other_entries) == 2
def _broken(mutate):
doc = copy.deepcopy(RATE)
mutate(doc)
return doc
@pytest.mark.parametrize("mutate", [
lambda d: d.update(schema_version="0.2"),
lambda d: d.pop("declaration_id"),
lambda d: d.update(sources=[]),
lambda d: d.update(unknown=1),
lambda d: d["sources"][0].pop("expected_min"),
lambda d: d["sources"][0].pop("drop_below"),
lambda d: d["sources"][0].update(window_seconds=3600), # both window forms
lambda d: d["sources"][0].pop("window"), # neither
lambda d: d["sources"][0].update(expected_min=-1),
lambda d: d["sources"][0].update(heartbeat=RATE["sources"][1]["heartbeat"]),
lambda d: d["sources"][0].update(form="rate"),
lambda d: d["sources"][1].pop("heartbeat"), # neither half
lambda d: d["sources"][1].update(window="PT1H"),
lambda d: d["sources"][1]["heartbeat"].pop("missing"),
lambda d: d["sources"][1]["heartbeat"].update(interval_seconds=60),
lambda d: d["sources"].append(copy.deepcopy(d["sources"][0])), # duplicate class
])
def test_what_the_schema_refuses_is_refused(mutate):
with pytest.raises(ValueError):
parse_declaration(_broken(mutate))
def _counter(counts):
return lambda source, since, until: [{"class": c, "count": n} for c, n in counts.items()]
def test_a_stream_below_its_declared_rate_is_a_finding():
findings = evaluate([_sender()], _counter({"approval.issued": 1}))
assert len(findings) == 1
finding = findings[0].as_dict()
assert finding["kind"] == BELOW_CADENCE
assert (finding["observed"], finding["expected_min"]) == (1, 2)
assert "not proof of suppression" in finding["means"]
def test_a_stream_at_its_declared_rate_is_not_a_finding():
assert evaluate([_sender()], _counter({"approval.issued": 2})) == []
def test_an_undeclared_class_is_never_a_finding():
"""Heartbeat-or-reconciliation classes are T04/T06's, not rate-monitored."""
findings = evaluate([_sender()], _counter({"approval.issued": 5}))
assert all(f.event_class != "approval.revocation" for f in findings)
def test_a_wildcard_source_is_not_held_to_a_rate():
assert evaluate([_sender(sources=("*",))], _counter({})) == []
def test_the_registration_carries_the_declaration():
identity = _identity_from({
"name": "approval-engine", "token": "t", "sources": ["approval-engine"],
"emission_cadence": RATE,
})
assert identity.evidence_declaration()["emission_cadence"]["declaration_id"] == (
"approval-engine.audit.v1"
)
def test_an_invalid_declaration_refuses_the_registration():
with pytest.raises(ValueError):
_identity_from({
"name": "x", "token": "t", "sources": ["x"],
"emission_cadence": _broken(lambda d: d.pop("sources")),
})
def test_the_overlay_may_not_touch_the_declaration(tmp_path):
base = {"name": "approval-engine", "token": "t", "sources": ["approval-engine"],
"emission_cadence": RATE}
scope = tmp_path / "scope.json"
scope.write_text(json.dumps([{"name": "approval-engine", "emission_cadence": RATE}]))
with pytest.raises(ValueError, match="emission_cadence"):
SenderRegistry.from_env({
"AUDIT_CORE_SENDERS": json.dumps([base]),
"AUDIT_CORE_SENDERS_SCOPE_PATH": str(scope),
})
def test_the_findings_surface_reports_a_cadence_miss_and_clears_on_delivery(tmp_path):
operator = SenderIdentity(
name="operator", tokens=("operator",), sources=frozenset({"approval-engine"}),
tenants=frozenset({"*"}), may_write=False, may_read=True,
)
app = IngestionApplication(
SQLiteAuditBackend(str(tmp_path / "s.db")), SenderRegistry([_sender(), operator])
)
def misses():
status, body = invoke(app, "/v1/stream-findings", token="operator")
assert status.startswith("200")
return [f for f in body["stream_findings"] if f["kind"] == BELOW_CADENCE]
assert [f["observed"] for f in misses()] == [0]
for n in range(2):
assert emit(app, f"evt-{n}", "approval.issued")[0].startswith("202")
assert misses() == []