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/v1in-toto predicate type or DSSE statement builder in the codebase. TheRemediationVerifier(src/Remediation/StellaOps.Remediation.Core/Services/RemediationVerifier.cs) produces a plainVerificationResultrecord — it does not emit a signed in-toto Statement. The fields below correspond to the persistedremediation.pr_submissionsrow (PrSubmission/PrSubmissionEntity) plus the verifier output; the in-toto wrapper, subject, andpredicateTypeare 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.sql → remediation.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).
| Field | Type | Required | Source | Description |
|---|---|---|---|---|
id | string (UUID) | yes | PrSubmission.Id / pr_submissions.id | Submission record identifier |
cveId | string | yes | PrSubmission.CveId / cve_id | The CVE identifier being remediated |
prUrl | string | yes | PrSubmission.PrUrl / pr_url | URL of the pull request |
repositoryUrl | string | yes | PrSubmission.RepositoryUrl / repository_url | URL of the target repository |
sourceBranch | string | yes | PrSubmission.SourceBranch / source_branch | Source branch of the PR |
targetBranch | string | yes | PrSubmission.TargetBranch / target_branch | Target branch of the PR |
fixTemplateId | string (UUID) | no | PrSubmission.FixTemplateId / fix_template_id | ID of the fix template used, if any (FK to remediation.fix_templates) |
status | string | yes | PrSubmission.Status / status | Lifecycle status; defaults to opened (also merged; see Verification Pipeline) |
preScanDigest | string | no | PrSubmission.PreScanDigest / pre_scan_digest | SHA-256 digest of the pre-merge SBOM scan (sha256:<64 hex>) |
postScanDigest | string | no | PrSubmission.PostScanDigest / post_scan_digest | SHA-256 digest of the post-merge SBOM scan (sha256:<64 hex>) |
reachabilityDeltaDigest | string | no | PrSubmission.ReachabilityDeltaDigest / reachability_delta_digest | SHA-256 digest of the reachability delta report (sha256:<64 hex>) |
fixChainDsseDigest | string | no | PrSubmission.FixChainDsseDigest / fix_chain_dsse_digest | SHA-256 digest of the signed fix-chain DSSE envelope (sha256:<64 hex>) |
verdict | string | no | PrSubmission.Verdict / verdict | Verification outcome (nullable until verified): fixed, not_fixed, inconclusive |
contributorId | string (UUID) | no | PrSubmission.ContributorId / contributor_id | ID of the contributor who submitted the fix |
createdAt | string (ISO 8601) | yes | PrSubmission.CreatedAt / created_at | Submission creation timestamp |
mergedAt | string (ISO 8601) | no | PrSubmission.MergedAt / merged_at | Timestamp the PR was merged |
verifiedAt | string (ISO 8601) | no | PrSubmission.VerifiedAt / verified_at | Timestamp of verification completion (nullable until verified) |
affectedPaths | string[] | no | VerificationResult.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
contributorTrustScoreas a predicate field. It does not exist onPrSubmission,PrSubmissionEntity, or thepr_submissionstable. Contributor trust score lives only on theContributormodel /remediation.contributorstable and is exposed through the contributor endpoints (ContributorResponse.TrustScore).The verdict value
partialwas also listed previously; theRemediationVerifieronly ever emitsfixed,not_fixed, orinconclusive(constantsVerdictFixed/VerdictNotFixed/VerdictInconclusive). Thepartialstatus 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; thepredicatebody mirrors the persistedpr_submissionsrow plus verify-timeaffectedPaths.
{
"_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:
- If
preScanDigestorpostScanDigestis missing →inconclusive(remediation.proof.scan_digest_missing). - If either scan digest is not a well-formed
sha256:<64 hex>value →inconclusive(remediation.proof.scan_digest_malformed). - If
preScanDigest == postScanDigest→not_fixed(no remediation claimed). - If
reachabilityDeltaDigestis missing/malformed →inconclusive(remediation.proof.reachability_delta_digest_*). - If
fixChainDsseDigestis missing/malformed →inconclusive(remediation.proof.fix_chain_dsse_digest_*). - Otherwise the candidate verdict is
fixed, which is then handed to the configuredIRemediationProofValidator. The verdict is downgraded toinconclusiveunless the validator returns a valid result; on success the validator suppliesaffectedPaths.
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():
| Method | Route | Authorization policy | Notes |
|---|---|---|---|
| GET | /api/v1/remediation/submissions | remediation.read | List submissions (?cve, ?status, ?limit, ?offset) |
| GET | /api/v1/remediation/submissions/{id:guid} | remediation.read | Submission detail |
| POST | /api/v1/remediation/submissions | remediation.submit | Create submission from a PR (audited) |
| GET | /api/v1/remediation/submissions/{id:guid}/status | remediation.read | { id, status, verdict } |
| GET | /api/v1/remediation/templates | remediation.read | List fix templates |
| GET | /api/v1/remediation/templates/{id:guid} | remediation.read | Fix template detail |
| POST | /api/v1/remediation/templates | remediation.submit | Create fix template (audited) |
| GET | /api/v1/remediation/contributors | remediation.read | List contributors with trust score |
| GET | /api/v1/remediation/contributors/{username} | remediation.read | Contributor profile |
Authorization is a local stub. The
remediation.readandremediation.submitpolicies are registered inStellaOps.Remediation.WebService/Program.csaspolicy.RequireAssertion(_ => true)— they are not Authority scopes and do not appear inStellaOps.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.
