mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 09:48:18 +00:00
feat(detection-core): validate closed family declarations
This commit is contained in:
@@ -0,0 +1,468 @@
|
||||
"""Closed vulnerability-family declarations and record validation."""
|
||||
|
||||
from dataclasses import replace
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
import openultrasast.core.canonical as canonical
|
||||
import openultrasast.core.evidence as evidence
|
||||
import openultrasast.core.family as family
|
||||
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 = 10) -> semantics.SourceLocation:
|
||||
return semantics.SourceLocation("src/app.py", line, 2, "handle")
|
||||
|
||||
|
||||
def _product() -> semantics.EvidenceProvenance:
|
||||
return semantics.EvidenceProvenance(semantics.ProvenanceKind.PRODUCT_CODE, _digest("source"), None, None, None)
|
||||
|
||||
|
||||
def _fact() -> semantics.EvidenceProvenance:
|
||||
content = b"reviewed match"
|
||||
retention = _digest("retention")
|
||||
content_digest = canonical.bytes_digest(content)
|
||||
receipt_digest = canonical.derive_storage_receipt_digest(
|
||||
store_identity="reviewed-store",
|
||||
content_digest=content_digest,
|
||||
byte_length=len(content),
|
||||
retention_policy_digest=retention,
|
||||
)
|
||||
reference = canonical.ContentRef(
|
||||
"match",
|
||||
content_digest,
|
||||
"application/json",
|
||||
len(content),
|
||||
retention,
|
||||
receipt_digest,
|
||||
)
|
||||
return semantics.EvidenceProvenance(
|
||||
semantics.ProvenanceKind.SEMANTIC_FACT,
|
||||
_digest("source"),
|
||||
_digest("pack"),
|
||||
"fact.constraint",
|
||||
reference,
|
||||
)
|
||||
|
||||
|
||||
def _relation(
|
||||
*,
|
||||
status: semantics.RelationStatus = semantics.RelationStatus.COMPLETE,
|
||||
origin: semantics.EvidenceProvenance | None = None,
|
||||
role: str = "constraint",
|
||||
) -> semantics.SemanticRelation:
|
||||
return semantics.make_relation(
|
||||
kind=semantics.RelationKind.CONSTRAINT_EFFECT,
|
||||
role=role,
|
||||
source=semantics.SemanticNode("operation", "operation", _location()),
|
||||
target=semantics.SemanticNode("policy", "policy", _location(11)),
|
||||
location=_location(12),
|
||||
status=status,
|
||||
conditions=("policy_applies",),
|
||||
reasons=() if status is semantics.RelationStatus.COMPLETE else ("missing_reviewed_dependency_relation",),
|
||||
provenance=_product() if origin is None else origin,
|
||||
)
|
||||
|
||||
|
||||
def _observation(*, field_name: str = "resource", kind: semantics.FieldKind = semantics.FieldKind.TEXT) -> semantics.Observation:
|
||||
value: canonical.JsonValue = "document" if kind is semantics.FieldKind.TEXT else 7
|
||||
return semantics.make_observation(
|
||||
role="security_object",
|
||||
location=_location(13),
|
||||
fields=(semantics.FieldValue(field_name, kind, value),),
|
||||
status=semantics.RelationStatus.COMPLETE,
|
||||
provenance=_product(),
|
||||
)
|
||||
|
||||
|
||||
def _contract(*, fact_only: bool = False) -> family.FamilyContract:
|
||||
origin = (
|
||||
family.RelationOriginPolicy.EXCLUDED_DEPENDENCY_FACT_ONLY if fact_only else family.RelationOriginPolicy.PRODUCT_OR_REVIEWED_FACT
|
||||
)
|
||||
return family.FamilyContract(
|
||||
family_id="obligation",
|
||||
family_version="1.0.0",
|
||||
schema_version=evidence.EVIDENCE_PACKET_SCHEMA,
|
||||
ecosystems=("python",),
|
||||
security_object=family.ObjectSpec(
|
||||
kind="operation",
|
||||
fields=(family.FieldSpec("operation", semantics.FieldKind.TEXT, True),),
|
||||
identity_fields=("operation",),
|
||||
),
|
||||
roles=(
|
||||
family.RoleSpec(
|
||||
"constraint",
|
||||
(),
|
||||
(semantics.RelationKind.CONSTRAINT_EFFECT,),
|
||||
origin,
|
||||
),
|
||||
family.RoleSpec(
|
||||
"security_object",
|
||||
(family.FieldSpec("resource", semantics.FieldKind.TEXT, True),),
|
||||
(),
|
||||
family.RelationOriginPolicy.PRODUCT_OR_REVIEWED_FACT,
|
||||
),
|
||||
),
|
||||
decisions=(
|
||||
family.DecisionRule(
|
||||
"blocked",
|
||||
evidence.NeutralOutcome.VIOLATION,
|
||||
("constraint", "security_object"),
|
||||
(semantics.RelationStatus.COMPLETE,),
|
||||
),
|
||||
family.DecisionRule(
|
||||
"safe",
|
||||
evidence.NeutralOutcome.SATISFIED,
|
||||
("constraint", "security_object"),
|
||||
(semantics.RelationStatus.COMPLETE,),
|
||||
),
|
||||
family.DecisionRule(
|
||||
"unknown",
|
||||
evidence.NeutralOutcome.INDETERMINATE,
|
||||
("constraint",),
|
||||
(semantics.RelationStatus.INCOMPLETE, semantics.RelationStatus.UNRESOLVED),
|
||||
),
|
||||
),
|
||||
residual_questions=(family.ResidualQuestion("resolve_constraint", ("constraint",), ("blocked", "safe", "unknown")),),
|
||||
indeterminate_reasons=(
|
||||
"missing_reviewed_dependency_relation",
|
||||
"missing_required_role",
|
||||
"incomplete_required_role",
|
||||
"unsupported_scope",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _stamp(contract: family.FamilyContract) -> provenance.ProvenanceStamp:
|
||||
return provenance.ProvenanceStamp(
|
||||
_digest("analyzer"),
|
||||
family.family_contract_digest(contract),
|
||||
_digest("facts"),
|
||||
_digest("acquisition"),
|
||||
_digest("configuration"),
|
||||
_digest("population"),
|
||||
_digest("change"),
|
||||
)
|
||||
|
||||
|
||||
def _packet(
|
||||
contract: family.FamilyContract,
|
||||
*,
|
||||
relation: semantics.SemanticRelation | None = None,
|
||||
observation: semantics.Observation | None = None,
|
||||
family_fields: tuple[semantics.FieldValue, ...] | None = None,
|
||||
ecosystem: str = "python",
|
||||
) -> evidence.EvidencePacket:
|
||||
selected = _relation() if relation is None else relation
|
||||
reasons = selected.reasons if selected.status is not semantics.RelationStatus.COMPLETE else ()
|
||||
selected_fields = (semantics.FieldValue("operation", semantics.FieldKind.TEXT, "read"),) if family_fields is None else family_fields
|
||||
scope = evidence.AnalyzedScope(
|
||||
"base",
|
||||
"head",
|
||||
_digest("change"),
|
||||
(evidence.SourceRegion("src/app.py", 1, 30),),
|
||||
)
|
||||
return evidence.EvidencePacket(
|
||||
evidence.EVIDENCE_PACKET_SCHEMA,
|
||||
contract.family_id,
|
||||
family.family_contract_digest(contract),
|
||||
family.derive_security_object_id(contract.security_object, selected_fields),
|
||||
ecosystem,
|
||||
scope,
|
||||
(selected,),
|
||||
(_observation() if observation is None else observation,),
|
||||
selected_fields,
|
||||
reasons,
|
||||
_stamp(contract),
|
||||
)
|
||||
|
||||
|
||||
def _record(
|
||||
contract: family.FamilyContract,
|
||||
*,
|
||||
relation: semantics.SemanticRelation | None = None,
|
||||
observation: semantics.Observation | None = None,
|
||||
family_fields: tuple[semantics.FieldValue, ...] | None = None,
|
||||
ecosystem: str = "python",
|
||||
code: str = "blocked",
|
||||
) -> evidence.EvidenceRecord:
|
||||
packet = _packet(
|
||||
contract,
|
||||
relation=relation,
|
||||
observation=observation,
|
||||
family_fields=family_fields,
|
||||
ecosystem=ecosystem,
|
||||
)
|
||||
indeterminate = relation is not None and relation.status is not semantics.RelationStatus.COMPLETE
|
||||
outcome = evidence.NeutralOutcome.INDETERMINATE if indeterminate else evidence.NeutralOutcome.VIOLATION
|
||||
decision = evidence.Decision(
|
||||
code,
|
||||
outcome,
|
||||
evidence.EvidenceBasis.DETERMINISTIC,
|
||||
packet.scope,
|
||||
packet.completeness_reasons,
|
||||
evidence.derive_packet_digest(packet),
|
||||
provenance.DecisionProvenance(None, None, None, None, None, None),
|
||||
)
|
||||
return evidence.EvidenceRecord(packet, decision, "")
|
||||
|
||||
|
||||
def test_closed_family_contract_and_record_are_accepted() -> None:
|
||||
contract = _contract()
|
||||
family.validate_contract(contract)
|
||||
family.validate_record(contract, _record(contract))
|
||||
assert len(family.family_contract_digest(contract)) == 64
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field",
|
||||
[
|
||||
"family_id",
|
||||
"family_version",
|
||||
"schema_version",
|
||||
"ecosystems",
|
||||
"roles",
|
||||
"decisions",
|
||||
"residual_questions",
|
||||
"indeterminate_reasons",
|
||||
],
|
||||
)
|
||||
def test_contract_requires_every_declared_section(field: str) -> None:
|
||||
contract = _contract()
|
||||
with pytest.raises(family.FamilyContractError):
|
||||
if field == "family_id":
|
||||
replace(contract, family_id="")
|
||||
elif field == "family_version":
|
||||
replace(contract, family_version="")
|
||||
elif field == "schema_version":
|
||||
replace(contract, schema_version="")
|
||||
elif field == "ecosystems":
|
||||
replace(contract, ecosystems=())
|
||||
elif field == "roles":
|
||||
replace(contract, roles=())
|
||||
elif field == "decisions":
|
||||
replace(contract, decisions=())
|
||||
elif field == "residual_questions":
|
||||
replace(contract, residual_questions=())
|
||||
else:
|
||||
replace(contract, indeterminate_reasons=())
|
||||
|
||||
|
||||
def test_security_object_identity_fields_must_be_required_and_declared() -> None:
|
||||
contract = _contract()
|
||||
with pytest.raises(family.FamilyContractError, match="identity"):
|
||||
replace(contract, security_object=replace(contract.security_object, identity_fields=("missing",)))
|
||||
optional = family.FieldSpec("operation", semantics.FieldKind.TEXT, False)
|
||||
with pytest.raises(family.FamilyContractError, match="identity"):
|
||||
replace(contract, security_object=replace(contract.security_object, fields=(optional,)))
|
||||
with pytest.raises(family.FamilyContractError, match="identity"):
|
||||
replace(contract, security_object=replace(contract.security_object, identity_fields=()))
|
||||
|
||||
|
||||
def test_role_and_decision_declarations_are_closed_and_unique() -> None:
|
||||
contract = _contract()
|
||||
with pytest.raises(family.FamilyContractError, match="duplicate"):
|
||||
replace(contract, roles=contract.roles + (contract.roles[0],))
|
||||
with pytest.raises(family.FamilyContractError, match="undeclared role"):
|
||||
replace(contract, decisions=(replace(contract.decisions[0], required_roles=("other",)),) + contract.decisions[1:])
|
||||
with pytest.raises(family.FamilyContractError, match="complete"):
|
||||
replace(
|
||||
contract,
|
||||
decisions=(replace(contract.decisions[0], allowed_statuses=(semantics.RelationStatus.INCOMPLETE,)),) + contract.decisions[1:],
|
||||
)
|
||||
with pytest.raises(family.FamilyContractError, match="minimum evidence"):
|
||||
replace(contract, decisions=(replace(contract.decisions[0], required_roles=()),) + contract.decisions[1:])
|
||||
|
||||
|
||||
def test_residual_questions_reference_only_declared_roles_and_decisions() -> None:
|
||||
contract = _contract()
|
||||
with pytest.raises(family.FamilyContractError, match="undeclared role"):
|
||||
replace(contract, residual_questions=(replace(contract.residual_questions[0], required_roles=("other",)),))
|
||||
with pytest.raises(family.FamilyContractError, match="undeclared decision"):
|
||||
replace(contract, residual_questions=(replace(contract.residual_questions[0], allowed_decisions=("other",)),))
|
||||
with pytest.raises(family.FamilyContractError, match="require roles"):
|
||||
replace(contract, residual_questions=(replace(contract.residual_questions[0], required_roles=()),))
|
||||
|
||||
|
||||
def test_declaration_collections_require_immutable_tuples() -> None:
|
||||
contract = _contract()
|
||||
with pytest.raises(family.FamilyContractError, match="ecosystems"):
|
||||
replace(contract, ecosystems=cast(tuple[str, ...], ["python"]))
|
||||
with pytest.raises(family.FamilyContractError, match="indeterminate reasons"):
|
||||
replace(contract, indeterminate_reasons=cast(tuple[str, ...], ["unsupported_scope"]))
|
||||
|
||||
|
||||
def test_record_refuses_unsupported_family_ecosystem_and_schema_without_fallback() -> None:
|
||||
contract = _contract()
|
||||
record = _record(contract)
|
||||
with pytest.raises(family.UnsupportedFamilyScope, match="family"):
|
||||
family.validate_record(replace(contract, family_id="different"), record)
|
||||
with pytest.raises(family.UnsupportedFamilyScope, match="ecosystem"):
|
||||
family.validate_record(contract, _record(contract, ecosystem="javascript"))
|
||||
with pytest.raises(family.UnsupportedFamilyScope, match="schema"):
|
||||
family.validate_record(replace(contract, schema_version="other-schema"), record)
|
||||
|
||||
|
||||
def test_record_refuses_unknown_and_mistyped_family_fields() -> None:
|
||||
contract = _contract()
|
||||
unknown = (semantics.FieldValue("other", semantics.FieldKind.TEXT, "read"),)
|
||||
wrong_type = (semantics.FieldValue("operation", semantics.FieldKind.INTEGER, 7),)
|
||||
with pytest.raises(family.FamilyContractError, match="field"):
|
||||
family.validate_record(contract, _record(contract, family_fields=unknown))
|
||||
with pytest.raises(family.FamilyContractError, match="kind"):
|
||||
family.validate_record(contract, _record(contract, family_fields=wrong_type))
|
||||
with pytest.raises(family.FamilyContractError, match="required"):
|
||||
family.validate_record(contract, _record(contract, family_fields=()))
|
||||
|
||||
|
||||
def _record_with_packet(record: evidence.EvidenceRecord, packet: evidence.EvidencePacket) -> evidence.EvidenceRecord:
|
||||
decision = replace(record.decision, packet_digest=evidence.derive_packet_digest(packet))
|
||||
return evidence.EvidenceRecord(packet, decision, "")
|
||||
|
||||
|
||||
def test_record_refuses_substituted_security_object_identity() -> None:
|
||||
contract = _contract()
|
||||
record = _record(contract)
|
||||
substituted = replace(record.packet, security_object_id=_digest("other-object"))
|
||||
with pytest.raises(family.FamilyContractError, match="security object identity"):
|
||||
family.validate_record(contract, _record_with_packet(record, substituted))
|
||||
|
||||
|
||||
def test_identity_field_change_cannot_reuse_the_old_security_object_identity() -> None:
|
||||
contract = _contract()
|
||||
record = _record(contract)
|
||||
changed = replace(
|
||||
record.packet,
|
||||
family_fields=(semantics.FieldValue("operation", semantics.FieldKind.TEXT, "write"),),
|
||||
)
|
||||
with pytest.raises(family.FamilyContractError, match="security object identity"):
|
||||
family.validate_record(contract, _record_with_packet(record, changed))
|
||||
|
||||
|
||||
def test_nonidentity_field_change_does_not_redirect_security_object_identity() -> None:
|
||||
contract = _contract()
|
||||
display = family.FieldSpec("display_name", semantics.FieldKind.TEXT, False)
|
||||
extended = replace(
|
||||
contract,
|
||||
security_object=replace(contract.security_object, fields=contract.security_object.fields + (display,)),
|
||||
)
|
||||
first = _record(
|
||||
extended,
|
||||
family_fields=(
|
||||
semantics.FieldValue("operation", semantics.FieldKind.TEXT, "read"),
|
||||
semantics.FieldValue("display_name", semantics.FieldKind.TEXT, "first"),
|
||||
),
|
||||
)
|
||||
second = _record(
|
||||
extended,
|
||||
family_fields=(
|
||||
semantics.FieldValue("operation", semantics.FieldKind.TEXT, "read"),
|
||||
semantics.FieldValue("display_name", semantics.FieldKind.TEXT, "second"),
|
||||
),
|
||||
)
|
||||
family.validate_record(extended, first)
|
||||
family.validate_record(extended, second)
|
||||
assert first.packet.security_object_id == second.packet.security_object_id
|
||||
|
||||
|
||||
def test_security_object_identity_uses_declared_field_order_not_input_order() -> None:
|
||||
spec = family.ObjectSpec(
|
||||
"operation",
|
||||
(
|
||||
family.FieldSpec("operation", semantics.FieldKind.TEXT, True),
|
||||
family.FieldSpec("arity", semantics.FieldKind.INTEGER, True),
|
||||
),
|
||||
("operation", "arity"),
|
||||
)
|
||||
values = (
|
||||
semantics.FieldValue("operation", semantics.FieldKind.TEXT, "read"),
|
||||
semantics.FieldValue("arity", semantics.FieldKind.INTEGER, 1),
|
||||
)
|
||||
assert family.derive_security_object_id(spec, values) == family.derive_security_object_id(spec, tuple(reversed(values)))
|
||||
reversed_spec = replace(spec, identity_fields=("arity", "operation"))
|
||||
assert family.derive_security_object_id(spec, values) != family.derive_security_object_id(reversed_spec, values)
|
||||
|
||||
|
||||
def test_record_refuses_unknown_roles_relation_kinds_and_observation_fields() -> None:
|
||||
contract = _contract()
|
||||
with pytest.raises(family.FamilyContractError, match="role"):
|
||||
family.validate_record(contract, _record(contract, relation=_relation(role="other")))
|
||||
wrong_kind = replace(_relation(), kind=semantics.RelationKind.CALL, relation_id="")
|
||||
with pytest.raises(family.FamilyContractError, match="kind"):
|
||||
family.validate_record(contract, _record(contract, relation=wrong_kind))
|
||||
with pytest.raises(family.FamilyContractError, match="field"):
|
||||
family.validate_record(contract, _record(contract, observation=_observation(field_name="other")))
|
||||
|
||||
|
||||
def test_record_refuses_unknown_decision_and_outcome_mapping() -> None:
|
||||
contract = _contract()
|
||||
with pytest.raises(family.FamilyContractError, match="decision"):
|
||||
family.validate_record(contract, _record(contract, code="other"))
|
||||
record = _record(contract)
|
||||
wrong = replace(record, decision=replace(record.decision, outcome=evidence.NeutralOutcome.SATISFIED), record_id="")
|
||||
with pytest.raises(family.FamilyContractError, match="outcome"):
|
||||
family.validate_record(contract, wrong)
|
||||
|
||||
|
||||
def test_indeterminate_reasons_must_be_declared() -> None:
|
||||
contract = _contract()
|
||||
relation = _relation(status=semantics.RelationStatus.INCOMPLETE)
|
||||
record = _record(contract, relation=relation, code="unknown")
|
||||
family.validate_record(contract, record)
|
||||
changed = replace(contract, indeterminate_reasons=("other",))
|
||||
with pytest.raises(family.FamilyContractError, match="reason"):
|
||||
family.validate_record(changed, _record(changed, relation=relation, code="unknown"))
|
||||
|
||||
|
||||
def test_determinate_rule_requires_every_role_and_allowed_status() -> None:
|
||||
contract = _contract()
|
||||
missing = replace(_record(contract).packet, observations=())
|
||||
decision = replace(_record(contract).decision, packet_digest=evidence.derive_packet_digest(missing))
|
||||
record = evidence.EvidenceRecord(missing, decision, "")
|
||||
with pytest.raises(family.FamilyContractError, match="required role"):
|
||||
family.validate_record(contract, record)
|
||||
|
||||
|
||||
def test_excluded_dependency_complete_relation_requires_reviewed_fact() -> None:
|
||||
contract = _contract(fact_only=True)
|
||||
with pytest.raises(family.FamilyContractError, match="reviewed semantic fact"):
|
||||
family.validate_record(contract, _record(contract))
|
||||
family.validate_record(contract, _record(contract, relation=_relation(origin=_fact())))
|
||||
|
||||
|
||||
def test_missing_reviewed_dependency_is_named_incomplete_not_product_complete() -> None:
|
||||
contract = _contract(fact_only=True)
|
||||
relation = _relation(status=semantics.RelationStatus.INCOMPLETE)
|
||||
family.validate_record(contract, _record(contract, relation=relation, code="unknown"))
|
||||
wrong_reason = replace(relation, reasons=("different",), relation_id="")
|
||||
packet = replace(
|
||||
_packet(contract, relation=wrong_reason),
|
||||
completeness_reasons=("different",),
|
||||
)
|
||||
decision = evidence.Decision(
|
||||
"unknown",
|
||||
evidence.NeutralOutcome.INDETERMINATE,
|
||||
evidence.EvidenceBasis.DETERMINISTIC,
|
||||
packet.scope,
|
||||
packet.completeness_reasons,
|
||||
evidence.derive_packet_digest(packet),
|
||||
provenance.DecisionProvenance(None, None, None, None, None, None),
|
||||
)
|
||||
with pytest.raises(family.FamilyContractError, match="missing_reviewed_dependency_relation"):
|
||||
family.validate_record(contract, evidence.EvidenceRecord(packet, decision, ""))
|
||||
|
||||
|
||||
def test_contract_digest_changes_with_every_declaration_surface() -> None:
|
||||
contract = _contract()
|
||||
variants = (
|
||||
replace(contract, family_version="1.0.1"),
|
||||
replace(contract, ecosystems=("python", "java")),
|
||||
replace(contract, indeterminate_reasons=contract.indeterminate_reasons + ("new_reason",)),
|
||||
)
|
||||
assert len({family.family_contract_digest(contract), *(family.family_contract_digest(item) for item in variants)}) == 4
|
||||
Reference in New Issue
Block a user