feat(detection-core): replay fact-derived relations

This commit is contained in:
Marius Ciepluch
2026-10-31 16:33:53 +01:00
parent 117b5b4bf7
commit 2796647259
4 changed files with 723 additions and 11 deletions
+430 -9
View File
@@ -7,7 +7,7 @@ from dataclasses import dataclass, replace
import pytest
from detection_core_fixtures import InMemoryContentResolver, InMemoryPopulationCustody, custody_contract, stored_artifact
from openultrasast.core import canonical, provenance
from openultrasast.core import canonical, evidence, provenance
from openultrasast.core.evidence import NeutralOutcome
from openultrasast.core.factapply import FactApplicationError, apply_fact
from openultrasast.core.factmatch import (
@@ -34,7 +34,9 @@ from openultrasast.core.factpack import (
derive_review_evidence_digest,
derive_review_independence_policy_digest,
)
from openultrasast.core.factvalidate import FactRelationValidationError, validate_fact_relations
from openultrasast.core.family import (
MISSING_REVIEWED_DEPENDENCY,
DecisionRule,
FamilyContract,
FieldSpec,
@@ -42,15 +44,20 @@ from openultrasast.core.family import (
RelationOriginPolicy,
ResidualQuestion,
RoleSpec,
derive_security_object_id,
family_contract_digest,
)
from openultrasast.core.semantics import (
EvidenceProvenance,
FieldKind,
FieldValue,
ProvenanceKind,
RelationKind,
RelationStatus,
SemanticNode,
SemanticRelation,
SourceLocation,
make_relation,
)
@@ -68,21 +75,47 @@ class ApplyCase:
population_digest: str
def _contract() -> FamilyContract:
class MultiPopulationCustody(InMemoryPopulationCustody):
def __init__(
self,
logs: tuple[provenance.PopulationCustodyLog, ...],
policy: provenance.PopulationCustodyPolicy,
) -> None:
self._logs = {log.population_digest: log for log in logs}
self._multi_policy = policy
def current_log(self, population_digest: str) -> provenance.PopulationCustodyLog:
try:
return self._logs[population_digest]
except KeyError as exc:
raise provenance.PopulationCustodyError("population custody log not found") from exc
def append(self, request: provenance.PopulationUseRequest, custodian: str) -> provenance.PopulationUseReceipt:
prior = self.current_log(request.population_digest)
receipt = provenance.record_population_use(request, prior, custodian, self._multi_policy)
self._logs[request.population_digest] = provenance.append_population_use(prior, receipt, self._multi_policy)
return receipt
def _contract(*, fact_only: bool = False) -> FamilyContract:
origin = RelationOriginPolicy.EXCLUDED_DEPENDENCY_FACT_ONLY if fact_only else RelationOriginPolicy.PRODUCT_OR_REVIEWED_FACT
return FamilyContract(
"reference-family",
"1",
"family.v2",
evidence.EVIDENCE_PACKET_SCHEMA,
("python",),
ObjectSpec("resource", (FieldSpec("name", FieldKind.TEXT, True),), ("name",)),
(
RoleSpec("flow", (), (RelationKind.CALL,), RelationOriginPolicy.PRODUCT_OR_REVIEWED_FACT),
RoleSpec("flow", (), (RelationKind.CALL,), origin),
RoleSpec("source", (FieldSpec("name", FieldKind.TEXT, False),), (), RelationOriginPolicy.PRODUCT_OR_REVIEWED_FACT),
RoleSpec("target", (FieldSpec("name", FieldKind.TEXT, False),), (), RelationOriginPolicy.PRODUCT_OR_REVIEWED_FACT),
),
(DecisionRule("unknown", NeutralOutcome.INDETERMINATE, (), (RelationStatus.UNRESOLVED,)),),
(ResidualQuestion("classify", ("flow",), ("unknown",)),),
("missing",),
(
DecisionRule("safe", NeutralOutcome.SATISFIED, ("flow",), (RelationStatus.COMPLETE,)),
DecisionRule("unknown", NeutralOutcome.INDETERMINATE, (), (RelationStatus.INCOMPLETE, RelationStatus.UNRESOLVED)),
),
(ResidualQuestion("classify", ("flow",), ("safe", "unknown")),),
("missing", MISSING_REVIEWED_DEPENDENCY),
)
@@ -94,8 +127,9 @@ def _append_use(
*,
labels_accessed: bool,
arm_id: str,
population_digest: str | None = None,
) -> provenance.LineageUse:
current = custody.current_log(canonical.content_digest("population"))
current = custody.current_log(population_digest or canonical.content_digest("population"))
request = provenance.PopulationUseRequest(
current.population_digest,
arm_id,
@@ -152,8 +186,9 @@ def _case(
matcher_labels: bool = False,
promotion_labels: bool = False,
omitted_promotion_labels: bool = False,
fact_only: bool = False,
) -> ApplyCase:
contract = _contract()
contract = _contract(fact_only=fact_only)
evidence, review_resolver, reviewer = _review_evidence()
review_policy = _review_policy(reviewer)
effects = effects or (RelationEffect(RelationKind.CALL, "flow", "source", "target", ("matched",)),)
@@ -283,6 +318,128 @@ def _apply(case: ApplyCase) -> tuple[SemanticRelation, ...]:
)
def _case_with_distinct_evaluation_population() -> ApplyCase:
case = _case(fact_only=True)
source_log = case.custody.current_log(case.population_digest)
evaluation_digest = canonical.content_digest("distinct-evaluation-population")
evaluation_log = provenance.empty_population_custody_log(
evaluation_digest,
case.custody_policy.reviewed_custodian_digests[0],
)
custody = MultiPopulationCustody((source_log, evaluation_log), case.custody_policy)
return replace(case, custody=custody, population_digest=evaluation_digest)
def _case_with_three_populations() -> tuple[ApplyCase, str]:
case = _case(fact_only=True)
custodian = case.custody_policy.reviewed_custodian_digests[0]
source_log = case.custody.current_log(case.population_digest)
fact_empty = provenance.empty_population_custody_log(case.population_digest, custodian)
fact_log = provenance.append_population_use(fact_empty, source_log.receipts[0], case.custody_policy)
matcher_population = canonical.content_digest("distinct-matcher-population")
matcher_empty = provenance.empty_population_custody_log(matcher_population, custodian)
matcher_request = provenance.PopulationUseRequest(
matcher_population,
"matcher-arm",
provenance.LineagePurpose.MATCHING_BEHAVIOR,
False,
case.match.matcher_behavior_digest,
matcher_empty.log_digest,
)
matcher_receipt = provenance.record_population_use(matcher_request, matcher_empty, custodian, case.custody_policy)
matcher_log = provenance.append_population_use(matcher_empty, matcher_receipt, case.custody_policy)
matcher_use = provenance.LineageUse(
provenance.LineagePurpose.MATCHING_BEHAVIOR,
matcher_population,
False,
case.match.matcher_behavior_digest,
matcher_receipt.receipt_digest,
)
matcher_lineage = _manifest("matcher-arm", case.match.matcher_behavior_digest, (matcher_use,))
provisional_match = replace(
case.match,
matcher_lineage_manifest_digest=matcher_lineage.manifest_digest,
match_digest="0" * 64,
)
match = replace(provisional_match, match_digest=derive_fact_match_digest(provisional_match))
match_reference, match_resolver = stored_artifact(fact_match_bytes(match), artifact_id="fact-match")
evaluation_digest = canonical.content_digest("third-evaluation-population")
evaluation_log = provenance.empty_population_custody_log(evaluation_digest, custodian)
custody = MultiPopulationCustody((fact_log, matcher_log, evaluation_log), case.custody_policy)
artifacts = {**case.resolver.artifacts, **match_resolver.artifacts}
changed = replace(
case,
match=match,
match_reference=match_reference,
lineages=(case.lineages[0], matcher_lineage),
custody=custody,
resolver=InMemoryContentResolver(artifacts),
population_digest=evaluation_digest,
)
return changed, matcher_population
def _stamp(case: ApplyCase) -> provenance.ProvenanceStamp:
return provenance.ProvenanceStamp(
canonical.content_digest("analyzer"),
family_contract_digest(case.contract),
canonical.content_digest("fact-set"),
canonical.content_digest("acquisition"),
canonical.content_digest("configuration"),
case.population_digest,
canonical.content_digest("change-input"),
)
def _record(case: ApplyCase, relation: SemanticRelation) -> evidence.EvidenceRecord:
fields = (FieldValue("name", FieldKind.TEXT, "resource"),)
scope = evidence.AnalyzedScope(
"base",
"head",
canonical.content_digest("change"),
(evidence.SourceRegion(case.match.product_location.path, 1, 30),),
)
reasons = relation.reasons if relation.status is not RelationStatus.COMPLETE else ()
packet = evidence.EvidencePacket(
evidence.EVIDENCE_PACKET_SCHEMA,
case.contract.family_id,
family_contract_digest(case.contract),
derive_security_object_id(case.contract.security_object, fields),
case.pack.ecosystem,
scope,
(relation,),
(),
fields,
reasons,
_stamp(case),
)
outcome = NeutralOutcome.SATISFIED if relation.status is RelationStatus.COMPLETE else NeutralOutcome.INDETERMINATE
decision = evidence.Decision(
"safe" if relation.status is RelationStatus.COMPLETE else "unknown",
outcome,
evidence.EvidenceBasis.DETERMINISTIC,
scope,
reasons,
evidence.derive_packet_digest(packet),
provenance.DecisionProvenance(None, None, None, None, None, None),
)
return evidence.EvidenceRecord(packet, decision, "")
def _validate(case: ApplyCase, record: evidence.EvidenceRecord, *, packs: tuple[FactPack, ...] | None = None) -> None:
validate_fact_relations(
record,
case.contract,
(case.pack,) if packs is None else packs,
case.population_digest,
case.lineages,
case.custody,
case.custody_policy,
case.review_policy,
case.resolver,
)
def test_complete_match_materializes_only_declared_effect_with_exact_provenance() -> None:
case = _case()
@@ -366,3 +523,267 @@ def test_omitted_current_promotion_spend_cannot_hide_behind_clean_pack_lineage()
assert case.custody.current_log(case.population_digest).status is provenance.PopulationStatus.SPENT
with pytest.raises(FactApplicationError, match="promotion.*protected_population_labels"):
_apply(case)
def test_reviewed_dependency_relation_replays_from_every_named_input() -> None:
case = _case(fact_only=True)
(relation,) = _apply(case)
record = _record(case, relation)
_validate(case, record)
def test_product_only_dependency_behavior_is_refused() -> None:
case = _case(fact_only=True)
(reviewed,) = _apply(case)
product = replace(
reviewed,
provenance=EvidenceProvenance(ProvenanceKind.PRODUCT_CODE, case.match.match_digest, None, None, None),
relation_id="",
)
with pytest.raises(FactRelationValidationError, match="reviewed semantic fact"):
_validate(case, _record(case, product))
def test_missing_dependency_fact_remains_named_and_indeterminate() -> None:
case = _case(fact_only=True)
location = case.match.product_location
missing = make_relation(
kind=RelationKind.CALL,
role="flow",
source=SemanticNode("source", "source", location),
target=SemanticNode("target", "target", location),
location=location,
status=RelationStatus.INCOMPLETE,
conditions=("matched",),
reasons=(MISSING_REVIEWED_DEPENDENCY,),
provenance=EvidenceProvenance(ProvenanceKind.PRODUCT_CODE, case.match.match_digest, None, None, None),
)
record = _record(case, missing)
_validate(case, record)
assert record.decision.outcome is NeutralOutcome.INDETERMINATE
assert record.decision.reasons == (MISSING_REVIEWED_DEPENDENCY,)
def test_fact_relation_replay_refuses_undeclared_role_and_invalid_scope() -> None:
case = _case(fact_only=True)
(relation,) = _apply(case)
undeclared = replace(relation, role="undeclared", relation_id="")
with pytest.raises(FactRelationValidationError, match="undeclared semantic role"):
_validate(case, _record(case, undeclared))
record = _record(case, relation)
packet = replace(record.packet, ecosystem="javascript")
decision = replace(record.decision, packet_digest=evidence.derive_packet_digest(packet))
with pytest.raises(FactRelationValidationError, match="unsupported ecosystem scope"):
_validate(case, evidence.EvidenceRecord(packet, decision, ""))
def test_fact_relation_replay_refuses_inconsistent_relation_and_fact_identity() -> None:
case = _case(fact_only=True)
(relation,) = _apply(case)
inconsistent = replace(relation, conditions=("substituted",), relation_id="")
with pytest.raises(FactRelationValidationError, match="does not equal replayed"):
_validate(case, _record(case, inconsistent))
wrong_origin = replace(relation.provenance, semantic_fact_id="other.fact")
wrong_fact = replace(relation, provenance=wrong_origin, relation_id="")
with pytest.raises(FactRelationValidationError, match="fact identity"):
_validate(case, _record(case, wrong_fact))
def test_fact_relation_replay_refuses_substituted_resolved_match_bytes() -> None:
case = _case(fact_only=True)
(relation,) = _apply(case)
wrong_ref, wrong_resolver = stored_artifact(b'{"plausible":"but-wrong"}', artifact_id="wrong-replay-match")
wrong_origin = replace(
relation.provenance,
source_content_digest=wrong_ref.content_digest,
fact_match=wrong_ref,
)
wrong_relation = replace(relation, provenance=wrong_origin, relation_id="")
changed = replace(
case,
resolver=InMemoryContentResolver({**case.resolver.artifacts, **wrong_resolver.artifacts}),
)
with pytest.raises(FactRelationValidationError, match="match artifact"):
_validate(changed, _record(changed, wrong_relation))
def test_fact_relation_replay_refuses_ambiguous_pack_and_changed_current_custody() -> None:
case = _case(fact_only=True)
(relation,) = _apply(case)
record = _record(case, relation)
with pytest.raises(FactRelationValidationError, match="pack identity is missing or ambiguous"):
_validate(case, record, packs=(case.pack, case.pack))
_append_use(
case.custody,
case.custody_policy.reviewed_custodian_digests[0],
provenance.LineagePurpose.PROMOTION,
case.pack.normalized_content_digest,
labels_accessed=True,
arm_id="other-arm",
)
with pytest.raises(FactRelationValidationError, match="promotion lineage is contaminated"):
_validate(case, record)
def test_fact_relation_replay_refuses_omitted_current_semantic_fact_label_use() -> None:
case = _case(fact_only=True)
(relation,) = _apply(case)
record = _record(case, relation)
_append_use(
case.custody,
case.custody_policy.reviewed_custodian_digests[0],
provenance.LineagePurpose.SEMANTIC_FACT,
case.pack.normalized_content_digest,
labels_accessed=True,
arm_id="fact-arm",
)
assert case.custody.current_log(case.population_digest).status is provenance.PopulationStatus.SPENT
with pytest.raises(FactRelationValidationError, match="semantic fact.*protected_population_labels"):
_validate(case, record)
def test_fact_relation_replay_refuses_cross_arm_protected_semantic_fact_label_use() -> None:
case = _case(fact_only=True)
(relation,) = _apply(case)
record = _record(case, relation)
_append_use(
case.custody,
case.custody_policy.reviewed_custodian_digests[0],
provenance.LineagePurpose.SEMANTIC_FACT,
case.pack.normalized_content_digest,
labels_accessed=True,
arm_id="other-arm",
)
assert case.custody.current_log(case.population_digest).status is provenance.PopulationStatus.SPENT
with pytest.raises(FactRelationValidationError, match="semantic fact.*protected_population_labels"):
_validate(case, record)
def test_fact_relation_replay_refuses_cross_arm_label_use_from_lineage_source_population() -> None:
case = _case_with_distinct_evaluation_population()
(relation,) = _apply(case)
record = _record(case, relation)
source_population = canonical.content_digest("population")
_append_use(
case.custody,
case.custody_policy.reviewed_custodian_digests[0],
provenance.LineagePurpose.SEMANTIC_FACT,
case.pack.normalized_content_digest,
labels_accessed=True,
arm_id="other-arm",
)
assert source_population != case.population_digest
assert case.custody.current_log(source_population).status is provenance.PopulationStatus.SPENT
with pytest.raises(FactRelationValidationError, match="semantic fact.*label_access_forbidden"):
_validate(case, record)
def test_fact_relation_replay_refuses_label_use_from_other_supplied_lineage_population() -> None:
case, matcher_population = _case_with_three_populations()
(relation,) = _apply(case)
record = _record(case, relation)
_append_use(
case.custody,
case.custody_policy.reviewed_custodian_digests[0],
provenance.LineagePurpose.SEMANTIC_FACT,
case.pack.normalized_content_digest,
labels_accessed=True,
arm_id="other-arm",
population_digest=matcher_population,
)
assert matcher_population not in {
case.population_digest,
case.lineages[0].uses[0].source_population_digest,
}
assert case.custody.current_log(matcher_population).status is provenance.PopulationStatus.SPENT
with pytest.raises(FactRelationValidationError, match="semantic fact.*label_access_forbidden"):
_validate(case, record)
@pytest.mark.parametrize(
("purpose", "subject", "labels_accessed"),
(
(provenance.LineagePurpose.SEMANTIC_FACT, "exact-pack", False),
(provenance.LineagePurpose.MATCHING_BEHAVIOR, "exact-pack", True),
(provenance.LineagePurpose.SEMANTIC_FACT, "wrong-digest", True),
),
)
def test_fact_relation_replay_ignores_unrelated_cross_arm_source_receipts(
purpose: provenance.LineagePurpose,
subject: str,
labels_accessed: bool,
) -> None:
case = _case_with_distinct_evaluation_population()
(relation,) = _apply(case)
record = _record(case, relation)
influenced = case.pack.normalized_content_digest if subject == "exact-pack" else canonical.content_digest(subject)
_append_use(
case.custody,
case.custody_policy.reviewed_custodian_digests[0],
purpose,
influenced,
labels_accessed=labels_accessed,
arm_id="other-arm",
)
_validate(case, record)
@pytest.mark.parametrize(
("purpose", "subject", "labels_accessed"),
(
(provenance.LineagePurpose.SEMANTIC_FACT, "exact-pack", False),
(provenance.LineagePurpose.MATCHING_BEHAVIOR, "exact-pack", True),
(provenance.LineagePurpose.SEMANTIC_FACT, "wrong-digest", True),
),
)
def test_fact_relation_replay_ignores_unrelated_receipts_in_other_lineage_population(
purpose: provenance.LineagePurpose,
subject: str,
labels_accessed: bool,
) -> None:
case, matcher_population = _case_with_three_populations()
(relation,) = _apply(case)
record = _record(case, relation)
influenced = case.pack.normalized_content_digest if subject == "exact-pack" else canonical.content_digest(subject)
_append_use(
case.custody,
case.custody_policy.reviewed_custodian_digests[0],
purpose,
influenced,
labels_accessed=labels_accessed,
arm_id="other-arm",
population_digest=matcher_population,
)
_validate(case, record)
def test_fact_relation_replay_refuses_omitted_current_semantic_fact_use_without_labels() -> None:
case = _case(fact_only=True)
(relation,) = _apply(case)
record = _record(case, relation)
_append_use(
case.custody,
case.custody_policy.reviewed_custodian_digests[0],
provenance.LineagePurpose.SEMANTIC_FACT,
case.pack.normalized_content_digest,
labels_accessed=False,
arm_id="fact-arm",
)
assert case.custody.current_log(case.population_digest).status is provenance.PopulationStatus.AVAILABLE
with pytest.raises(FactRelationValidationError, match="semantic fact lineage omitted current use"):
_validate(case, record)