Contract: Triage Auto-Suppress Predicate (v1)

Status

Purpose

Define the stella.ops/triageSuppress@v1 predicate type used when a runtime micro-witness DSSE confirms that a vulnerability’s canonical_id matches a VEX not_affected consensus, enabling automatic triage suppression with full auditability.

This contract is for engineers working on the Signals join service, the Attestor predicate model, and the EvidenceLocker/AuditPack export path. It deliberately documents both the target design and the current implementation gaps (see Implementation Status) so callers do not assume behaviour that is not yet wired.

Implementation Status (as of 2026-05-30)

This contract is partially implemented. The data model, the rule-evaluation logic, and the audit-pack export path exist; the end-to-end signing/anchoring pipeline and automated tests do not.

ComponentWhereStatus
Predicate data model (TriageSuppressPredicate, VexConsensusRef, WitnessEvidenceRef, DeterministicReplayInputs)src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Predicates/TriageSuppressPredicate.csImplemented
Suppression-rule evaluator (TriageSuppressJoinService.EvaluateAsync / EvaluateCore)src/Signals/StellaOps.Signals/Services/TriageSuppressJoinService.csImplemented
DI registration (AddTriageSuppressServices, wired in Signals Program.cs)src/Signals/StellaOps.Signals/Services/TriageSuppressServiceCollectionExtensions.csImplemented
Audit-pack export (TriageSuppressExportBundle, ExportSegment.TriageSuppress, ZIP entry predicates/triage-suppress.json)src/__Libraries/StellaOps.AuditPack/Services/TriageSuppressExportBundle.csImplemented
VexLens consensus query inside the join serviceNOT IMPLEMENTED — VEX status/confidence/digest are supplied by the caller; the service does not query VexLens. There is no triage-suppress code under src/VexLens/.
Predicate emission → Signer → Attestor → Rekor anchoringNOT IMPLEMENTED — the evaluator returns a TriageSuppressResult decision; it does not build, sign, or anchor a DSSE. Deferred to runtime QA per the sprint.
Confidence-threshold gate (minimum_vex_confidence)NOT IMPLEMENTED in the evaluator — the 0.75 constant is unused (see the note under “When Suppression Is Decided”).
Predicate-type registration in the Attestor predicate-type registryNOT IMPLEMENTED — stella.ops/triageSuppress@v1 is a hardcoded URI constant (TriageSuppressPredicate.PredicateTypeUri); it is not seeded in 002_add_predicate_type_registry.sql.
Determinism / integration / negative testsNOT IMPLEMENTED — no dedicated tests exist for the join service or predicate; deferred to runtime QA per the sprint.

Predicate Type URI

stella.ops/triageSuppress@v1

When Suppression Is Decided

TriageSuppressJoinService.EvaluateAsync returns a Suppressed = true decision (reason vex_not_affected_with_unreachability_confirmation) when the supplied reachability state is one of {ConfirmedUnreachable, StaticallyUnreachable, RuntimeUnobserved}. The intent of the join is that ALL of the following hold; the first two are the caller’s responsibility (the service does not query VexLens — VEX status, confidence, and digest are supplied on the TriageSuppressRequest/result):

  1. A micro-witness DSSE exists for (canonical_id, cve_id) with an observation/lattice state in the suppressable set above.
  2. VexLens consensus for (canonical_id, cve_id) has status not_affected (the caller is expected to invoke the service only in this case; the evaluator hardcodes VexStatus = "not_affected" on its result).
  3. VexLens consensus confidence score >= configured threshold (default constant 0.75).

NOTE (implementation): The current evaluator (EvaluateCore) decides solely on the reachability state. It does NOT call VexLens and does NOT enforce the confidence threshold. The constant TriageSuppressJoinService.DefaultMinimumVexConfidence = 0.75 is declared but unused; the result’s VexConfidenceScore is a placeholder (0.0) and VexConsensusDigest is empty — both are expected to be filled in by the caller. The confidence gate (condition 3) and the not_affected gate (condition 2) are therefore not currently enforced inside the service.

Suppression Rules Truth Table

The reachability states are the eight values of StellaOps.Signals.Lattice.ReachabilityLatticeState (short codes from ToCode(): U, SR, SU, RO, RU, CR, CU, X). In the evaluator: the AUTO-SUPPRESS rows correspond to the SuppressableStates set {CU, SU, RU}; the LOG ONLY, FLAG CONFLICT rows correspond to the ConflictStates set {RO, CR, X} (result has ConflictDetected = true, reason conflict:vex_not_affected_but_reachability_<code>); the remaining LOG ONLY rows (U, SR) fall through with reason insufficient_evidence:reachability_<code>. All non-suppress rows set RequiresHumanReview = true.

The affected / under_investigation / fixed rows below are caller-enforced expectations: the evaluator is only invoked for not_affected consensus and hardcodes VexStatus = "not_affected", so it never actually evaluates those VEX statuses.

VEX StatusReachability StateActionRequires Human Review
not_affectedConfirmedUnreachable (CU)AUTO-SUPPRESSNo
not_affectedStaticallyUnreachable (SU)AUTO-SUPPRESSNo
not_affectedRuntimeUnobserved (RU)AUTO-SUPPRESSNo
not_affectedUnknown (U)LOG ONLYYes
not_affectedStaticallyReachable (SR)LOG ONLYYes
not_affectedRuntimeObserved (RO)LOG ONLY, FLAG CONFLICTYes
not_affectedConfirmedReachable (CR)LOG ONLY, FLAG CONFLICTYes
not_affectedContested (X)LOG ONLY, FLAG CONFLICTYes
affectedanyNEVER SUPPRESSYes
under_investigationanyNEVER SUPPRESSYes
fixedanyNEVER SUPPRESS (separate workflow)No

Predicate Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Triage Suppress Predicate v1",
  "type": "object",
  "required": ["cve_id", "suppress_reason", "vex_consensus", "witness_evidence", "reachability_state", "timestamp"],
  "properties": {
    "cve_id": {
      "type": "string",
      "description": "CVE identifier (e.g., CVE-2025-0001)",
      "pattern": "^CVE-\\d{4}-\\d{4,}$"
    },
    "suppress_reason": {
      "type": "string",
      "enum": ["vex_not_affected_with_unreachability_confirmation"],
      "description": "Machine-readable reason for suppression"
    },
    "vex_consensus": {
      "type": "object",
      "required": ["status", "confidence_score", "consensus_digest", "computed_at"],
      "properties": {
        "status": { "type": "string", "enum": ["not_affected"] },
        "justification": { "type": "string" },
        "confidence_score": { "type": "number", "minimum": 0, "maximum": 1 },
        "consensus_digest": { "type": "string", "description": "SHA-256 of the VexLens consensus record" },
        "source_count": { "type": "integer", "description": "Number of VEX sources contributing" },
        "computed_at": { "type": "string", "format": "date-time" }
      }
    },
    "witness_evidence": {
      "type": "object",
      "required": ["witness_id", "dsse_digest", "observation_type"],
      "properties": {
        "witness_id": { "type": "string", "description": "Witness ID (wit:sha256:...)" },
        "dsse_digest": { "type": "string", "description": "SHA-256 of the witness DSSE envelope" },
        "observation_type": { "type": "string", "enum": ["RuntimeUnobserved", "ConfirmedUnreachable", "StaticallyUnreachable"] },
        "predicate_type": { "type": "string", "description": "URI of the witness predicate type" }
      }
    },
    "reachability_state": {
      "type": "string",
      "enum": ["ConfirmedUnreachable", "StaticallyUnreachable", "RuntimeUnobserved"],
      "description": "Lattice state from the 8-state reachability model"
    },
    "timestamp": {
      "type": "string",
      "format": "date-time",
      "description": "UTC timestamp of suppression evaluation"
    },
    "deterministic_replay_inputs": {
      "type": "object",
      "description": "Inputs sufficient to replay this suppression decision",
      "properties": {
        "canonical_id": { "type": "string" },
        "vex_consensus_digest": { "type": "string" },
        "witness_id": { "type": "string" }
      }
    }
  }
}

DSSE Envelope Shape

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "artifact",
      "digest": {
        "sha256": "<canonical_id_hex>"
      }
    }
  ],
  "predicateType": "stella.ops/triageSuppress@v1",
  "predicate": {
    "cve_id": "CVE-2025-0001",
    "suppress_reason": "vex_not_affected_with_unreachability_confirmation",
    "vex_consensus": {
      "status": "not_affected",
      "justification": "vulnerable_code_not_in_execute_path",
      "confidence_score": 0.92,
      "consensus_digest": "sha256:abc123...",
      "source_count": 3,
      "computed_at": "2026-02-19T12:00:00Z"
    },
    "witness_evidence": {
      "witness_id": "wit:sha256:def456...",
      "dsse_digest": "sha256:789abc...",
      "observation_type": "ConfirmedUnreachable",
      "predicate_type": "https://stella.ops/predicates/path-witness/v1"
    },
    "reachability_state": "ConfirmedUnreachable",
    "timestamp": "2026-02-19T12:05:00Z",
    "deterministic_replay_inputs": {
      "canonical_id": "sha256:...",
      "vex_consensus_digest": "sha256:abc123...",
      "witness_id": "wit:sha256:def456..."
    }
  }
}

Idempotency

Given the same (canonical_id, cve_id, vex_consensus_digest, witness_id), the service MUST produce byte-identical DSSE output. This is intended to be enforced by:

NOTE (implementation gap): The current evaluator sets the result’s EvaluatedAt from TimeProvider.GetUtcNow() (wall clock), NOT from the input timestamps. As written, the rule evaluation is pure (the decision depends only on the reachability state), but the emitted timestamp is time-varying. A TestTimeProvider can be injected for deterministic tests, and the determinism/byte-identical-DSSE requirement is deferred to the (not-yet-implemented) emission pipeline. Do not rely on byte-identical output until that pipeline derives the timestamp from inputs as specified above.

Policy Opt-In (roadmap — NOT IMPLEMENTED)

NOTE: The configuration object below is the intended design and is NOT yet wired. No triage_suppress policy section is parsed by any service today. The only related code knob is the private constant TriageSuppressJoinService.DefaultMinimumVexConfidence = 0.75, which is currently unused (see the confidence-threshold note above). enabled, allowed_reachability_states, and require_rekor_anchor have no corresponding implementation; the evaluator’s suppressable set is hardcoded to {ConfirmedUnreachable, StaticallyUnreachable, RuntimeUnobserved}.

Auto-suppress is intended to be disabled by default. Operators would enable it via policy configuration:

{
  "triage_suppress": {
    "enabled": true,
    "minimum_vex_confidence": 0.75,
    "allowed_reachability_states": ["ConfirmedUnreachable", "StaticallyUnreachable", "RuntimeUnobserved"],
    "require_rekor_anchor": true
  }
}

Audit Trail

Every suppression evaluation (whether it results in suppression or not) is logged. The evaluator itself emits structured logs via TriageSuppressJoinService.LogEvaluation:

The intended full evidence trail:

Audit-pack export (implemented)

For offline audit, suppression evidence is exported through the AuditPack module (StellaOps.AuditPack). When ExportRequest.IncludeTriageSuppress is set, the export writes a TriageSuppressExportBundle to the ZIP entry predicates/triage-suppress.json, containing:

so an auditor can independently re-verify the join. Bundle membership is recorded via ExportManifest.IncludesTriageSuppress / BundleContentType.TriageSuppress. The offline verification script that walks this chain is specified here but is NOT yet implemented (deferred per SPRINT_20260219_012 MWS-03).

References

Source symbols (ground truth)