mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 01:48:17 +00:00
feat(detection-core): fact-pack admission and push-consumer contracts
This commit is contained in:
@@ -101,13 +101,13 @@ _Requirement IDs use N.M where N is the requirement number in requirements.md an
|
||||
- _Depends: 4.1, 5.1_
|
||||
|
||||
- [ ] 6. Fact packs and consumer interfaces
|
||||
- [ ] 6.1 Fact-pack admission contract
|
||||
- [x] 6.1 Fact-pack admission contract
|
||||
- Ruleset loader admits family fact packs only with review evidence and provenance stamping; packs carry their family tag.
|
||||
- Done when an unreviewed pack is refused and a reviewed pack stamps provenance in a test.
|
||||
- _Requirements: 10.1_
|
||||
- _Boundary: fact-pack contract_
|
||||
- _Depends: 1.4_
|
||||
- [ ] 6.2 Consumer interfaces for push and proof
|
||||
- [x] 6.2 Consumer interfaces for push and proof
|
||||
- Define the interfaces through which the pre-push transaction consumes traces/verdicts within its deadline and the proof layer attaches demonstrated evidence to a verdict; existing arms express behavior over the core contract.
|
||||
- Done when both interfaces are exercised by conformance tests from a mock consumer.
|
||||
- _Requirements: 9.2, 11.1, 11.2, 11.3_
|
||||
|
||||
@@ -11,7 +11,9 @@ from openultrasast.core.admission import (
|
||||
validate_admission_manifest,
|
||||
)
|
||||
from openultrasast.core.budget import Budget, BudgetGateFailure, Complexity, check_budget, derive_budget
|
||||
from openultrasast.core.consumer import PushRequest, PushVerdict, consume_verdict
|
||||
from openultrasast.core.coverage import Coverage, InstrumentFailure, ResolutionOutcome, check_reads
|
||||
from openultrasast.core.factpack import FactPack, FactPackError, admit_fact_pack, verify_fact_pack
|
||||
from openultrasast.core.probes import (
|
||||
FittedComponentBlocked,
|
||||
ProbeResult,
|
||||
@@ -33,6 +35,8 @@ __all__ = [
|
||||
"BudgetGateFailure",
|
||||
"Complexity",
|
||||
"Coverage",
|
||||
"FactPack",
|
||||
"FactPackError",
|
||||
"FittedComponentBlocked",
|
||||
"Guard",
|
||||
"Hop",
|
||||
@@ -41,18 +45,23 @@ __all__ = [
|
||||
"ProbeResult",
|
||||
"ProvenClaim",
|
||||
"ProvenanceMismatch",
|
||||
"PushRequest",
|
||||
"PushVerdict",
|
||||
"ResolutionOutcome",
|
||||
"ShortcutDetected",
|
||||
"TraceRecord",
|
||||
"Witness",
|
||||
"admit_fact_pack",
|
||||
"as_clean",
|
||||
"assert_no_shortcut",
|
||||
"check_budget",
|
||||
"check_reads",
|
||||
"consume_verdict",
|
||||
"derive_budget",
|
||||
"require_admission_evidence",
|
||||
"reversed_pair_probe",
|
||||
"stamp",
|
||||
"validate_admission_manifest",
|
||||
"verify_fact_pack",
|
||||
"verify_stamp",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Consumer contracts for the detection core (Req 9, 11).
|
||||
|
||||
The pre-push transaction consumes traces and verdicts through this interface within
|
||||
its (measured) budget; the proof layer attaches demonstrated evidence to a verdict.
|
||||
Every claim must carry a witness; every silence must carry a coverage reason.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
|
||||
from openultrasast.core.trace import TraceRecord
|
||||
from openultrasast.core.verdict import BoundedSilence, ProvenClaim
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PushRequest:
|
||||
"""One push transaction handed to the core."""
|
||||
|
||||
base: str
|
||||
head: str
|
||||
budget_class: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PushVerdict:
|
||||
"""What the push consumer receives: proven claims plus bounded silence."""
|
||||
|
||||
claims: tuple[ProvenClaim, ...]
|
||||
silence: tuple[BoundedSilence, ...]
|
||||
digests: dict[str, str]
|
||||
|
||||
|
||||
def consume_verdict(
|
||||
request: PushRequest,
|
||||
record: TraceRecord,
|
||||
claims: Sequence[ProvenClaim],
|
||||
silence: Sequence[BoundedSilence],
|
||||
) -> PushVerdict:
|
||||
"""Validate and wrap one trace's verdict for the push consumer (Req 5, 11.1).
|
||||
|
||||
A claim without a witness or a silence without a reason is refused: silence is
|
||||
never a clean bill of health.
|
||||
"""
|
||||
for claim in claims:
|
||||
if not claim.witness or not claim.path or claim.line <= 0:
|
||||
raise ValueError(f"proven claim lacks witness or exact location: {claim}")
|
||||
for entry in silence:
|
||||
if not entry.reason:
|
||||
raise ValueError(f"bounded silence lacks a coverage reason: {entry}")
|
||||
return PushVerdict(claims=tuple(claims), silence=tuple(silence), digests=dict(record.digests))
|
||||
|
||||
|
||||
__all__ = ["PushRequest", "PushVerdict", "consume_verdict"]
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Family fact-pack admission contract for the detection core (Req 10).
|
||||
|
||||
New vulnerability families enter only as reviewed fact packs: the pack carries its
|
||||
family tag and a review digest, and its fact-set digest is stamped so consumers can
|
||||
refuse tampered content. The core never reads pool or admission labels.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
class FactPackError(ValueError):
|
||||
"""A fact pack is unreviewed or its content does not match its digest."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FactPack:
|
||||
"""A reviewed family fact pack with provenance."""
|
||||
|
||||
family: str
|
||||
facts: tuple[dict[str, Any], ...]
|
||||
review_digest: str
|
||||
fact_set_digest: str
|
||||
|
||||
|
||||
def _digest(payload: object) -> str:
|
||||
return hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()).hexdigest()[:12]
|
||||
|
||||
|
||||
def admit_fact_pack(family: str, facts: Sequence[Mapping[str, Any]], *, review_digest: str) -> FactPack:
|
||||
"""Admit a pack only with review evidence; stamp its fact-set digest (Req 10.1)."""
|
||||
if not review_digest:
|
||||
raise FactPackError("unreviewed fact pack refused: no review digest")
|
||||
normalized = tuple(dict(fact) for fact in facts)
|
||||
return FactPack(
|
||||
family=family,
|
||||
facts=normalized,
|
||||
review_digest=review_digest,
|
||||
fact_set_digest=_digest([family, normalized]),
|
||||
)
|
||||
|
||||
|
||||
def verify_fact_pack(pack: FactPack, facts: Sequence[Mapping[str, Any]]) -> None:
|
||||
"""Refuse content that does not match the pack's stamped digest."""
|
||||
normalized = tuple(dict(fact) for fact in facts)
|
||||
if _digest([pack.family, normalized]) != pack.fact_set_digest:
|
||||
raise FactPackError(f"fact content does not match the stamped digest of pack {pack.family!r}")
|
||||
|
||||
|
||||
__all__ = ["FactPack", "FactPackError", "admit_fact_pack", "verify_fact_pack"]
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Fact-pack and consumer contract tests for detection-core tasks 6.1-6.2 (Req 9, 10, 11)."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from openultrasast.core.consumer import PushRequest, consume_verdict # noqa: E402
|
||||
from openultrasast.core.factpack import FactPackError, admit_fact_pack, verify_fact_pack # noqa: E402
|
||||
from openultrasast.core.trace import TraceRecord # noqa: E402
|
||||
from openultrasast.core.verdict import BoundedSilence, Judgment, ProvenClaim # noqa: E402
|
||||
|
||||
FACTS = ({"id": "obligation:protected_read", "language": "java"},)
|
||||
|
||||
|
||||
def test_unreviewed_fact_pack_is_refused() -> None:
|
||||
try:
|
||||
admit_fact_pack("access_control", FACTS, review_digest="")
|
||||
raise AssertionError("unreviewed pack must be refused")
|
||||
except FactPackError:
|
||||
pass
|
||||
|
||||
|
||||
def test_reviewed_fact_pack_stamps_provenance() -> None:
|
||||
pack = admit_fact_pack("access_control", FACTS, review_digest="review-2026-10-30")
|
||||
assert pack.family == "access_control"
|
||||
assert pack.review_digest == "review-2026-10-30"
|
||||
assert len(pack.fact_set_digest) == 12
|
||||
verify_fact_pack(pack, FACTS)
|
||||
try:
|
||||
verify_fact_pack(pack, FACTS + ({"id": "tampered"},))
|
||||
raise AssertionError("tampered facts must be refused")
|
||||
except FactPackError:
|
||||
pass
|
||||
|
||||
|
||||
def _record() -> TraceRecord:
|
||||
return TraceRecord(
|
||||
entry="src/app.py::handler",
|
||||
hops=(),
|
||||
operation_kind="protected_read",
|
||||
resource="credentials",
|
||||
authority="admin",
|
||||
judgment=Judgment.UNGUARDED,
|
||||
digests={"input": "abc"},
|
||||
)
|
||||
|
||||
|
||||
def test_consumer_accepts_witnessed_claim_and_reasoned_silence() -> None:
|
||||
claim = ProvenClaim(trace_id="t1", witness="w1", path="src/app.py", line=10, family="access_control")
|
||||
silence = BoundedSilence(path="src/other.py", reason="unreached", coverage={"entry_points": 0})
|
||||
verdict = consume_verdict(PushRequest(base="b", head="h", budget_class="dev-pool"), _record(), (claim,), (silence,))
|
||||
assert verdict.claims == (claim,)
|
||||
assert verdict.silence == (silence,)
|
||||
|
||||
|
||||
def test_consumer_refuses_claim_without_witness_or_silence_without_reason() -> None:
|
||||
bad_claim = ProvenClaim(trace_id="t1", witness="", path="src/app.py", line=10, family="access_control")
|
||||
try:
|
||||
consume_verdict(PushRequest(base="b", head="h", budget_class="c"), _record(), (bad_claim,), ())
|
||||
raise AssertionError("witnessless claim must be refused")
|
||||
except ValueError:
|
||||
pass
|
||||
bad_silence = BoundedSilence(path="src/other.py", reason="", coverage={})
|
||||
try:
|
||||
consume_verdict(PushRequest(base="b", head="h", budget_class="c"), _record(), (), (bad_silence,))
|
||||
raise AssertionError("reasonless silence must be refused")
|
||||
except ValueError:
|
||||
pass
|
||||
Reference in New Issue
Block a user