mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 09:48:18 +00:00
350 lines
14 KiB
Python
350 lines
14 KiB
Python
"""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))
|