feat(detection-core): foundation contract package (trace, verdict, coverage, provenance)

This commit is contained in:
Marius Ciepluch
2026-10-30 16:04:10 +01:00
parent 839d58e3d3
commit 0b56ad5514
12 changed files with 1077 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
"""Detection core: the shared security-trace contract package.
Owns contracts only -- no analysis logic. Arms implement the trace contract;
runners and consumers adopt coverage, budget, provenance, and admission contracts.
"""
from openultrasast.core.coverage import Coverage, InstrumentFailure, ResolutionOutcome, check_reads
from openultrasast.core.provenance import ProvenanceMismatch, stamp, verify_stamp
from openultrasast.core.trace import Guard, Hop, TraceRecord, Witness
from openultrasast.core.verdict import BoundedSilence, Judgment, ProvenClaim, as_clean
__all__ = [
"BoundedSilence",
"Coverage",
"Guard",
"Hop",
"InstrumentFailure",
"Judgment",
"ProvenClaim",
"ProvenanceMismatch",
"ResolutionOutcome",
"TraceRecord",
"Witness",
"as_clean",
"check_reads",
"stamp",
"verify_stamp",
]
+57
View File
@@ -0,0 +1,57 @@
"""Coverage accounting for the detection core (Req 3).
Every input is accounted for: entry recognition, exactly one resolution bucket per
call, named reasons for unchecked operations. A zero-byte or unreadable input is an
instrument failure -- a zero from an unread input is a failed run, never a clean one.
"""
from __future__ import annotations
import enum
from collections.abc import Mapping
from dataclasses import dataclass, field
class InstrumentFailure(ValueError):
"""The run's input was not read; any result from it is invalid."""
class ResolutionOutcome(enum.Enum):
"""Exactly one bucket per call (Req 3.1)."""
COMPLETE = "complete"
INCOMPLETE = "incomplete"
UNRESOLVED = "unresolved"
@dataclass(frozen=True)
class Coverage:
"""Coverage of one analyzed change."""
entry_points: int
frameworks_recognised: dict[str, int] = field(default_factory=dict)
calls: int = 0
complete: int = 0
incomplete: int = 0
unresolved: int = 0
unchecked_reasons: dict[str, int] = field(default_factory=dict)
def validate(self) -> None:
"""Refuse coverage where calls lack exactly one resolution bucket."""
buckets = (self.complete, self.incomplete, self.unresolved)
if min(buckets) < 0:
raise ValueError(f"negative resolution bucket in {buckets}")
if sum(buckets) != self.calls:
raise ValueError(
f"resolution buckets ({self.complete}+{self.incomplete}+{self.unresolved}) must equal the call count ({self.calls})"
)
def check_reads(reads: Mapping[str, int]) -> None:
"""Fail the run when any tracked input was read as zero bytes (Req 3.2)."""
zero = sorted(path for path, size in reads.items() if size <= 0)
if zero:
raise InstrumentFailure(f"zero-byte read of tracked input: {', '.join(zero)}")
__all__ = ["Coverage", "InstrumentFailure", "ResolutionOutcome", "check_reads"]
+62
View File
@@ -0,0 +1,62 @@
"""Provenance digest stamping for the detection core (Req 6).
Every analysis is stamped with the analyzer revision, fact-set, acquisition-record,
configuration, population, and per-change input digests. A result whose digests
cannot be re-derived from the named inputs is refused, never accepted.
"""
from __future__ import annotations
import hashlib
import json
from collections.abc import Mapping
from typing import Any
class ProvenanceMismatch(ValueError):
"""A recorded digest does not match the re-derived input identity."""
_DIGEST_FIELDS = (
"analyzer_revision",
"fact_set_digest",
"acquisition_record_digest",
"configuration_digest",
"population_digest",
"input_digest",
)
def _input_digest(change_id: str, population_digest: str) -> str:
payload = json.dumps([population_digest, change_id], sort_keys=True, separators=(",", ":"))
return hashlib.sha256(payload.encode()).hexdigest()[:12]
def stamp(
*,
analyzer_revision: str,
fact_set_digest: str,
acquisition_record_digest: str,
configuration_digest: str,
population_digest: str,
change_id: str,
) -> dict[str, str]:
"""The provenance stamp of one analysis (Req 6.1)."""
return {
"analyzer_revision": analyzer_revision,
"fact_set_digest": fact_set_digest,
"acquisition_record_digest": acquisition_record_digest,
"configuration_digest": configuration_digest,
"population_digest": population_digest,
"input_digest": _input_digest(change_id, population_digest),
}
def verify_stamp(recorded: Mapping[str, Any], rederived: Mapping[str, Any]) -> None:
"""Refuse when any digest cannot be re-derived (Req 6.2)."""
for field_name in _DIGEST_FIELDS:
if recorded.get(field_name) != rederived.get(field_name):
raise ProvenanceMismatch(f"{field_name}: recorded {recorded.get(field_name)!r} != rederived {rederived.get(field_name)!r}")
__all__ = ["ProvenanceMismatch", "stamp", "verify_stamp"]
+74
View File
@@ -0,0 +1,74 @@
"""Canonical trace schema for the detection core (Req 1).
One family-agnostic trace record per analyzed change, serialized deterministically:
identical inputs produce byte-identical canonical output. The access-control
prover's trace is the first implementation of this contract.
"""
from __future__ import annotations
import json
from dataclasses import asdict, dataclass, field
from typing import Any
from openultrasast.core.verdict import Judgment
def _canonical_bytes(payload: dict[str, Any]) -> bytes:
return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
@dataclass(frozen=True)
class Hop:
"""One ordered hop with its call-target completeness."""
function: str
call_line: int | None
complete: bool
@dataclass(frozen=True)
class Witness:
"""Evidence for or against a guard, with provenance and match relation."""
kind: str
provenance: str
relation: str
@dataclass(frozen=True)
class Guard:
"""A guard effect observed on a path (kind plus effect)."""
kind: str
effect: str
@dataclass(frozen=True)
class TraceRecord:
"""The canonical security trace: entry, hops, operation, witnesses, guards, judgment, digests."""
entry: str
hops: tuple[Hop, ...]
operation_kind: str
resource: str
authority: str
witnesses: tuple[Witness, ...] = ()
guards: tuple[Guard, ...] = ()
completeness_reasons: tuple[str, ...] = ()
judgment: Judgment = Judgment.INDETERMINATE
digests: dict[str, str] = field(default_factory=dict)
def record(self) -> dict[str, Any]:
"""JSON-ready dict with sorted keys; enums encode as their value strings."""
payload: dict[str, Any] = asdict(self)
payload["judgment"] = self.judgment.value
record: dict[str, Any] = json.loads(json.dumps(payload, sort_keys=True))
return record
def canonical(self) -> bytes:
"""Deterministic byte serialization (Req 1.2)."""
return _canonical_bytes(self.record())
__all__ = ["Hop", "TraceRecord", "Witness"]
+51
View File
@@ -0,0 +1,51 @@
"""Verdict vocabulary and verdict contract for the detection core (Req 2, 5, 9).
Every analyzed operation is judged as exactly one of unguarded, discharged, or
indeterminate. A verdict is either a proven claim (witness attached, exact
location) or a bounded silence (coverage reason attached). Indeterminate never
converts to clean, and models may only judge residuals zero-shot over a trace.
"""
from __future__ import annotations
import enum
from dataclasses import dataclass
from typing import Any
class Judgment(enum.Enum):
"""The three-way judgment vocabulary; value strings are the wire format."""
UNGUARDED = "unguarded"
DISCHARGED = "discharged"
INDETERMINATE = "indeterminate"
@dataclass(frozen=True)
class ProvenClaim:
"""A defect claim carrying its witness and exact location (Req 5.1)."""
trace_id: str
witness: str
path: str
line: int
family: str
@dataclass(frozen=True)
class BoundedSilence:
"""An undecided site with its coverage reason; never a clean result (Req 5.2)."""
path: str
reason: str
coverage: dict[str, Any]
def as_clean(judgment: Judgment) -> Judgment:
"""Return a clean judgment, refusing to convert indeterminate (Req 2.1)."""
if judgment is Judgment.INDETERMINATE:
raise ValueError("indeterminate never converts to a clean judgment")
return judgment
__all__ = ["BoundedSilence", "Judgment", "ProvenClaim", "as_clean"]