Skip to content
New kind of attack — caught and fixed before it was publicly known·Read the timeline
Probity Open Predicate Standard
/predicate/v1/security-verdict
View raw .md →

Probity Security-Verdict, v1

predicateType: https://getprobity.dev/predicate/v1/security-verdict

A security-verdict is a signed, offline-verifiable statement that a single named artifact was run in isolation, attacked with a pinned corpus of exploits, and given a PASS or FAIL verdict with per-attack evidence. It is the product output of an adversarial scan: an admission controller, a CI gate, or an auditor reads it to decide whether to admit the artifact, without re-running the scan and without trusting the tool that produced it.

This document specifies the predicate body. The signing envelope, subject binding, and offline-verify mechanics are in signing-envelope.md. The forensic evidence the verdict references is specified in catch-record.md.

The key words MUST, MUST NOT, REQUIRED, SHOULD, MAY, and OPTIONAL are to be interpreted as in RFC 2119.

Subject

The subject is the scanned artifact, by OCI digest. This is the deployable image the verdict is about, so an admission controller or registry can link the verdict to the exact image it is about to admit.

"subject": [
  { "name": "<scanned-artifact-name>", "digest": { "sha256": "<64-hex-oci-digest>" } }
]

Predicate

{
  "verdict": "fail",
  "verdict_reason": "rule cve:CVE-2025-53109 (CRITICAL) bypassed expected layer 'substrate.vfs_hook'",
  "session_id": "<session-id-from-signed-session-manifest>",
  "corpus_digest": "<sha256-of-packaged-attack-corpus>",
  "gate_engine_commit": "<auditor-pinnable-engine-identity>",
  "substrate_ref": { "name": "<substrate-name>", "digest": { "sha256": "<64-hex>" } },
  "policy": { "id": "<policy-id>", "blocking_severities": ["CRITICAL", "HIGH"] },
  "attack_results": [
    {
      "rule_id": "cve:CVE-2025-53109",
      "severity": "CRITICAL",
      "expected_layer": "substrate.vfs_hook",
      "blocked": false,
      "actual_layer": null,
      "full_payload_sha256": "<64-hex>"
    }
  ],
  "evidence_ref": {
    "batch_root": "<64-hex-rfc6962-batch-root>",
    "bundle_id": "<64-hex-cas-bundle-id>"
  },
  "execution_envelope": { "trace_id": "<opaque-trace-id>" },
  "compromised_state_ciphertext_digest": "<sha256-on-FAIL-only>"
}

Field semantics

  • verdict (REQUIRED) - pass or fail (lowercase). Determined by the policy, not by a naive "every attack blocked" rule.

  • verdict_reason (REQUIRED) - a human-readable summary of why the verdict is what it is. On a fail it names the first blocking-tier bypass; on a pass it states that all blocking-tier classes were contained. This is a display string; automated consumers use the structured fields.

  • session_id (REQUIRED) - the session id from the producer's signed session manifest, which binds together the session id, substrate digest, corpus digest, and engine identity at the moment the corpus was injected. It binds the verdict to a specific injection event: a verifier with the manifest can confirm the corpus was injected into the named substrate for this exact run, rather than stitched together afterward. The manifest proves INJECTION; corpus_digest alone proves only the corpus DEFINITION.

  • corpus_digest (REQUIRED) - the SHA-256 (64-char lowercase hex) of the packaged attack corpus the run was produced against. It pins the INPUT: the signed verdict commits to a specific set of exploit payloads, so a third party need not trust that the producer threw the named exploits rather than something else. Honesty: corpus_digest is NOT a solo completeness guarantee. It proves which corpus package was used; it does not, by itself, prove the corpus was exhaustive or that its payloads are genuinely adversarial. Whether the corpus is the complete approved set is verified out-of-band against an auditor-supplied anchor (see Value-binding anchors). On its own, corpus_digest is a pin on the input identity, not a proof of input quality or coverage.

  • gate_engine_commit (REQUIRED) - an auditor-pinnable identity of the engine that produced the verdict, in the spirit of an SLSA builder.id. Its purpose is identity-binding, not source reproduction: an auditor who knows the approved engine identity can confirm the verdict was produced by that engine, and the value also participates in the signed session manifest binding. Honesty: a third party cannot, from this string alone, re-derive or re-run the engine; the field lets an auditor pin and compare an identity it already knows, not independently reconstruct the producer.

  • substrate_ref (REQUIRED) - a reference (name + digest) to the isolation substrate the scan ran on. In this standard the substrate is an opaque, separately attested input identified only by its digest; how the substrate's integrity is attested is a wave-2 concern (see README.md). A verifier treats substrate_ref.digest as the value to check against an auditor-supplied approved digest and against a revocation log (below).

  • policy (REQUIRED) - the severity-tier ruleset. id names the ruleset (versioned independently of the predicate so the ruleset can evolve without bumping the predicate). blocking_severities lists the severity tiers whose unblocked attack forces a fail.

  • attack_results (REQUIRED) - one entry per attack in the corpus. (This generalizes an earlier CVE-only shape: an attack is no longer assumed to be a CVE.) Each entry:

    • rule_id (REQUIRED) - a namespaced rule identifier. The namespace before the colon names the rule family; the remainder is the family-specific id. CVE is ONE namespace among others: cve:CVE-2025-53109. Other rule families use their own namespace prefixes. A bare, un-namespaced id is non-conforming.
    • severity (REQUIRED) - the severity tier of the rule.
    • expected_layer (REQUIRED) - the registry layer value expected to block this attack.
    • blocked (REQUIRED) - true iff a catch-record was emitted whose normalized layer equals expected_layer (and whose violation type matches). Decided over the normalized vocabulary registry values.
    • actual_layer (REQUIRED) - the layer that actually fired, or null if none did.
    • full_payload_sha256 (OPTIONAL) - the SHA-256 of the exact payload thrown for this attack, using the canonical key full_payload_sha256 (matching the catch-record key). Pins the per-attack input down to the byte.
  • evidence_ref (REQUIRED) - a REFERENCE to the forensic evidence, not the evidence itself. The verdict does NOT embed catch-records inline (which would make the envelope unbounded). Instead it carries:

    • batch_root (REQUIRED) - the RFC 6962-style batch Merkle root over the run's ordered catch-records (see catch-record.md).
    • bundle_id (REQUIRED) - the content-addressed SHA-256 of the verdict bundle, the key under which the catch-records are resolvable from content-addressed storage.

    A verifier resolves the catch-records from content-addressed storage by bundle_id, recomputes the batch root, and confirms it equals batch_root before trusting any blocked: true. The records are the evidence behind every block; the reference keeps the verdict bounded while keeping the evidence cryptographically tied to it.

  • execution_envelope (OPTIONAL) - application-level correlation metadata for the run (for example an opaque trace id). It carries no security claim; it is a convenience pointer for correlating the verdict with other observability data.

  • compromised_state_ciphertext_digest (OPTIONAL, top-level) - present ONLY on a fail verdict that captured a forensic snapshot of the compromised state. It is the SHA-256 of that snapshot, encrypted to the customer's public key. The producer proves the snapshot exists and is bound to the verdict; only the customer can decrypt the snapshot itself. Honesty: a verifier confirms a ciphertext digest is committed in the signed verdict, not that the plaintext is a faithful capture of the compromised state (the verifier cannot decrypt it).

Verdict rule (severity-tiered)

  1. For each attack: blocked = true iff there exists a catch-record (in the referenced batch) whose normalized layer equals the attack's expected_layer and whose violation_type matches.
  2. verdict = fail iff there exists an attack with severity in policy.blocking_severities and blocked == false; otherwise verdict = pass.
  3. verdict_reason names the first blocking-tier bypass on a fail, or states that all blocking-tier classes were contained on a pass.

Per-attack results are ALWAYS recorded regardless of verdict. A miss in a non-blocking tier is recorded as evidence, not as a fail.

Verification (third-party, offline)

After the envelope checks in signing-envelope.md succeed (signature, keyid, predicateType, subject non-empty):

  1. Confirm the subject digest equals the artifact the verifier intends to admit.
  2. Validate every attack_results[].rule_id is namespaced, and every expected_layer / actual_layer (when non-null) is a value in the vocabulary registry.
  3. Resolve the catch-records by evidence_ref.bundle_id, verify them per catch-record.md, recompute the RFC 6962 batch root, and confirm it equals evidence_ref.batch_root. Confirm that every blocked: true is backed by a matching catch-record.
  4. Apply the auditor-supplied value-binding anchors (below).
  5. Apply the substrate revocation check (below) before trusting a pass.

No live execution environment is required.

Value-binding anchors (auditor-supplied)

A signature proves the bytes were not tampered with; it does not by itself stop two residual forgeries. An auditor (who alone knows the approved reference values) supplies two external anchors, checked alongside the verdict:

  • Exhaustion - the auditor supplies the COMPLETE rule_id set of the approved corpus. The verdict's attack_results MUST cover exactly that set. A verdict that silently omits attacks (a short attack_results) is rejected, defeating a skip-the-hard-attacks forgery. This is the out-of-band check that backs the honesty caveat on corpus_digest: exhaustion is established against the auditor's anchor, not asserted by the verdict alone.

  • Substrate freshness - the auditor supplies the approved substrate digest. substrate_ref.digest MUST equal it. A valid signature over a real-but-stale substrate is rejected.

These anchors are independently verifiable, but only relative to values the auditor brings; a verifier with no auditor anchors can still check the signature, the subject binding, and the evidence, but cannot establish exhaustion or freshness from the verdict alone.

Substrate revocation (OpenVEX)

A cryptographic signature never expires, but the substrate a verdict attests can be retroactively broken: a vulnerability disclosed AFTER the verdict was signed can turn a once-valid pass into a stale one. Before trusting a pass, a verifier MUST consult an OpenVEX revocation log for the substrate_ref digest. If the log carries an affected or under_investigation statement against that digest, the verifier MUST treat the pass as revoked regardless of signature validity. This closes the time-of-check / time-of-use gap on the substrate.

In v1 the revocation log MAY be a single static published document the verifier fetches and checks against the substrate_ref digest. An automated feed that opens a VEX statement when a substrate-relevant vulnerability is disclosed is roadmap, not v1.

Related standards

  • SLSA Verification Summary Attestation (VSA) - a security-verdict plays a role analogous to a VSA (a signed statement that a verification ran and what it concluded). gate_engine_commit is modeled on the VSA builder.id idea: an auditor-pinnable identity for the producer of the verdict. A security-verdict diverges from a VSA in carrying adversarial per-attack results and a forensic-evidence reference rather than a policy-evaluation summary. A consumer that wants a VSA-shaped summary can derive one from a security-verdict.
  • OpenVEX - used for substrate revocation, as above.
  • in-toto / DSSE
  • catch-record - the referenced forensic evidence.

Versioning

This is the security-verdict v1 predicate. Adding a new optional field is additive and stays within v1; a verifier ignores fields it does not recognize. A change to the verdict rule, a rename or removal of a field, or a change to the attack_results shape is breaking and requires a new major version. The policy ruleset is versioned independently via policy.id.

Get access

Run your agents through Probity.

A few details about your setup and we'll get you gating agents fast.