mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 01:48:17 +00:00
feat(detection-core): bind evidence packets to neutral decisions
This commit is contained in:
@@ -64,7 +64,7 @@
|
||||
- Refuse protected-population influence, redirected artifact identities, wrong purposes, and incomplete receipt history.
|
||||
- Done when matching subjects cannot mask a wrong influenced artifact and contaminated populations are deterministically refused.
|
||||
- _Requirements: 13.1, 13.3, 13.4_
|
||||
- [ ] 2.5 Bind immutable evidence packets to neutral decisions
|
||||
- [x] 2.5 Bind immutable evidence packets to neutral decisions
|
||||
- Preserve family identity, security object, analyzed scope, ordered semantics, observations, family fields, completeness, and provenance.
|
||||
- Keep family decision codes separate from violation, satisfied, and indeterminate outcomes.
|
||||
- Require bounded scope for satisfaction, named reasons for indeterminate outcomes, and a valid deterministic, model-corroborated, or execution-backed basis.
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
"""Immutable family-neutral evidence packets and neutral decisions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from openultrasast.core import canonical
|
||||
from openultrasast.core.provenance import DecisionProvenance, ModelCallIdentity, ProvenanceStamp
|
||||
from openultrasast.core.semantics import (
|
||||
EvidenceProvenance,
|
||||
FieldValue,
|
||||
Observation,
|
||||
RelationStatus,
|
||||
SemanticNode,
|
||||
SemanticRelation,
|
||||
SourceLocation,
|
||||
)
|
||||
|
||||
EVIDENCE_PACKET_SCHEMA = "openultrasast.detection-core.evidence-packet.v2"
|
||||
EVIDENCE_RECORD_SCHEMA = "openultrasast.detection-core.evidence-record.v2"
|
||||
_DIGEST = re.compile(r"[0-9a-f]{64}")
|
||||
|
||||
|
||||
class EvidenceValidationError(ValueError):
|
||||
"""An evidence packet or decision violates the neutral core contract."""
|
||||
|
||||
|
||||
class NeutralOutcome(Enum):
|
||||
VIOLATION = "violation"
|
||||
SATISFIED = "satisfied"
|
||||
INDETERMINATE = "indeterminate"
|
||||
|
||||
|
||||
class EvidenceBasis(Enum):
|
||||
DETERMINISTIC = "deterministic"
|
||||
MODEL_CORROBORATED = "model_corroborated"
|
||||
EXECUTION_BACKED = "execution_backed"
|
||||
|
||||
|
||||
def _require_text(value: object, field: str) -> None:
|
||||
if not isinstance(value, str) or not value:
|
||||
raise EvidenceValidationError(f"{field} must be nonempty text")
|
||||
|
||||
|
||||
def _require_digest(value: object, field: str) -> None:
|
||||
if not isinstance(value, str) or _DIGEST.fullmatch(value) is None:
|
||||
raise EvidenceValidationError(f"{field} must be a full lowercase SHA-256 digest")
|
||||
|
||||
|
||||
def _require_text_tuple(value: object, field: str) -> None:
|
||||
if not isinstance(value, tuple) or not all(isinstance(item, str) and item for item in value):
|
||||
raise EvidenceValidationError(f"{field} must be a tuple of nonempty text")
|
||||
try:
|
||||
canonical.require_unique_identities(value, kind=field)
|
||||
except canonical.DuplicateIdentityError as exc:
|
||||
raise EvidenceValidationError(str(exc)) from exc
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SourceRegion:
|
||||
path: str
|
||||
start_line: int
|
||||
end_line: int
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_require_text(self.path, "path")
|
||||
for value, field in ((self.start_line, "start_line"), (self.end_line, "end_line")):
|
||||
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
|
||||
raise EvidenceValidationError(f"{field} must be a positive line number")
|
||||
if self.end_line < self.start_line:
|
||||
raise EvidenceValidationError("end_line must not precede start_line")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AnalyzedScope:
|
||||
base_revision: str
|
||||
head_revision: str
|
||||
change_digest: str
|
||||
regions: tuple[SourceRegion, ...]
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_require_text(self.base_revision, "base_revision")
|
||||
_require_text(self.head_revision, "head_revision")
|
||||
_require_digest(self.change_digest, "change_digest")
|
||||
if not isinstance(self.regions, tuple) or not self.regions:
|
||||
raise EvidenceValidationError("regions must be a nonempty tuple")
|
||||
if not all(isinstance(region, SourceRegion) for region in self.regions):
|
||||
raise EvidenceValidationError("every region must be a SourceRegion")
|
||||
identities = tuple(f"{region.path}:{region.start_line}:{region.end_line}" for region in self.regions)
|
||||
try:
|
||||
canonical.require_unique_identities(identities, kind="region")
|
||||
except canonical.DuplicateIdentityError as exc:
|
||||
raise EvidenceValidationError(str(exc)) from exc
|
||||
|
||||
|
||||
def _location_value(location: SourceLocation) -> canonical.JsonValue:
|
||||
return {"path": location.path, "line": location.line, "column": location.column, "symbol": location.symbol}
|
||||
|
||||
|
||||
def _node_value(node: SemanticNode) -> canonical.JsonValue:
|
||||
return {"node_id": node.node_id, "role": node.role, "location": _location_value(node.location)}
|
||||
|
||||
|
||||
def _content_ref_value(reference: canonical.ContentRef) -> canonical.JsonValue:
|
||||
return {
|
||||
"artifact_id": reference.artifact_id,
|
||||
"content_digest": reference.content_digest,
|
||||
"media_type": reference.media_type,
|
||||
"byte_length": reference.byte_length,
|
||||
"retention_policy_digest": reference.retention_policy_digest,
|
||||
"storage_receipt_digest": reference.storage_receipt_digest,
|
||||
}
|
||||
|
||||
|
||||
def _semantic_provenance_value(value: EvidenceProvenance) -> canonical.JsonValue:
|
||||
fact_match: canonical.JsonValue = None if value.fact_match is None else _content_ref_value(value.fact_match)
|
||||
return {
|
||||
"kind": value.kind.value,
|
||||
"source_content_digest": value.source_content_digest,
|
||||
"semantic_pack_digest": value.semantic_pack_digest,
|
||||
"semantic_fact_id": value.semantic_fact_id,
|
||||
"fact_match": fact_match,
|
||||
}
|
||||
|
||||
|
||||
def _field_value(value: FieldValue) -> canonical.JsonValue:
|
||||
return {"name": value.name, "kind": value.kind.value, "value": value.value}
|
||||
|
||||
|
||||
def _relation_value(value: SemanticRelation) -> canonical.JsonValue:
|
||||
return {
|
||||
"relation_id": value.relation_id,
|
||||
"kind": value.kind.value,
|
||||
"role": value.role,
|
||||
"source": _node_value(value.source),
|
||||
"target": _node_value(value.target),
|
||||
"location": _location_value(value.location),
|
||||
"status": value.status.value,
|
||||
"conditions": value.conditions,
|
||||
"reasons": value.reasons,
|
||||
"provenance": _semantic_provenance_value(value.provenance),
|
||||
}
|
||||
|
||||
|
||||
def _observation_value(value: Observation) -> canonical.JsonValue:
|
||||
return {
|
||||
"observation_id": value.observation_id,
|
||||
"role": value.role,
|
||||
"location": _location_value(value.location),
|
||||
"fields": tuple(_field_value(field) for field in value.fields),
|
||||
"status": value.status.value,
|
||||
"provenance": _semantic_provenance_value(value.provenance),
|
||||
}
|
||||
|
||||
|
||||
def _scope_value(value: AnalyzedScope) -> canonical.JsonValue:
|
||||
return {
|
||||
"base_revision": value.base_revision,
|
||||
"head_revision": value.head_revision,
|
||||
"change_digest": value.change_digest,
|
||||
"regions": tuple({"path": region.path, "start_line": region.start_line, "end_line": region.end_line} for region in value.regions),
|
||||
}
|
||||
|
||||
|
||||
def _stamp_value(value: ProvenanceStamp) -> canonical.JsonValue:
|
||||
return {
|
||||
"analyzer_revision": value.analyzer_revision,
|
||||
"family_contract_digest": value.family_contract_digest,
|
||||
"fact_set_digest": value.fact_set_digest,
|
||||
"acquisition_record_digest": value.acquisition_record_digest,
|
||||
"configuration_digest": value.configuration_digest,
|
||||
"population_digest": value.population_digest,
|
||||
"change_input_digest": value.change_input_digest,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EvidencePacket:
|
||||
schema_version: str
|
||||
family_id: str
|
||||
family_contract_digest: str
|
||||
security_object_id: str
|
||||
ecosystem: str
|
||||
scope: AnalyzedScope
|
||||
transitions: tuple[SemanticRelation, ...]
|
||||
observations: tuple[Observation, ...]
|
||||
family_fields: tuple[FieldValue, ...]
|
||||
completeness_reasons: tuple[str, ...]
|
||||
provenance: ProvenanceStamp
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_packet(self)
|
||||
|
||||
|
||||
def _validate_packet(packet: EvidencePacket) -> None:
|
||||
if packet.schema_version != EVIDENCE_PACKET_SCHEMA:
|
||||
raise EvidenceValidationError(f"unknown evidence packet schema: {packet.schema_version!r}")
|
||||
for value, field in (
|
||||
(packet.family_id, "family_id"),
|
||||
(packet.security_object_id, "security_object_id"),
|
||||
(packet.ecosystem, "ecosystem"),
|
||||
):
|
||||
_require_text(value, field)
|
||||
_require_digest(packet.family_contract_digest, "family_contract_digest")
|
||||
if not isinstance(packet.scope, AnalyzedScope):
|
||||
raise EvidenceValidationError("scope must be an AnalyzedScope")
|
||||
_validate_packet_members(packet)
|
||||
_validate_completeness(packet)
|
||||
if not isinstance(packet.provenance, ProvenanceStamp):
|
||||
raise EvidenceValidationError("provenance must be a ProvenanceStamp")
|
||||
if packet.provenance.family_contract_digest != packet.family_contract_digest:
|
||||
raise EvidenceValidationError("packet family contract digest does not match provenance")
|
||||
|
||||
|
||||
def _validate_packet_members(packet: EvidencePacket) -> None:
|
||||
members = (
|
||||
(packet.transitions, SemanticRelation, "relation", "relation_id"),
|
||||
(packet.observations, Observation, "observation", "observation_id"),
|
||||
(packet.family_fields, FieldValue, "family field", "name"),
|
||||
)
|
||||
for values, expected_type, kind, identity_field in members:
|
||||
if not isinstance(values, tuple) or not all(isinstance(value, expected_type) for value in values):
|
||||
raise EvidenceValidationError(f"{kind} values must be a tuple of {expected_type.__name__}")
|
||||
try:
|
||||
canonical.require_unique_identities(tuple(getattr(value, identity_field) for value in values), kind=kind)
|
||||
except canonical.DuplicateIdentityError as exc:
|
||||
raise EvidenceValidationError(str(exc)) from exc
|
||||
|
||||
|
||||
def _validate_completeness(packet: EvidencePacket) -> None:
|
||||
_require_text_tuple(packet.completeness_reasons, "completeness reason")
|
||||
relations_incomplete = any(item.status is not RelationStatus.COMPLETE for item in packet.transitions)
|
||||
observations_incomplete = any(item.status is not RelationStatus.COMPLETE for item in packet.observations)
|
||||
if (relations_incomplete or observations_incomplete) and not packet.completeness_reasons:
|
||||
raise EvidenceValidationError("non-complete semantics require named completeness reasons")
|
||||
relation_reasons = {reason for relation in packet.transitions for reason in relation.reasons}
|
||||
if not relation_reasons.issubset(packet.completeness_reasons):
|
||||
raise EvidenceValidationError("every non-complete relation reason must be named in packet completeness")
|
||||
|
||||
|
||||
def _packet_value(packet: EvidencePacket) -> canonical.JsonValue:
|
||||
return {
|
||||
"schema_version": packet.schema_version,
|
||||
"family_id": packet.family_id,
|
||||
"family_contract_digest": packet.family_contract_digest,
|
||||
"security_object_id": packet.security_object_id,
|
||||
"ecosystem": packet.ecosystem,
|
||||
"scope": _scope_value(packet.scope),
|
||||
"transitions": tuple(_relation_value(item) for item in packet.transitions),
|
||||
"observations": tuple(_observation_value(item) for item in packet.observations),
|
||||
"family_fields": tuple(_field_value(item) for item in packet.family_fields),
|
||||
"completeness_reasons": packet.completeness_reasons,
|
||||
"provenance": _stamp_value(packet.provenance),
|
||||
}
|
||||
|
||||
|
||||
def derive_packet_digest(packet: EvidencePacket) -> str:
|
||||
if not isinstance(packet, EvidencePacket):
|
||||
raise EvidenceValidationError("packet must be an EvidencePacket")
|
||||
return canonical.content_digest(_packet_value(packet))
|
||||
|
||||
|
||||
def _model_identity_value(value: ModelCallIdentity) -> canonical.JsonValue:
|
||||
return {
|
||||
"provider": value.provider,
|
||||
"model": value.model,
|
||||
"program_digest": value.program_digest,
|
||||
"prompt_digest": value.prompt_digest,
|
||||
"configuration_digest": value.configuration_digest,
|
||||
"adapter_revision": value.adapter_revision,
|
||||
}
|
||||
|
||||
|
||||
def _decision_provenance_value(value: DecisionProvenance) -> canonical.JsonValue:
|
||||
model: canonical.JsonValue = None if value.model_identity is None else _model_identity_value(value.model_identity)
|
||||
return {
|
||||
"model_identity": model,
|
||||
"model_result_digest": value.model_result_digest,
|
||||
"model_declaration_digest": value.model_declaration_digest,
|
||||
"proof_record_digest": value.proof_record_digest,
|
||||
"proof_producer_digest": value.proof_producer_digest,
|
||||
"lineage_manifest_digest": value.lineage_manifest_digest,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Decision:
|
||||
family_code: str
|
||||
outcome: NeutralOutcome
|
||||
basis: EvidenceBasis
|
||||
bounded_scope: AnalyzedScope
|
||||
reasons: tuple[str, ...]
|
||||
packet_digest: str
|
||||
provenance: DecisionProvenance
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_require_text(self.family_code, "family_code")
|
||||
if not isinstance(self.outcome, NeutralOutcome):
|
||||
raise EvidenceValidationError("outcome must be a NeutralOutcome")
|
||||
if not isinstance(self.basis, EvidenceBasis):
|
||||
raise EvidenceValidationError("basis must be an EvidenceBasis")
|
||||
if not isinstance(self.bounded_scope, AnalyzedScope):
|
||||
raise EvidenceValidationError("bounded_scope must be an AnalyzedScope")
|
||||
_require_text_tuple(self.reasons, "decision reason")
|
||||
_require_digest(self.packet_digest, "packet_digest")
|
||||
if not isinstance(self.provenance, DecisionProvenance):
|
||||
raise EvidenceValidationError("provenance must be DecisionProvenance")
|
||||
_validate_basis(self.basis, self.provenance)
|
||||
|
||||
|
||||
def _validate_basis(basis: EvidenceBasis, value: DecisionProvenance) -> None:
|
||||
model = (value.model_identity, value.model_result_digest, value.model_declaration_digest, value.lineage_manifest_digest)
|
||||
proof = (value.proof_record_digest, value.proof_producer_digest)
|
||||
if basis is EvidenceBasis.DETERMINISTIC and any(item is not None for item in (*model, *proof)):
|
||||
raise EvidenceValidationError("deterministic basis forbids model, proof, and lineage provenance")
|
||||
if basis is EvidenceBasis.MODEL_CORROBORATED and (any(item is None for item in model) or any(item is not None for item in proof)):
|
||||
raise EvidenceValidationError("model-corroborated basis requires complete model provenance and forbids proof provenance")
|
||||
if basis is EvidenceBasis.EXECUTION_BACKED and (any(item is None for item in proof) or any(item is not None for item in model)):
|
||||
raise EvidenceValidationError("execution-backed basis requires complete proof provenance and forbids model provenance")
|
||||
|
||||
|
||||
def _decision_value(decision: Decision) -> canonical.JsonValue:
|
||||
return {
|
||||
"family_code": decision.family_code,
|
||||
"outcome": decision.outcome.value,
|
||||
"basis": decision.basis.value,
|
||||
"bounded_scope": _scope_value(decision.bounded_scope),
|
||||
"reasons": decision.reasons,
|
||||
"packet_digest": decision.packet_digest,
|
||||
"provenance": _decision_provenance_value(decision.provenance),
|
||||
}
|
||||
|
||||
|
||||
def _scope_within(bounded: AnalyzedScope, analyzed: AnalyzedScope) -> bool:
|
||||
if (bounded.base_revision, bounded.head_revision, bounded.change_digest) != (
|
||||
analyzed.base_revision,
|
||||
analyzed.head_revision,
|
||||
analyzed.change_digest,
|
||||
):
|
||||
return False
|
||||
return all(region in analyzed.regions for region in bounded.regions)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EvidenceRecord:
|
||||
packet: EvidencePacket
|
||||
decision: Decision
|
||||
record_id: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not isinstance(self.packet, EvidencePacket) or not isinstance(self.decision, Decision):
|
||||
raise EvidenceValidationError("record requires an EvidencePacket and Decision")
|
||||
_validate_record_pair(self.packet, self.decision)
|
||||
expected = derive_record_id(self.packet, self.decision)
|
||||
if not self.record_id:
|
||||
object.__setattr__(self, "record_id", expected)
|
||||
elif self.record_id != expected:
|
||||
raise EvidenceValidationError("record_id does not match canonical record fields")
|
||||
|
||||
|
||||
def _validate_record_pair(packet: EvidencePacket, decision: Decision) -> None:
|
||||
if decision.packet_digest != derive_packet_digest(packet):
|
||||
raise EvidenceValidationError("decision packet digest does not match packet")
|
||||
if not _scope_within(decision.bounded_scope, packet.scope):
|
||||
raise EvidenceValidationError("decision bounded scope is outside analyzed packet scope")
|
||||
noncomplete = any(item.status is not RelationStatus.COMPLETE for item in packet.transitions) or any(
|
||||
item.status is not RelationStatus.COMPLETE for item in packet.observations
|
||||
)
|
||||
if decision.outcome is not NeutralOutcome.INDETERMINATE and (packet.completeness_reasons or noncomplete):
|
||||
raise EvidenceValidationError("non-complete semantics or completeness reasons forbid a determinate outcome")
|
||||
if decision.outcome is NeutralOutcome.INDETERMINATE:
|
||||
if not decision.reasons:
|
||||
raise EvidenceValidationError("indeterminate outcome requires a named reason")
|
||||
if not set(packet.completeness_reasons).issubset(decision.reasons):
|
||||
raise EvidenceValidationError("indeterminate reasons must include every packet completeness reason")
|
||||
elif decision.reasons:
|
||||
raise EvidenceValidationError("determinate outcomes must not carry indeterminate reasons")
|
||||
|
||||
|
||||
def _record_value(packet: EvidencePacket, decision: Decision) -> canonical.JsonValue:
|
||||
return {
|
||||
"schema_version": EVIDENCE_RECORD_SCHEMA,
|
||||
"packet": _packet_value(packet),
|
||||
"decision": _decision_value(decision),
|
||||
}
|
||||
|
||||
|
||||
def derive_record_id(packet: EvidencePacket, decision: Decision) -> str:
|
||||
return canonical.content_digest(_record_value(packet, decision))
|
||||
|
||||
|
||||
def evidence_record_bytes(record: EvidenceRecord) -> bytes:
|
||||
if not isinstance(record, EvidenceRecord):
|
||||
raise EvidenceValidationError("record must be an EvidenceRecord")
|
||||
return canonical.canonical_bytes(_record_value(record.packet, record.decision))
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AnalyzedScope",
|
||||
"Decision",
|
||||
"EVIDENCE_PACKET_SCHEMA",
|
||||
"EVIDENCE_RECORD_SCHEMA",
|
||||
"EvidenceBasis",
|
||||
"EvidencePacket",
|
||||
"EvidenceRecord",
|
||||
"EvidenceValidationError",
|
||||
"NeutralOutcome",
|
||||
"SourceRegion",
|
||||
"derive_packet_digest",
|
||||
"derive_record_id",
|
||||
"evidence_record_bytes",
|
||||
]
|
||||
@@ -0,0 +1,349 @@
|
||||
"""Immutable evidence packet and neutral decision contracts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import fields, replace
|
||||
from enum import Enum
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
import openultrasast.core.canonical as canonical
|
||||
import openultrasast.core.evidence as evidence
|
||||
import openultrasast.core.provenance as provenance
|
||||
import openultrasast.core.semantics as semantics
|
||||
|
||||
|
||||
def _digest(value: str) -> str:
|
||||
return canonical.content_digest(value)
|
||||
|
||||
|
||||
def _location(line: int = 7) -> semantics.SourceLocation:
|
||||
return semantics.SourceLocation("src/app.py", line, 2, "handle")
|
||||
|
||||
|
||||
def _product_provenance() -> semantics.EvidenceProvenance:
|
||||
return semantics.EvidenceProvenance(
|
||||
semantics.ProvenanceKind.PRODUCT_CODE,
|
||||
_digest("source"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def _relation(status: semantics.RelationStatus = semantics.RelationStatus.COMPLETE) -> semantics.SemanticRelation:
|
||||
source = semantics.SemanticNode("request", "request", _location())
|
||||
target = semantics.SemanticNode("resource", "resource", _location(8))
|
||||
reasons = () if status is semantics.RelationStatus.COMPLETE else (f"relation:{status.value}",)
|
||||
return semantics.make_relation(
|
||||
kind=semantics.RelationKind.CONSTRAINT_EFFECT,
|
||||
role="authorization_constraint",
|
||||
source=source,
|
||||
target=target,
|
||||
location=_location(9),
|
||||
status=status,
|
||||
conditions=("authenticated",),
|
||||
reasons=reasons,
|
||||
provenance=_product_provenance(),
|
||||
)
|
||||
|
||||
|
||||
def _observation(status: semantics.RelationStatus = semantics.RelationStatus.COMPLETE) -> semantics.Observation:
|
||||
return semantics.make_observation(
|
||||
role="security_object",
|
||||
location=_location(10),
|
||||
fields=(semantics.FieldValue("kind", semantics.FieldKind.TEXT, "resource"),),
|
||||
status=status,
|
||||
provenance=_product_provenance(),
|
||||
)
|
||||
|
||||
|
||||
def _scope(*regions: evidence.SourceRegion) -> evidence.AnalyzedScope:
|
||||
selected = regions or (evidence.SourceRegion("src/app.py", 1, 20),)
|
||||
return evidence.AnalyzedScope("base", "head", _digest("change"), selected)
|
||||
|
||||
|
||||
def _stamp() -> provenance.ProvenanceStamp:
|
||||
return provenance.ProvenanceStamp(
|
||||
*(
|
||||
_digest(name)
|
||||
for name in (
|
||||
"analyzer",
|
||||
"family-contract",
|
||||
"facts",
|
||||
"acquisition",
|
||||
"configuration",
|
||||
"population",
|
||||
"change-input",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _packet(
|
||||
*,
|
||||
status: semantics.RelationStatus = semantics.RelationStatus.COMPLETE,
|
||||
completeness: tuple[str, ...] | None = None,
|
||||
transitions: tuple[semantics.SemanticRelation, ...] | None = None,
|
||||
) -> evidence.EvidencePacket:
|
||||
relation = _relation(status)
|
||||
reasons = () if status is semantics.RelationStatus.COMPLETE else relation.reasons
|
||||
return evidence.EvidencePacket(
|
||||
schema_version=evidence.EVIDENCE_PACKET_SCHEMA,
|
||||
family_id="access-control",
|
||||
family_contract_digest=_digest("family-contract"),
|
||||
security_object_id="resource:42",
|
||||
ecosystem="python",
|
||||
scope=_scope(),
|
||||
transitions=(relation,) if transitions is None else transitions,
|
||||
observations=(_observation(),),
|
||||
family_fields=(semantics.FieldValue("operation", semantics.FieldKind.TEXT, "read"),),
|
||||
completeness_reasons=reasons if completeness is None else completeness,
|
||||
provenance=_stamp(),
|
||||
)
|
||||
|
||||
|
||||
def _empty_decision_provenance() -> provenance.DecisionProvenance:
|
||||
return provenance.DecisionProvenance(None, None, None, None, None, None)
|
||||
|
||||
|
||||
def _decision(
|
||||
packet: evidence.EvidencePacket,
|
||||
*,
|
||||
outcome: evidence.NeutralOutcome = evidence.NeutralOutcome.VIOLATION,
|
||||
basis: evidence.EvidenceBasis = evidence.EvidenceBasis.DETERMINISTIC,
|
||||
reasons: tuple[str, ...] = (),
|
||||
bounded_scope: evidence.AnalyzedScope | None = None,
|
||||
decision_provenance: provenance.DecisionProvenance | None = None,
|
||||
) -> evidence.Decision:
|
||||
return evidence.Decision(
|
||||
family_code="unauthorized_access",
|
||||
outcome=outcome,
|
||||
basis=basis,
|
||||
bounded_scope=packet.scope if bounded_scope is None else bounded_scope,
|
||||
reasons=reasons,
|
||||
packet_digest=evidence.derive_packet_digest(packet),
|
||||
provenance=_empty_decision_provenance() if decision_provenance is None else decision_provenance,
|
||||
)
|
||||
|
||||
|
||||
def _record(
|
||||
*,
|
||||
packet: evidence.EvidencePacket | None = None,
|
||||
outcome: evidence.NeutralOutcome = evidence.NeutralOutcome.VIOLATION,
|
||||
basis: evidence.EvidenceBasis = evidence.EvidenceBasis.DETERMINISTIC,
|
||||
reasons: tuple[str, ...] = (),
|
||||
bounded_scope: evidence.AnalyzedScope | None = None,
|
||||
decision_provenance: provenance.DecisionProvenance | None = None,
|
||||
) -> evidence.EvidenceRecord:
|
||||
selected = _packet() if packet is None else packet
|
||||
decision = _decision(
|
||||
selected,
|
||||
outcome=outcome,
|
||||
basis=basis,
|
||||
reasons=reasons,
|
||||
bounded_scope=bounded_scope,
|
||||
decision_provenance=decision_provenance,
|
||||
)
|
||||
return evidence.EvidenceRecord(selected, decision, "")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("outcome", [evidence.NeutralOutcome.VIOLATION, evidence.NeutralOutcome.SATISFIED])
|
||||
def test_determinate_records_round_trip_canonically(outcome: evidence.NeutralOutcome) -> None:
|
||||
record = _record(outcome=outcome)
|
||||
content = evidence.evidence_record_bytes(record)
|
||||
parsed = json.loads(content)
|
||||
|
||||
assert len(content) > 0
|
||||
assert json.dumps(parsed, ensure_ascii=False, allow_nan=False, sort_keys=True, separators=(",", ":")).encode() == content
|
||||
assert record.record_id == canonical.bytes_digest(content)
|
||||
assert parsed["decision"]["family_code"] == "unauthorized_access"
|
||||
assert parsed["decision"]["outcome"] == outcome.value
|
||||
assert parsed["packet"]["family_id"] == "access-control"
|
||||
|
||||
|
||||
def test_incomplete_record_round_trips_as_named_indeterminate() -> None:
|
||||
packet = _packet(status=semantics.RelationStatus.INCOMPLETE)
|
||||
record = _record(
|
||||
packet=packet,
|
||||
outcome=evidence.NeutralOutcome.INDETERMINATE,
|
||||
reasons=packet.completeness_reasons,
|
||||
)
|
||||
parsed = json.loads(evidence.evidence_record_bytes(record))
|
||||
|
||||
assert parsed["decision"]["outcome"] == "indeterminate"
|
||||
assert parsed["decision"]["reasons"] == ["relation:incomplete"]
|
||||
|
||||
|
||||
def test_packet_preserves_ordered_semantics_and_family_fields() -> None:
|
||||
first = _relation()
|
||||
second = replace(first, location=_location(11), relation_id="")
|
||||
forward = _packet(transitions=(first, second))
|
||||
reverse = _packet(transitions=(second, first))
|
||||
|
||||
assert forward.transitions == (first, second)
|
||||
assert evidence.derive_packet_digest(forward) != evidence.derive_packet_digest(reverse)
|
||||
assert forward.family_fields[0].name == "operation"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [semantics.RelationStatus.INCOMPLETE, semantics.RelationStatus.UNRESOLVED])
|
||||
@pytest.mark.parametrize("outcome", [evidence.NeutralOutcome.VIOLATION, evidence.NeutralOutcome.SATISFIED])
|
||||
def test_noncomplete_semantics_cannot_produce_determinate_outcome(
|
||||
status: semantics.RelationStatus,
|
||||
outcome: evidence.NeutralOutcome,
|
||||
) -> None:
|
||||
packet = _packet(status=status)
|
||||
with pytest.raises(evidence.EvidenceValidationError, match="non-complete|completeness"):
|
||||
_record(packet=packet, outcome=outcome)
|
||||
|
||||
|
||||
def test_packet_requires_every_noncomplete_reason_to_be_named() -> None:
|
||||
with pytest.raises(evidence.EvidenceValidationError, match="completeness"):
|
||||
_packet(status=semantics.RelationStatus.INCOMPLETE, completeness=())
|
||||
|
||||
|
||||
def test_noncomplete_observation_also_forces_indeterminate_outcome() -> None:
|
||||
packet = replace(
|
||||
_packet(),
|
||||
observations=(_observation(semantics.RelationStatus.UNRESOLVED),),
|
||||
completeness_reasons=("observation:unresolved",),
|
||||
)
|
||||
with pytest.raises(evidence.EvidenceValidationError, match="non-complete"):
|
||||
_record(packet=packet, outcome=evidence.NeutralOutcome.VIOLATION)
|
||||
|
||||
|
||||
def test_indeterminate_requires_reasons_and_carries_every_packet_reason() -> None:
|
||||
packet = _packet(status=semantics.RelationStatus.INCOMPLETE)
|
||||
with pytest.raises(evidence.EvidenceValidationError, match="reason"):
|
||||
_record(packet=packet, outcome=evidence.NeutralOutcome.INDETERMINATE)
|
||||
with pytest.raises(evidence.EvidenceValidationError, match="completeness"):
|
||||
_record(packet=packet, outcome=evidence.NeutralOutcome.INDETERMINATE, reasons=("different",))
|
||||
|
||||
|
||||
def test_satisfaction_requires_nonempty_bounded_scope_within_packet_scope() -> None:
|
||||
packet = _packet()
|
||||
with pytest.raises(evidence.EvidenceValidationError, match="bounded scope"):
|
||||
_record(
|
||||
packet=packet,
|
||||
outcome=evidence.NeutralOutcome.SATISFIED,
|
||||
bounded_scope=_scope(evidence.SourceRegion("src/other.py", 1, 2)),
|
||||
)
|
||||
outside = evidence.AnalyzedScope("base", "head", _digest("other-change"), packet.scope.regions)
|
||||
with pytest.raises(evidence.EvidenceValidationError, match="bounded scope"):
|
||||
_record(packet=packet, outcome=evidence.NeutralOutcome.SATISFIED, bounded_scope=outside)
|
||||
|
||||
|
||||
def test_satisfaction_accepts_an_exact_nonempty_subset_scope() -> None:
|
||||
packet = replace(
|
||||
_packet(),
|
||||
scope=_scope(evidence.SourceRegion("src/app.py", 1, 10), evidence.SourceRegion("src/app.py", 20, 30)),
|
||||
)
|
||||
bounded = _scope(evidence.SourceRegion("src/app.py", 1, 10))
|
||||
record = _record(packet=packet, outcome=evidence.NeutralOutcome.SATISFIED, bounded_scope=bounded)
|
||||
assert record.decision.bounded_scope == bounded
|
||||
|
||||
|
||||
def _model_provenance() -> provenance.DecisionProvenance:
|
||||
identity = provenance.ModelCallIdentity(
|
||||
provider="provider",
|
||||
model="model",
|
||||
program_digest=_digest("program"),
|
||||
prompt_digest=_digest("prompt"),
|
||||
configuration_digest=_digest("model-configuration"),
|
||||
adapter_revision="adapter-v1",
|
||||
)
|
||||
return provenance.DecisionProvenance(
|
||||
identity,
|
||||
_digest("model-result"),
|
||||
_digest("model-declaration"),
|
||||
None,
|
||||
None,
|
||||
_digest("lineage"),
|
||||
)
|
||||
|
||||
|
||||
def _proof_provenance() -> provenance.DecisionProvenance:
|
||||
return provenance.DecisionProvenance(None, None, None, _digest("proof"), _digest("producer"), None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("basis", "decision_provenance"),
|
||||
[
|
||||
(evidence.EvidenceBasis.DETERMINISTIC, _empty_decision_provenance()),
|
||||
(evidence.EvidenceBasis.MODEL_CORROBORATED, _model_provenance()),
|
||||
(evidence.EvidenceBasis.EXECUTION_BACKED, _proof_provenance()),
|
||||
],
|
||||
)
|
||||
def test_every_closed_evidence_basis_accepts_only_its_complete_provenance(
|
||||
basis: evidence.EvidenceBasis,
|
||||
decision_provenance: provenance.DecisionProvenance,
|
||||
) -> None:
|
||||
record = _record(basis=basis, decision_provenance=decision_provenance)
|
||||
assert record.decision.basis is basis
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("basis", "decision_provenance"),
|
||||
[
|
||||
(evidence.EvidenceBasis.DETERMINISTIC, _model_provenance()),
|
||||
(evidence.EvidenceBasis.DETERMINISTIC, _proof_provenance()),
|
||||
(evidence.EvidenceBasis.MODEL_CORROBORATED, _empty_decision_provenance()),
|
||||
(evidence.EvidenceBasis.MODEL_CORROBORATED, replace(_model_provenance(), model_result_digest=None)),
|
||||
(evidence.EvidenceBasis.EXECUTION_BACKED, _empty_decision_provenance()),
|
||||
(evidence.EvidenceBasis.EXECUTION_BACKED, replace(_proof_provenance(), proof_producer_digest=None)),
|
||||
(evidence.EvidenceBasis.EXECUTION_BACKED, _model_provenance()),
|
||||
],
|
||||
)
|
||||
def test_basis_provenance_is_a_closed_union(
|
||||
basis: evidence.EvidenceBasis,
|
||||
decision_provenance: provenance.DecisionProvenance,
|
||||
) -> None:
|
||||
with pytest.raises(evidence.EvidenceValidationError, match="basis|provenance"):
|
||||
_record(basis=basis, decision_provenance=decision_provenance)
|
||||
|
||||
|
||||
def test_packet_has_no_model_or_proof_fields() -> None:
|
||||
names = {item.name for item in fields(evidence.EvidencePacket)}
|
||||
assert not any("model" in name or "proof" in name for name in names)
|
||||
assert {"model_identity", "proof_record_digest"} <= {item.name for item in fields(provenance.DecisionProvenance)}
|
||||
|
||||
|
||||
def test_decision_is_bound_to_exact_packet_digest_and_record_identity() -> None:
|
||||
record = _record()
|
||||
with pytest.raises(evidence.EvidenceValidationError, match="packet digest"):
|
||||
replace(record, decision=replace(record.decision, packet_digest=_digest("other")), record_id="")
|
||||
with pytest.raises(evidence.EvidenceValidationError, match="record_id"):
|
||||
replace(record, record_id="0" * 64)
|
||||
|
||||
|
||||
def test_packet_rejects_duplicate_logical_identities() -> None:
|
||||
relation = _relation()
|
||||
with pytest.raises(evidence.EvidenceValidationError, match="duplicate"):
|
||||
_packet(transitions=(relation, relation))
|
||||
packet = _packet()
|
||||
with pytest.raises(evidence.EvidenceValidationError, match="duplicate"):
|
||||
replace(packet, family_fields=(packet.family_fields[0], packet.family_fields[0]))
|
||||
|
||||
|
||||
def test_scope_and_text_contracts_are_runtime_checked() -> None:
|
||||
with pytest.raises(evidence.EvidenceValidationError, match="line"):
|
||||
evidence.SourceRegion("src/app.py", 9, 8)
|
||||
with pytest.raises(evidence.EvidenceValidationError, match="region"):
|
||||
evidence.AnalyzedScope("base", "head", _digest("change"), ())
|
||||
packet = _packet()
|
||||
with pytest.raises(evidence.EvidenceValidationError, match="family_id"):
|
||||
replace(packet, family_id=cast(str, 7))
|
||||
|
||||
|
||||
def test_unknown_runtime_enums_and_schema_are_refused() -> None:
|
||||
packet = _packet()
|
||||
with pytest.raises(evidence.EvidenceValidationError, match="schema"):
|
||||
replace(packet, schema_version="unknown")
|
||||
|
||||
class ForeignOutcome(Enum):
|
||||
VIOLATION = "violation"
|
||||
|
||||
with pytest.raises(evidence.EvidenceValidationError, match="NeutralOutcome"):
|
||||
replace(_decision(packet), outcome=cast(evidence.NeutralOutcome, ForeignOutcome.VIOLATION))
|
||||
Reference in New Issue
Block a user