Remediation PR Predicate Schema v1

Stella Ops records the outcome of verifying a remediation pull request — did a specific PR actually fix the targeted CVE? — as evidence in the release control plane. This contract describes the fields of that remediation record and the forward-looking remediation-pr/v1 in-toto predicate that will attest it.

Audience: engineers building the Remediation Registry, the verification pipeline, or consumers that read remediation evidence. Read the status note below first: the predicate is a proposed shape, not yet emitted by code.

Status: PROPOSED / NOT YET IMPLEMENTED as an attestation predicate. As of the current implementation there is no remediation-pr/v1 in-toto predicate type or DSSE statement builder in the codebase. The RemediationVerifier (src/Remediation/StellaOps.Remediation.Core/Services/RemediationVerifier.cs) produces a plain VerificationResult record — it does not emit a signed in-toto Statement. The fields below correspond to the persisted remediation.pr_submissions row (PrSubmission / PrSubmissionEntity) plus the verifier output; the in-toto wrapper, subject, and predicateType are a forward design for when these records are attested. The only DSSE envelope referenced by a PR submission today is the separate fix-chain attestation (see Envelope).

Predicate Type

https://stellaops.io/predicates/remediation-pr/v1 (proposed; not registered in StellaOps.Signer.Core.PredicateTypes and not in PredicateTypes.GetAllowedPredicateTypes()).

Purpose

Records the verification outcome of a remediation pull request, including scan delta evidence, reachability impact, and the signed fix-chain envelope. The intent is for this predicate to be produced at the end of the verification pipeline and attest that a specific PR either did or did not remediate the targeted CVE. Today the same data is stored on the remediation.pr_submissions table and surfaced via the Remediation Registry API; it is not yet wrapped in a signed predicate.

Subject

The subject (proposed) is the PR submission record, identified by its UUID:

{
  "subject": [
    {
      "name": "pr-submission",
      "digest": {
        "sha256": "<submission-record-digest>"
      }
    }
  ]
}

Predicate Fields

The columns below are grounded in the persisted record (src/Remediation/StellaOps.Remediation.Persistence/Migrations/001_remediation_registry_schema.sqlremediation.pr_submissions; PrSubmission in StellaOps.Remediation.Core/Models/PrSubmission.cs) and, where noted, in the verifier output (VerificationResult in StellaOps.Remediation.Core/Services/IRemediationVerifier.cs).

FieldTypeRequiredSourceDescription
idstring (UUID)yesPrSubmission.Id / pr_submissions.idSubmission record identifier
cveIdstringyesPrSubmission.CveId / cve_idThe CVE identifier being remediated
prUrlstringyesPrSubmission.PrUrl / pr_urlURL of the pull request
repositoryUrlstringyesPrSubmission.RepositoryUrl / repository_urlURL of the target repository
sourceBranchstringyesPrSubmission.SourceBranch / source_branchSource branch of the PR
targetBranchstringyesPrSubmission.TargetBranch / target_branchTarget branch of the PR
fixTemplateIdstring (UUID)noPrSubmission.FixTemplateId / fix_template_idID of the fix template used, if any (FK to remediation.fix_templates)
statusstringyesPrSubmission.Status / statusLifecycle status; defaults to opened (also merged; see Verification Pipeline)
preScanDigeststringnoPrSubmission.PreScanDigest / pre_scan_digestSHA-256 digest of the pre-merge SBOM scan (sha256:<64 hex>)
postScanDigeststringnoPrSubmission.PostScanDigest / post_scan_digestSHA-256 digest of the post-merge SBOM scan (sha256:<64 hex>)
reachabilityDeltaDigeststringnoPrSubmission.ReachabilityDeltaDigest / reachability_delta_digestSHA-256 digest of the reachability delta report (sha256:<64 hex>)
fixChainDsseDigeststringnoPrSubmission.FixChainDsseDigest / fix_chain_dsse_digestSHA-256 digest of the signed fix-chain DSSE envelope (sha256:<64 hex>)
verdictstringnoPrSubmission.Verdict / verdictVerification outcome (nullable until verified): fixed, not_fixed, inconclusive
contributorIdstring (UUID)noPrSubmission.ContributorId / contributor_idID of the contributor who submitted the fix
createdAtstring (ISO 8601)yesPrSubmission.CreatedAt / created_atSubmission creation timestamp
mergedAtstring (ISO 8601)noPrSubmission.MergedAt / merged_atTimestamp the PR was merged
verifiedAtstring (ISO 8601)noPrSubmission.VerifiedAt / verified_atTimestamp of verification completion (nullable until verified)
affectedPathsstring[]noVerificationResult.AffectedPaths (verify-time only)Call graph paths affected by the fix; not persisted on the submission — computed by the proof validator at verification. The default production validator (UnavailableRemediationProofValidator) returns an empty list.

Removed fields. Earlier drafts listed contributorTrustScore as a predicate field. It does not exist on PrSubmission, PrSubmissionEntity, or the pr_submissions table. Contributor trust score lives only on the Contributor model / remediation.contributors table and is exposed through the contributor endpoints (ContributorResponse.TrustScore).

The verdict value partial was also listed previously; the RemediationVerifier only ever emits fixed, not_fixed, or inconclusive (constants VerdictFixed / VerdictNotFixed / VerdictInconclusive). The partial status exists only on the separate fix-chain predicate (FixChainVerdict.StatusPartial), not on the remediation verdict.

Example

Proposed envelope shape. The in-toto wrapper (_type, subject, predicateType) is not produced by the current code; the predicate body mirrors the persisted pr_submissions row plus verify-time affectedPaths.

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "pr-submission",
      "digest": { "sha256": "abc123..." }
    }
  ],
  "predicateType": "https://stellaops.io/predicates/remediation-pr/v1",
  "predicate": {
    "id": "9f8e7d6c-...",
    "cveId": "CVE-2024-1234",
    "prUrl": "https://github.com/org/repo/pull/42",
    "repositoryUrl": "https://github.com/org/repo",
    "sourceBranch": "fix/CVE-2024-1234",
    "targetBranch": "main",
    "fixTemplateId": "a1b2c3d4-...",
    "status": "merged",
    "preScanDigest": "sha256:aaa...",
    "postScanDigest": "sha256:bbb...",
    "reachabilityDeltaDigest": "sha256:ccc...",
    "fixChainDsseDigest": "sha256:ddd...",
    "verdict": "fixed",
    "affectedPaths": [
      "com.example.App -> org.vuln.Lib.method()"
    ],
    "contributorId": "e5f6g7h8-...",
    "createdAt": "2026-02-20T13:00:00Z",
    "mergedAt": "2026-02-20T14:00:00Z",
    "verifiedAt": "2026-02-20T14:30:00Z"
  }
}

Verification Pipeline

RemediationVerifier.VerifyAsync (StellaOps.Remediation.Core/Services/RemediationVerifier.cs) determines the verdict as follows:

  1. If preScanDigest or postScanDigest is missing → inconclusive (remediation.proof.scan_digest_missing).
  2. If either scan digest is not a well-formed sha256:<64 hex> value → inconclusive (remediation.proof.scan_digest_malformed).
  3. If preScanDigest == postScanDigestnot_fixed (no remediation claimed).
  4. If reachabilityDeltaDigest is missing/malformed → inconclusive (remediation.proof.reachability_delta_digest_*).
  5. If fixChainDsseDigest is missing/malformed → inconclusive (remediation.proof.fix_chain_dsse_digest_*).
  6. Otherwise the candidate verdict is fixed, which is then handed to the configured IRemediationProofValidator. The verdict is downgraded to inconclusiveunless the validator returns a valid result; on success the validator supplies affectedPaths.

The default production registration is UnavailableRemediationProofValidator, which always returns inconclusive with diagnostic remediation.proof_validation.unavailable (fail-closed until a real validator is wired to Scanner/ReachGraph/Attestor evidence).

API

Exposed by RemediationRegistryEndpoints (StellaOps.Remediation.WebService/Endpoints/RemediationRegistryEndpoints.cs), all under RequireTenant():

MethodRouteAuthorization policyNotes
GET/api/v1/remediation/submissionsremediation.readList submissions (?cve, ?status, ?limit, ?offset)
GET/api/v1/remediation/submissions/{id:guid}remediation.readSubmission detail
POST/api/v1/remediation/submissionsremediation.submitCreate submission from a PR (audited)
GET/api/v1/remediation/submissions/{id:guid}/statusremediation.read{ id, status, verdict }
GET/api/v1/remediation/templatesremediation.readList fix templates
GET/api/v1/remediation/templates/{id:guid}remediation.readFix template detail
POST/api/v1/remediation/templatesremediation.submitCreate fix template (audited)
GET/api/v1/remediation/contributorsremediation.readList contributors with trust score
GET/api/v1/remediation/contributors/{username}remediation.readContributor profile

Authorization is a local stub. The remediation.read and remediation.submit policies are registered in StellaOps.Remediation.WebService/Program.cs as policy.RequireAssertion(_ => true) — they are not Authority scopes and do not appear in StellaOps.Auth.Abstractions.StellaOpsScopes. There is no dedicated remediation scope in the canonical catalog at this time.

POST /api/v1/remediation/submissions accepts a CreatePrSubmissionRequest (prUrl, repositoryUrl, sourceBranch, targetBranch, cveId, fixTemplateId?); the server initializes status = "opened". Scan digests, verdict, and the fix-chain DSSE digest are populated later by the verification pipeline, not at creation.

Envelope

The fixChainDsseDigest on the PrSubmission record stores the SHA-256 digest of the fix-chain DSSE envelope — a separate attestation defined by StellaOps.Attestor.FixChain.FixChainPredicate, predicate type https://stella-ops.org/predicates/fix-chain/v1, built by FixChainStatementBuilder. That predicate carries cveId, component, vulnerableBinary/patchedBinary refs, signatureDiff, reachability, and its own verdict (which does include a partial status). The remediation-pr/v1 predicate described here is not itself signed or wrapped in a DSSE envelope by current code.