Finish KG-WP-0003: stream completeness and live qonto observation
Classify evidence as load-bearing or attributive, draft the emission-cadence declaration for Taxonomy, treat silence as a stream finding, keep completeness separate from record richness, forbid immune memory as a state plane, and make containment proposals reconstructable to their origin. Observe real qonto-assistant audit events; deny-class completeness stays unknown until the source publishes a heartbeat. Assistant: grok Assistant-Session: 01a05ef1-9e5a-70f2-b0ff-0b05d6b38ae9
This commit is contained in:
parent
c85646dc3c
commit
9daea96c43
35 changed files with 2023 additions and 138 deletions
|
|
@ -3,6 +3,9 @@ from __future__ import annotations
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SRC = Path(__file__).resolve().parents[1] / "src"
|
||||
if str(SRC) not in sys.path:
|
||||
sys.path.insert(0, str(SRC))
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SRC = ROOT / "src"
|
||||
TESTS = Path(__file__).resolve().parent
|
||||
for path in (SRC, TESTS):
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
|
|
|
|||
26
tests/helpers.py
Normal file
26
tests/helpers.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from kings_guard.adapters import observation_from_audit_event
|
||||
from kings_guard.contracts import ImmuneObservation
|
||||
from kings_guard.fixtures import QontoAssistantPilotFixture, load_qonto_assistant_pilot
|
||||
|
||||
|
||||
def load_pilot() -> QontoAssistantPilotFixture:
|
||||
return load_qonto_assistant_pilot()
|
||||
|
||||
|
||||
def observation_from_fixture(
|
||||
fixture: QontoAssistantPilotFixture | None = None,
|
||||
event: Mapping[str, object] | None = None,
|
||||
) -> ImmuneObservation:
|
||||
fixture = fixture or load_qonto_assistant_pilot()
|
||||
return observation_from_audit_event(
|
||||
event or fixture.audit_event,
|
||||
subject_id=fixture.normalization_hints["subject_id"],
|
||||
capability_scope=fixture.normalization_hints["capability_scope"],
|
||||
identity_binding=fixture.normalization_hints["identity_binding"],
|
||||
egress_destination=fixture.normalization_hints["egress_destination"],
|
||||
genome=fixture.genome,
|
||||
)
|
||||
19
tests/test_cadence_declaration.py
Normal file
19
tests/test_cadence_declaration.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DRAFT = ROOT / "specs" / "EmissionCadenceDeclaration.md"
|
||||
|
||||
|
||||
def test_emission_cadence_draft_exists_for_taxonomy_handover() -> None:
|
||||
text = DRAFT.read_text(encoding="utf-8")
|
||||
assert DRAFT.is_file()
|
||||
assert "owner: Taxonomy" in text
|
||||
assert "drafter: kings-guard" in text
|
||||
assert "qonto-assistant" in text
|
||||
assert "GH-WP-0002-T04" in text
|
||||
assert "approval-engine/cadence.yaml" in text
|
||||
assert "expected rate" in text.lower() or "expected-rate" in text
|
||||
assert "heartbeat" in text.lower()
|
||||
assert "reconciliation" in text.lower()
|
||||
assert "alongside the security genome" in text.lower() or "alongside the genome" in text.lower()
|
||||
assert "security_genome" in text
|
||||
111
tests/test_completeness.py
Normal file
111
tests/test_completeness.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
from kings_guard.cadence import load_qonto_assistant_cadence
|
||||
from kings_guard.contracts import (
|
||||
PostureAssessment,
|
||||
PostureLevel,
|
||||
ReconciliationView,
|
||||
StreamCompleteness,
|
||||
StreamHeartbeat,
|
||||
assessment_trust_key,
|
||||
)
|
||||
from kings_guard.posture import PostureEvaluator
|
||||
|
||||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
|
||||
def _healthy_assessment(*, completeness: StreamCompleteness, confidence: int) -> PostureAssessment:
|
||||
return PostureAssessment(
|
||||
posture=PostureLevel.HEALTHY,
|
||||
risk_score=5,
|
||||
confidence_score=confidence,
|
||||
stream_completeness=completeness,
|
||||
completeness_reason="test",
|
||||
findings=(),
|
||||
tolerated_findings=(),
|
||||
rationale="test",
|
||||
)
|
||||
|
||||
|
||||
def test_completeness_is_separated_from_record_richness() -> None:
|
||||
fixture = load_pilot()
|
||||
observation = observation_from_fixture(fixture)
|
||||
evaluation = PostureEvaluator().evaluate(fixture.genome, observation)
|
||||
|
||||
assert evaluation.assessment.confidence_score >= 85
|
||||
assert evaluation.assessment.stream_completeness is StreamCompleteness.UNKNOWN
|
||||
assert "record" in evaluation.assessment.completeness_reason
|
||||
assert "cannot vouch" in evaluation.assessment.rationale or "unknown" in evaluation.assessment.rationale
|
||||
|
||||
|
||||
def test_unmet_cadence_degrades_completeness_and_says_so_in_words() -> None:
|
||||
fixture = load_pilot()
|
||||
observation = observation_from_fixture(fixture)
|
||||
cadence = load_qonto_assistant_cadence()
|
||||
evaluation = PostureEvaluator().evaluate_with_stream(
|
||||
fixture.genome,
|
||||
observation,
|
||||
cadence,
|
||||
now="2026-07-24T10:10:00Z",
|
||||
watching_since="2026-07-22T09:10:00Z",
|
||||
heartbeats=(),
|
||||
reconciliation=ReconciliationView(source_counts={"audit.deny": 1}, evidence_counts={"audit.deny": 1}),
|
||||
)
|
||||
|
||||
assert evaluation.assessment.stream_completeness is StreamCompleteness.DEGRADED
|
||||
assert evaluation.assessment.confidence_score >= 85
|
||||
assert "cannot vouch" in evaluation.assessment.rationale
|
||||
|
||||
|
||||
def test_incomplete_stream_is_never_more_trustworthy_than_a_complete_one() -> None:
|
||||
rich_incomplete = _healthy_assessment(
|
||||
completeness=StreamCompleteness.DEGRADED, confidence=95
|
||||
)
|
||||
sparse_complete = _healthy_assessment(
|
||||
completeness=StreamCompleteness.COMPLETE, confidence=70
|
||||
)
|
||||
unknown = _healthy_assessment(completeness=StreamCompleteness.UNKNOWN, confidence=95)
|
||||
|
||||
assert assessment_trust_key(rich_incomplete) < assessment_trust_key(sparse_complete)
|
||||
assert assessment_trust_key(unknown) < assessment_trust_key(sparse_complete)
|
||||
assert assessment_trust_key(unknown) < assessment_trust_key(rich_incomplete)
|
||||
|
||||
|
||||
def test_complete_stream_keeps_richness_and_states_completeness() -> None:
|
||||
fixture = load_pilot()
|
||||
observation = observation_from_fixture(fixture)
|
||||
cadence = load_qonto_assistant_cadence()
|
||||
allow_event = dict(fixture.audit_event)
|
||||
allow_event["decision"] = "allow"
|
||||
allow_event["deny_reason"] = None
|
||||
allow_event["request_id"] = "req-allow-volume"
|
||||
from kings_guard.adapters import observation_from_audit_event
|
||||
|
||||
allow = observation_from_audit_event(
|
||||
allow_event,
|
||||
subject_id=fixture.normalization_hints["subject_id"],
|
||||
capability_scope=fixture.normalization_hints["capability_scope"],
|
||||
identity_binding=fixture.normalization_hints["identity_binding"],
|
||||
egress_destination=fixture.normalization_hints["egress_destination"],
|
||||
genome=fixture.genome,
|
||||
)
|
||||
evaluation = PostureEvaluator().evaluate_with_stream(
|
||||
fixture.genome,
|
||||
observation,
|
||||
cadence,
|
||||
now="2026-07-24T09:10:00Z",
|
||||
observations=(observation, allow),
|
||||
watching_since="2026-07-22T09:10:00Z",
|
||||
heartbeats=(
|
||||
StreamHeartbeat(
|
||||
source_system="qonto-assistant",
|
||||
timestamp="2026-07-24T09:00:00Z",
|
||||
event_class="audit.heartbeat",
|
||||
assertion="nothing-to-report",
|
||||
counts={"audit.deny": 1},
|
||||
),
|
||||
),
|
||||
reconciliation=ReconciliationView(source_counts={"audit.deny": 1}, evidence_counts={"audit.deny": 1}),
|
||||
)
|
||||
|
||||
assert evaluation.assessment.stream_completeness is StreamCompleteness.COMPLETE
|
||||
assert "Stream completeness is complete." in evaluation.assessment.rationale
|
||||
assert evaluation.assessment.confidence_score >= 85
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
from kings_guard.adapters import observation_from_audit_event
|
||||
from kings_guard.contracts import PostureLevel, SecurityGenome, as_jsonable
|
||||
from kings_guard.fixtures import load_qonto_assistant_pilot
|
||||
from kings_guard.posture import PostureEvaluator
|
||||
|
||||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
|
||||
def test_qonto_fixture_loads_a_normalized_genome() -> None:
|
||||
fixture = load_qonto_assistant_pilot()
|
||||
fixture = load_pilot()
|
||||
|
||||
assert isinstance(fixture.genome, SecurityGenome)
|
||||
assert fixture.genome.genome_id == "kg:genome:qonto-assistant"
|
||||
|
|
@ -15,14 +15,7 @@ def test_qonto_fixture_loads_a_normalized_genome() -> None:
|
|||
|
||||
|
||||
def test_qonto_audit_event_normalizes_to_immune_observation() -> None:
|
||||
fixture = load_qonto_assistant_pilot()
|
||||
observation = observation_from_audit_event(
|
||||
fixture.audit_event,
|
||||
subject_id=fixture.normalization_hints["subject_id"],
|
||||
capability_scope=fixture.normalization_hints["capability_scope"],
|
||||
identity_binding=fixture.normalization_hints["identity_binding"],
|
||||
egress_destination=fixture.normalization_hints["egress_destination"],
|
||||
)
|
||||
observation = observation_from_fixture()
|
||||
|
||||
assert observation.source_system == "qonto-assistant"
|
||||
assert observation.capability == "finance.qonto.read"
|
||||
|
|
@ -33,16 +26,11 @@ def test_qonto_audit_event_normalizes_to_immune_observation() -> None:
|
|||
|
||||
|
||||
def test_posture_evaluation_is_jsonable() -> None:
|
||||
fixture = load_qonto_assistant_pilot()
|
||||
observation = observation_from_audit_event(
|
||||
fixture.audit_event,
|
||||
subject_id=fixture.normalization_hints["subject_id"],
|
||||
capability_scope=fixture.normalization_hints["capability_scope"],
|
||||
identity_binding=fixture.normalization_hints["identity_binding"],
|
||||
egress_destination=fixture.normalization_hints["egress_destination"],
|
||||
)
|
||||
fixture = load_pilot()
|
||||
observation = observation_from_fixture(fixture)
|
||||
evaluation = PostureEvaluator().evaluate(fixture.genome, observation)
|
||||
payload = as_jsonable(evaluation)
|
||||
|
||||
assert payload["assessment"]["posture"] == PostureLevel.INFLAMED.value
|
||||
assert payload["signals"][0]["signal_kind"] == "posture_hint"
|
||||
assert payload["assessment"]["stream_completeness"] == "unknown"
|
||||
|
|
|
|||
39
tests/test_effector_origin.py
Normal file
39
tests/test_effector_origin.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from kings_guard.contracts import StreamCompleteness
|
||||
|
||||
from helpers import load_pilot, observation_from_fixture
|
||||
from kings_guard.posture import PostureEvaluator
|
||||
|
||||
|
||||
def test_effector_requests_carry_origin_and_do_not_widen_authority() -> None:
|
||||
fixture = load_pilot()
|
||||
observation = observation_from_fixture(fixture)
|
||||
evaluation = PostureEvaluator().evaluate(fixture.genome, observation)
|
||||
signal = evaluation.signals[0]
|
||||
|
||||
assert signal.effector_requests
|
||||
for request in signal.effector_requests:
|
||||
assert request.originating_observation_id == observation.observation_id
|
||||
assert request.originating_signal_id == signal.signal_id
|
||||
assert request.stream_completeness is StreamCompleteness.UNKNOWN
|
||||
assert request.authority_boundary in {"advisory_only", "metadata_only"}
|
||||
assert request.restrictive_direction in {
|
||||
"reduce_authority",
|
||||
"require_step_up",
|
||||
"request_containment",
|
||||
"none",
|
||||
}
|
||||
|
||||
assert signal.effector_requests[0].restrictive_direction == "reduce_authority"
|
||||
assert signal.effector_requests[1].restrictive_direction == "none"
|
||||
assert signal.metadata["originating_observation_id"] == observation.observation_id
|
||||
|
||||
|
||||
def test_adjacent_boundary_states_receiving_side_origin_expectation() -> None:
|
||||
from pathlib import Path
|
||||
|
||||
text = (Path(__file__).resolve().parents[1] / "docs" / "AdjacentSystemBoundary.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "originating observation" in text
|
||||
assert "decision record" in text
|
||||
assert "does not widen authority" in text
|
||||
91
tests/test_evidence_class.py
Normal file
91
tests/test_evidence_class.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import pytest
|
||||
|
||||
from kings_guard.adapters import observation_from_audit_event
|
||||
from kings_guard.contracts import DeclaredEvidenceSource, EvidenceClass
|
||||
|
||||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
|
||||
def test_genome_declares_both_evidence_classes() -> None:
|
||||
genome = load_pilot().genome
|
||||
classes = {source.event_class: source.evidence_class for source in genome.evidence_sources}
|
||||
|
||||
assert classes["audit.deny"] is EvidenceClass.LOAD_BEARING
|
||||
assert classes["audit.allow"] is EvidenceClass.ATTRIBUTIVE
|
||||
assert classes["audit.heartbeat"] is EvidenceClass.LOAD_BEARING
|
||||
|
||||
|
||||
def test_observation_copies_declared_class_and_does_not_infer_it() -> None:
|
||||
fixture = load_pilot()
|
||||
observation = observation_from_fixture(fixture)
|
||||
|
||||
declared = fixture.genome.source_for("audit.deny")
|
||||
assert declared is not None
|
||||
assert observation.evidence_class is declared.evidence_class
|
||||
assert observation.evidence_class is EvidenceClass.LOAD_BEARING
|
||||
joined = " ".join(fixture.evidence_class_reasoning)
|
||||
assert "source declares" in joined
|
||||
assert "does not infer" in joined
|
||||
assert all(source.reasoning for source in fixture.genome.evidence_sources)
|
||||
|
||||
|
||||
def test_allow_event_is_attributive_because_the_source_said_so() -> None:
|
||||
fixture = load_pilot()
|
||||
event = dict(fixture.audit_event)
|
||||
event["decision"] = "allow"
|
||||
event["deny_reason"] = None
|
||||
event["request_id"] = "req-allow"
|
||||
|
||||
observation = observation_from_audit_event(
|
||||
event,
|
||||
subject_id=fixture.normalization_hints["subject_id"],
|
||||
capability_scope=fixture.normalization_hints["capability_scope"],
|
||||
identity_binding=fixture.normalization_hints["identity_binding"],
|
||||
egress_destination=fixture.normalization_hints["egress_destination"],
|
||||
genome=fixture.genome,
|
||||
)
|
||||
|
||||
assert observation.event_class == "audit.allow"
|
||||
assert observation.evidence_class is EvidenceClass.ATTRIBUTIVE
|
||||
|
||||
|
||||
def test_class_mismatch_with_source_declaration_is_rejected() -> None:
|
||||
fixture = load_pilot()
|
||||
with pytest.raises(ValueError, match="does not match source declaration"):
|
||||
observation_from_audit_event(
|
||||
fixture.audit_event,
|
||||
subject_id=fixture.normalization_hints["subject_id"],
|
||||
capability_scope=fixture.normalization_hints["capability_scope"],
|
||||
identity_binding=fixture.normalization_hints["identity_binding"],
|
||||
egress_destination=fixture.normalization_hints["egress_destination"],
|
||||
genome=fixture.genome,
|
||||
evidence_class=EvidenceClass.ATTRIBUTIVE,
|
||||
)
|
||||
|
||||
|
||||
def test_class_cannot_be_inferred_from_event_contents() -> None:
|
||||
fixture = load_pilot()
|
||||
with pytest.raises(ValueError, match="does not infer"):
|
||||
observation_from_audit_event(
|
||||
fixture.audit_event,
|
||||
subject_id=fixture.normalization_hints["subject_id"],
|
||||
capability_scope=fixture.normalization_hints["capability_scope"],
|
||||
identity_binding=fixture.normalization_hints["identity_binding"],
|
||||
egress_destination=fixture.normalization_hints["egress_destination"],
|
||||
)
|
||||
|
||||
|
||||
def test_declared_source_is_expressible_on_genome_and_observation() -> None:
|
||||
source = DeclaredEvidenceSource(
|
||||
source_id="example.audit.deny",
|
||||
source_system="example",
|
||||
event_class="audit.deny",
|
||||
evidence_class=EvidenceClass.LOAD_BEARING,
|
||||
reasoning="denials are load-bearing by statute §9.6",
|
||||
cadence_form="heartbeat-or-reconciliation",
|
||||
)
|
||||
assert source.evidence_class is EvidenceClass.LOAD_BEARING
|
||||
assert observation_from_fixture().evidence_class in {
|
||||
EvidenceClass.LOAD_BEARING,
|
||||
EvidenceClass.ATTRIBUTIVE,
|
||||
}
|
||||
65
tests/test_immune_memory.py
Normal file
65
tests/test_immune_memory.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
|
||||
from kings_guard.contracts import ImmuneMemoryEntry
|
||||
from kings_guard.posture import PostureEvaluator
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
INTENT = ROOT / "INTENT.md"
|
||||
CONTRACTS = ROOT / "specs" / "ImmuneContracts.md"
|
||||
SRC = ROOT / "src" / "kings_guard"
|
||||
|
||||
|
||||
def test_memory_entry_forbids_runtime_dependency_by_other_layers() -> None:
|
||||
entry = ImmuneMemoryEntry(
|
||||
memory_id="mem:example",
|
||||
subject_scope="qonto-assistant",
|
||||
summary="credential-exfil probe pattern",
|
||||
derived_from=("req-qonto-deny-credential-exfil",),
|
||||
recommended_countermeasures=("lock_actor_temporarily",),
|
||||
)
|
||||
assert entry.runtime_input_for_other_layers == "forbidden"
|
||||
assert entry.confidentiality == "non-secret"
|
||||
|
||||
|
||||
def test_posture_evaluator_does_not_accept_memory_as_input() -> None:
|
||||
for name in ("evaluate", "evaluate_stream", "evaluate_with_stream"):
|
||||
signature = inspect.signature(getattr(PostureEvaluator, name))
|
||||
for parameter in signature.parameters.values():
|
||||
annotation = str(parameter.annotation)
|
||||
assert "ImmuneMemoryEntry" not in annotation, name
|
||||
|
||||
|
||||
def test_no_src_function_takes_memory_as_runtime_input() -> None:
|
||||
"""Catch the drift of wiring immune memory into an engine-facing input."""
|
||||
hits: list[str] = []
|
||||
for path in SRC.rglob("*.py"):
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
continue
|
||||
for arg in node.args.args + node.args.kwonlyargs:
|
||||
annotation = ast.unparse(arg.annotation) if arg.annotation is not None else ""
|
||||
if "ImmuneMemoryEntry" in annotation and arg.arg not in {"entry", "memory"}:
|
||||
hits.append(f"{path.name}:{node.name}:{arg.arg}")
|
||||
if "ImmuneMemoryEntry" in annotation and node.name.startswith("evaluate"):
|
||||
hits.append(f"{path.name}:{node.name} evaluates from memory")
|
||||
assert hits == []
|
||||
|
||||
|
||||
def test_intent_stage_five_forbids_memory_as_state_plane() -> None:
|
||||
text = INTENT.read_text(encoding="utf-8")
|
||||
collapsed = " ".join(text.split())
|
||||
assert "Federated memory" in collapsed
|
||||
assert "without becoming a state plane" in collapsed
|
||||
assert "no engine, PEP, or workload may read it as" in collapsed
|
||||
|
||||
|
||||
def test_immune_contracts_state_the_state_plane_rule() -> None:
|
||||
text = CONTRACTS.read_text(encoding="utf-8")
|
||||
assert "not a state plane" in text.lower()
|
||||
assert "runtime_input_for_other_layers" in text
|
||||
assert "Tooling catalog change" in text
|
||||
|
|
@ -64,6 +64,37 @@ def test_checker_passes_on_the_real_tree():
|
|||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
def test_agent_principal_rule_checks_are_honest():
|
||||
"""§3.4 claims that are tests, and claims that remain assertions, are named."""
|
||||
data = yaml.safe_load(DECL.read_text())
|
||||
checks = data["agent_principal_rule_checks"]
|
||||
assert checks["no_standing_credential"]["form"] == "test"
|
||||
assert checks["memory_is_not_a_state_plane"]["form"] == "test"
|
||||
assert checks["tool_use_shapes"]["form"] == "assertion"
|
||||
assert checks["reconstructable_as_caller"]["form"] == "mixed"
|
||||
assert data["agent_principal_rules"]["no_standing_credential"] is True
|
||||
|
||||
|
||||
def test_checker_catches_a_standing_credential(tmp_path, monkeypatch):
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.spec_from_file_location("check_layer_conformance", SCRIPT)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
fake_src = tmp_path / "src" / "kings_guard"
|
||||
fake_src.mkdir(parents=True)
|
||||
(fake_src / "secrets.py").write_text('VAULT_TOKEN = "s.standing-secret"\n')
|
||||
(tmp_path / ".env").write_text("OPENBAO_TOKEN=s.also-standing\n")
|
||||
monkeypatch.setattr(module, "SRC", fake_src)
|
||||
monkeypatch.setattr(module, "ROOT", tmp_path)
|
||||
|
||||
hits = module.scan_standing_credentials()
|
||||
assert hits, "a standing credential was not detected — the checker is blind"
|
||||
kinds = " ".join(reason for _, reason in hits)
|
||||
assert "credential-shaped file" in kinds or "standing-credential" in kinds
|
||||
|
||||
|
||||
def test_checker_catches_an_undeclared_tooling_client(tmp_path, monkeypatch):
|
||||
"""The negative case: a direct OpenBao client must fail the check.
|
||||
|
||||
|
|
|
|||
46
tests/test_live_qonto_observation.py
Normal file
46
tests/test_live_qonto_observation.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from kings_guard.live import capture_qonto_assistant_events, qonto_assistant_available
|
||||
from kings_guard.posture import PostureEvaluator
|
||||
|
||||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not qonto_assistant_available(),
|
||||
reason="qonto-assistant checkout is required for live observation",
|
||||
)
|
||||
|
||||
|
||||
def test_fixture_regression_case_is_retained() -> None:
|
||||
observation = observation_from_fixture()
|
||||
assert observation.observation_id == "req-qonto-deny-credential-exfil"
|
||||
assert observation.deny_reason == "credential_exfil"
|
||||
|
||||
|
||||
def test_real_emitted_qonto_events_reach_the_evaluator() -> None:
|
||||
fixture = load_pilot()
|
||||
capture = capture_qonto_assistant_events(fixture.genome)
|
||||
|
||||
assert len(capture.events) >= 2
|
||||
decisions = {event["decision"] for event in capture.events}
|
||||
assert "allow" in decisions
|
||||
assert "deny" in decisions
|
||||
assert capture.mapping_notes
|
||||
assert all(observation.source_system == "qonto-assistant" for observation in capture.observations)
|
||||
|
||||
deny = next(item for item in capture.observations if item.decision.value == "deny")
|
||||
evaluation = PostureEvaluator().evaluate(fixture.genome, deny)
|
||||
|
||||
assert evaluation.assessment.posture.value in {"elevated", "inflamed"}
|
||||
assert evaluation.signals
|
||||
for signal in evaluation.signals:
|
||||
for request in signal.effector_requests:
|
||||
assert request.authority_boundary in {"advisory_only", "metadata_only"}
|
||||
|
||||
# Mapping confirmed against the real emit path; remaining gaps are source omissions,
|
||||
# not adapter drift.
|
||||
assert any("identity_binding" in item for item in capture.corrections_for_source)
|
||||
assert any("egress_destination" in item for item in capture.corrections_for_source)
|
||||
assert not any("mapping drifted" in item for item in capture.corrections_for_source)
|
||||
|
|
@ -1,17 +1,11 @@
|
|||
from kings_guard.adapters import observation_from_audit_event
|
||||
from kings_guard.fixtures import load_qonto_assistant_pilot
|
||||
from kings_guard.posture import PostureEvaluator
|
||||
|
||||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
|
||||
def test_qonto_pilot_produces_inflamed_posture_with_tolerance_context() -> None:
|
||||
fixture = load_qonto_assistant_pilot()
|
||||
observation = observation_from_audit_event(
|
||||
fixture.audit_event,
|
||||
subject_id=fixture.normalization_hints["subject_id"],
|
||||
capability_scope=fixture.normalization_hints["capability_scope"],
|
||||
identity_binding=fixture.normalization_hints["identity_binding"],
|
||||
egress_destination=fixture.normalization_hints["egress_destination"],
|
||||
)
|
||||
fixture = load_pilot()
|
||||
observation = observation_from_fixture(fixture)
|
||||
|
||||
evaluation = PostureEvaluator().evaluate(fixture.genome, observation)
|
||||
|
||||
|
|
@ -22,14 +16,8 @@ def test_qonto_pilot_produces_inflamed_posture_with_tolerance_context() -> None:
|
|||
|
||||
|
||||
def test_qonto_pilot_emits_advisory_only_effector_requests() -> None:
|
||||
fixture = load_qonto_assistant_pilot()
|
||||
observation = observation_from_audit_event(
|
||||
fixture.audit_event,
|
||||
subject_id=fixture.normalization_hints["subject_id"],
|
||||
capability_scope=fixture.normalization_hints["capability_scope"],
|
||||
identity_binding=fixture.normalization_hints["identity_binding"],
|
||||
egress_destination=fixture.normalization_hints["egress_destination"],
|
||||
)
|
||||
fixture = load_pilot()
|
||||
observation = observation_from_fixture(fixture)
|
||||
|
||||
evaluation = PostureEvaluator().evaluate(fixture.genome, observation)
|
||||
signal = evaluation.signals[0]
|
||||
|
|
|
|||
134
tests/test_stream.py
Normal file
134
tests/test_stream.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
from kings_guard.cadence import load_qonto_assistant_cadence
|
||||
from kings_guard.contracts import (
|
||||
ReconciliationView,
|
||||
SignalKind,
|
||||
StreamCompleteness,
|
||||
StreamHeartbeat,
|
||||
)
|
||||
from kings_guard.posture import PostureEvaluator, is_stream_finding
|
||||
|
||||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
|
||||
def test_cadence_draft_covers_both_forms_against_qonto() -> None:
|
||||
cadence = load_qonto_assistant_cadence()
|
||||
assert cadence.status == "taxonomy-draft"
|
||||
assert cadence.owner == "Taxonomy"
|
||||
assert cadence.drafter == "kings-guard"
|
||||
assert cadence.source_system == "qonto-assistant"
|
||||
assert cadence.reference_instance == "GH-WP-0002-T04"
|
||||
assert cadence.forms() == {"expected-rate", "heartbeat-or-reconciliation"}
|
||||
|
||||
|
||||
def test_unmet_declared_rate_is_a_stream_finding() -> None:
|
||||
cadence = load_qonto_assistant_cadence()
|
||||
stream = PostureEvaluator().evaluate_stream(
|
||||
(),
|
||||
cadence,
|
||||
now="2026-07-24T09:10:00Z",
|
||||
watching_since="2026-07-22T09:10:00Z",
|
||||
heartbeats=(
|
||||
StreamHeartbeat(
|
||||
source_system="qonto-assistant",
|
||||
timestamp="2026-07-24T09:00:00Z",
|
||||
event_class="audit.heartbeat",
|
||||
assertion="nothing-to-report",
|
||||
counts={"audit.deny": 0},
|
||||
),
|
||||
),
|
||||
reconciliation=ReconciliationView(source_counts={"audit.deny": 0}, evidence_counts={"audit.deny": 0}),
|
||||
)
|
||||
|
||||
assert any(item.startswith("stream:cadence_unmet:audit.allow") for item in stream.findings)
|
||||
assert all(is_stream_finding(item) for item in stream.findings)
|
||||
assert stream.completeness is StreamCompleteness.DEGRADED
|
||||
|
||||
|
||||
def test_missing_heartbeat_is_a_stream_finding() -> None:
|
||||
fixture = load_pilot()
|
||||
observation = observation_from_fixture(fixture)
|
||||
cadence = load_qonto_assistant_cadence()
|
||||
stream = PostureEvaluator().evaluate_stream(
|
||||
(observation,),
|
||||
cadence,
|
||||
now="2026-07-24T10:10:00Z",
|
||||
watching_since="2026-07-22T09:10:00Z",
|
||||
heartbeats=(),
|
||||
reconciliation=ReconciliationView(source_counts={"audit.deny": 1}, evidence_counts={"audit.deny": 1}),
|
||||
)
|
||||
|
||||
assert any(item.startswith("stream:heartbeat_missing:audit.deny") for item in stream.findings)
|
||||
assert "credential_exfil_probe" not in stream.findings
|
||||
assert stream.completeness is StreamCompleteness.DEGRADED
|
||||
|
||||
|
||||
def test_reconciliation_divergence_is_a_stream_finding() -> None:
|
||||
fixture = load_pilot()
|
||||
observation = observation_from_fixture(fixture)
|
||||
cadence = load_qonto_assistant_cadence()
|
||||
stream = PostureEvaluator().evaluate_stream(
|
||||
(observation,),
|
||||
cadence,
|
||||
now="2026-07-23T10:10:00Z",
|
||||
watching_since="2026-07-23T09:10:00Z",
|
||||
heartbeats=(
|
||||
StreamHeartbeat(
|
||||
source_system="qonto-assistant",
|
||||
timestamp="2026-07-23T10:00:00Z",
|
||||
event_class="audit.heartbeat",
|
||||
assertion="nothing-to-report",
|
||||
counts={"audit.deny": 2},
|
||||
),
|
||||
),
|
||||
reconciliation=ReconciliationView(
|
||||
source_counts={"audit.deny": 2},
|
||||
evidence_counts={"audit.deny": 1},
|
||||
),
|
||||
)
|
||||
|
||||
assert any(
|
||||
item.startswith("stream:reconciliation_divergence:audit.deny") for item in stream.findings
|
||||
)
|
||||
assert stream.completeness is StreamCompleteness.DEGRADED
|
||||
|
||||
|
||||
def test_stream_findings_are_distinguishable_from_content_findings() -> None:
|
||||
fixture = load_pilot()
|
||||
observation = observation_from_fixture(fixture)
|
||||
cadence = load_qonto_assistant_cadence()
|
||||
evaluation = PostureEvaluator().evaluate_with_stream(
|
||||
fixture.genome,
|
||||
observation,
|
||||
cadence,
|
||||
now="2026-07-24T10:10:00Z",
|
||||
watching_since="2026-07-22T09:10:00Z",
|
||||
heartbeats=(),
|
||||
reconciliation=ReconciliationView(source_counts={"audit.deny": 1}, evidence_counts={"audit.deny": 1}),
|
||||
)
|
||||
|
||||
content_findings = set(evaluation.assessment.findings)
|
||||
stream_findings = set(evaluation.stream.findings if evaluation.stream else ())
|
||||
assert "credential_exfil_probe" in content_findings
|
||||
assert stream_findings
|
||||
assert content_findings.isdisjoint(stream_findings)
|
||||
assert all(is_stream_finding(item) for item in stream_findings)
|
||||
kinds = {signal.signal_kind for signal in evaluation.signals}
|
||||
assert SignalKind.POSTURE_HINT in kinds
|
||||
assert SignalKind.STREAM_COMPLETENESS in kinds
|
||||
|
||||
|
||||
def test_heartbeat_not_yet_due_does_not_false_alarm() -> None:
|
||||
fixture = load_pilot()
|
||||
observation = observation_from_fixture(fixture)
|
||||
cadence = load_qonto_assistant_cadence()
|
||||
stream = PostureEvaluator().evaluate_stream(
|
||||
(observation,),
|
||||
cadence,
|
||||
now=observation.timestamp,
|
||||
watching_since=observation.timestamp,
|
||||
heartbeats=(),
|
||||
reconciliation=ReconciliationView(source_counts={"audit.deny": 1}, evidence_counts={"audit.deny": 1}),
|
||||
)
|
||||
|
||||
assert not any(item.startswith("stream:heartbeat_missing") for item in stream.findings)
|
||||
assert stream.completeness is StreamCompleteness.UNKNOWN
|
||||
Loading…
Add table
Add a link
Reference in a new issue