# Remediation Module Architecture

Technical architecture for the Remediation module of the Stella Ops release control plane — a developer-facing marketplace for signed-PR fix templates that remediate known vulnerabilities. Audience: backend engineers working on the remediation API, persistence, and verification pipeline, plus reviewers tracking which parts of the design are live versus planned.

Status: Partially implemented. Persistence-backed CRUD-style APIs exist for templates, submissions, contributors, and marketplace sources (Postgres-only, tenant-scoped, auditable). What is real and what is still missing, verified against src/Remediation on 2026-07-12:

  • Authorization is enforced (SPRINT W1h). The three policies are real scope policies over remediation:read / remediation:submit / remediation:manage — the old RequireAssertion(_ => true) placeholders are gone. The three scope values are now catalogued and seeded (SPRINT_20260712_002 / AUTH-2): they are StellaOpsScopes.RemediationRead / RemediationSubmit / RemediationManage, and S038_remediation_and_scanner_local_scopes.sql seeds the matching authority.permissions rows, so the fail-closed 403 lockout described here previously is closed — see Authorization Policies.
  • A real proof validator exists (SPRINT W1h-2). ScannerRemediationProofValidator verifies the ReachGraph reachability-delta and the Attestor fix-chain DSSE envelope. A signature-valid Attestor envelope is accepted only when its payload is an in-toto Statement/v1 with the exact FixChain predicate type, the requested CVE, a fixed verdict, and reachability proof that a positive pre-patch path count fell to zero. It is wired only when both Remediation:ProofValidation:ReachGraphBaseUrl and :AttestorBaseUrl are configured, otherwise the host degrades to the fail-closed UnavailableRemediationProofValidator and logs a startup WARNING.
  • Still not wired: the verification pipeline is not orchestrated — nothing populates the scan/reachability/DSSE digests, advances Status, or signs envelopes; RemediationVerifier only evaluates digests already present on a submission.
  • No compose host. The module is not deployed in devops/compose, so the scope-catalog lockout above is latent rather than live.

Overview

The Remediation module provides a developer-facing signed-PR remediation marketplace for the Stella Ops platform. It enables developers to discover, apply, and verify community-contributed or vendor-supplied fix templates for known vulnerabilities (CVEs).

Key Concepts

Fix Templates

Structured remediation patches tied to specific CVE + PURL combinations. Templates include unified diff content, version range applicability, and trust scores from contributor history.

PR Submissions

Tracks the lifecycle of a remediation pull request from submission through scanning, merging, and post-merge verification. Each submission produces attestation evidence including reachability deltas and fix-chain DSSE envelopes.

Contributors

Community members or vendors who submit fix templates. Each contributor has a trust score computed from their verification history (verified fixes, rejections).

Marketplace Sources

Curated collections of fix templates from community, partner, or vendor origins. Sources are rated independently and can be enabled or disabled per tenant.

Domain Model

FixTemplate (remediation.fix_templates)
|- TenantId (text, NOT NULL - tenant isolation key)
|- CveId (text, indexed)
|- Purl (text, indexed - pkg:type/name)
|- VersionRange (semver range)
|- PatchContent (unified diff)
|- Status (pending/verified/rejected)
|- TrustScore (0.0-1.0)
|- DsseDigest (nullable - signed envelope hash)
`- ContributorId / SourceId (foreign keys)

PrSubmission (remediation.pr_submissions)
|- TenantId (text, NOT NULL - tenant isolation key)
|- FixTemplateId (nullable FK -> fix_templates.id)
|- PrUrl, RepositoryUrl, SourceBranch, TargetBranch
|- CveId (text, indexed)
|- Status (text, default 'opened'; free-text column, indexed)
|- PreScanDigest, PostScanDigest
|- ReachabilityDeltaDigest, FixChainDsseDigest
|- Verdict (text, nullable)
|- ContributorId
`- CreatedAt, MergedAt, VerifiedAt

Contributor (remediation.contributors)
|- TenantId (text, NOT NULL - tenant isolation key)
|- Username (unique PER TENANT - ux_contributors_tenant_username)
|- VerifiedFixes, TotalSubmissions, RejectedSubmissions
`- TrustScore (computed)

MarketplaceSource (remediation.marketplace_sources)
|- Key (TEXT, UNIQUE - stored as a `<tenant>::<key>` composite for tenant isolation)
|- Name, Url (nullable)
|- SourceType (community/partner/vendor)
|- Enabled, TrustScore
`- CreatedAt, LastSyncAt

Note on status/verdict columns: Status and Verdict are free-text TEXT columns in PostgreSQL, not DB-enforced enums. The values listed elsewhere in this doc reflect the strings the current code produces or consumes; the schema does not constrain them. The RemediationVerifier only emits fixed, not_fixed, or inconclusive — a partial verdict is not produced by the current verifier (see Verification Pipeline below).

Tenancy contract (claim-bound)

Tenant identity is the isolation key on every read and write, and it is resolved exclusively from the authenticated stellaops:tenant claim (via IStellaOpsTenantAccessor / the shared StellaOpsTenantResolver). It is never read from a query parameter, request body, or caller-supplied header.

TableIsolation mechanism
fix_templatestenant_id column; every repository read carries a tenant_id = @tenant predicate
pr_submissionstenant_id column; reads and UpdateStatusAsync are tenant-predicated
contributorstenant_id column; username is unique per tenant (ux_contributors_tenant_username), not globally
marketplace_sources<tenant>::<key> composite key (delimiter ::), stripped on read — no tenant_id column

Enforcement details:

History (TEN-4, sprint 20260712_001): fix_templates, pr_submissions and contributors previously carried no tenant column and their repositories issued no tenant predicate, so any principal holding remediation:read could read every tenant’s fix templates, PR submissions (repository URLs, branch names, CVE ids) and contributor records. Migration 002_tenant_scoped_registry.sql adds tenant_id (backfilled to default) and scopes contributor-username uniqueness to the tenant. Proven by RemediationTenantIsolationTests (Postgres-backed) and RemediationTenancyEndpointTests (HTTP).

Trust Scoring

Contributor trust score formula:

score = clamp((verified * 1.0 - rejected * 0.5) / max(total, 1), 0, 1)

Trust tiers:

API Surface

All endpoints are under /api/v1/remediation/ and require tenant context (RequireTenant()). All routes are lowercased (LowercaseUrls = true). List endpoints accept limit and offset query parameters: limit defaults to 50, is clamped to the range [1, 200]; offset defaults to 0 and is clamped to >= 0. The Count field in list responses is the number of items on the returned page, not a total row count.

Templates

Submissions

Matching

Contributors

Implemented contributor API contract (2026-04-24):

Sources

Implemented source API contract (2026-03-04):

Authorization Policies

Each endpoint declares a named authorization policy via RequireAuthorization("..."). The ASP.NET policy name is dot-form; the scope claim value it requires is colon-form:

Policy nameRequired scope claimEndpoints
remediation.readremediation:readAll GET endpoints (templates, submissions, contributors, sources, matches)
remediation.submitremediation:submitPOST /templates, POST /submissions
remediation.manageremediation:managePOST /sources

Enforcement is real (SPRINT W1h). The earlier policy.RequireAssertion(_ => true) placeholders are gone. Program.cs:40 wires AddStellaOpsResourceServerAuthentication outside Testing (JWT bearer + bypass evaluator + scope handler), and Program.cs:57-59 registers the three policies with AddStellaOpsScopePolicy(<policy>, <scope>), which installs a real StellaOpsScopeRequirement. Endpoints enforce them (RemediationRegistryEndpoints.cs, RemediationSourceEndpoints.cs, RemediationMatchEndpoints.cs) in addition to RequireTenant().

✅ Scope-catalog lockout CLOSED (SPRINT_20260712_002 / AUTH-2, 2026-07-12). The three claims are now first-class catalog constants — StellaOpsScopes.RemediationRead / RemediationSubmit / RemediationManage — and Program.cs binds the policies to those constants rather than to magic strings.

This mattered more than it looked. StellaOpsScopes.All/KnownScopes are built by reflecting over the public string consts, and Authority calls options.RegisterScopes(…StellaOpsScopes.All) (StellaOps.Authority/Program.cs). An uncatalogued claim is therefore never registered with OpenIddict, so it could not be minted into a token by any path — not via the Console role editor, not via a config-defined role (AuthorityTenantRoleOptions.Validate throws on !IsKnown), not even via a hand-written authority.role_permissions row. Every Remediation endpoint would have returned 403 for every caller.

S038_remediation_and_scanner_local_scopes.sql seeds the matching authority.permissions rows (so the Console role editor offers them) and grants the trio to the admin role so the module is operable on an existing install. Proven by S038RemediationAndScannerScopeCatalogTests (a role assembled through the Console/RBAC path carries all three scopes) and StellaOpsScopesTests.Auth2_SingleScopePolicyClaims_AreRegistered.

Runtime Storage Contract (updated 2026-04-24)

Remediation runtime storage is now selected through Remediation:Storage:Driver (or Storage:Driver) with explicit startup validation:

Postgres repository classes are Postgres-only. They require RemediationDataSource; parameterless constructors and silent process-local repository state are not supported in production assemblies.

Migration notes:

Service Integration Baseline (2026-03-05)

Verification Pipeline

Design / not orchestrated. Steps 1-6 below describe the intended end-to-end flow, but there is no orchestration code in the Remediation module that drives it. Nothing populates pre_scan_digest, post_scan_digest, reachability_delta_digest, or fix_chain_dsse_digest automatically, advances Status, or signs envelopes. IRemediationRegistry.UpdateSubmissionStatusAsync(...) exists but is not exposed by any endpoint. What is implemented is the verdict half: RemediationVerifier.VerifyAsync(submission) evaluates digests already present on a submission (step 7), and — when configured — ScannerRemediationProofValidator really does fetch and cryptographically verify the ReachGraph delta and the Attestor fix-chain DSSE envelope behind it (see Proof-validation contract). The pipeline that scans, merges, and stamps those digests is still future scope.

  1. PR submitted (status: opened)
  2. Pre-merge scan captures baseline SBOM digest
  3. PR merged (status: merged)
  4. Post-merge scan captures updated SBOM digest
  5. Reachability delta computed between pre and post digests
  6. Fix-chain DSSE envelope signed
  7. Verdict determined by RemediationVerifier: fixed, not_fixed, or inconclusive. (The verifier never emits partial; that value appears only in older design notes.)

Proof-validation contract (updated 2026-04-28)

RemediationVerifier does not treat changed pre/post scan digests as proof that a vulnerability was fixed. A changed scan pair is only eligible for fixed after all of the following hold:

Which validator is registered is config-driven (RegisterProofValidation, Program.cs:210-329):

Missing or malformed proof material also returns inconclusive with stable diagnostic keys. The full set of diagnostic keys (RemediationProofDiagnostics) is:

Evaluation order in RemediationVerifier.DetermineVerdictAsync: missing or malformed pre/post scan digests -> inconclusive; pre/post digests equal (case-insensitive) -> not_fixed (short-circuit, before reachability/DSSE checks); otherwise missing/malformed reachability-delta or fix-chain DSSE digest -> inconclusive; only when all four digests are well-formed sha256:<64 hex> is the candidate verdict fixed, at which point IRemediationProofValidator.ValidateAsync(...) runs and a non-validated result downgrades the verdict back to inconclusive.

Matching pre/post scan digests may still return not_fixed, because that is a negative outcome and does not claim remediation proof success.

Webhook Integration

The RemediationPrWebhookHandler (src/Signals/StellaOps.Signals/Services/RemediationPrWebhookHandler.cs) in the Signals module exposes two pure helpers:

Detection only. This handler does not create submissions or call back into the Remediation module — it only classifies a PR title/labels and pulls out a CVE id. There is no wiring from Signals to POST /api/v1/remediation/submissions. The RepositoryUrl/SourceBranch/TargetBranch example title convention fix(CVE-XXXX-NNNNN): description is the recognized format.

Audit Emission

Three state-changing endpoints emit audit events through StellaOps.Audit.Emission (.Audited(...)), under audit module remediation (AuditModules.Remediation):

EndpointAction constantAction valueEntity
POST /templatesAuditActions.Remediation.CreateFixTemplatecreate_fix_templatefix_template
POST /submissionsAuditActions.Remediation.CreatePrSubmissioncreate_pr_submissionpr_submission
POST /sourcesAuditActions.Remediation.UpsertMarketplaceSourceupsert_marketplace_sourcemarketplace_source

Audit emission is wired in Program.cs via AddAuditEmission(builder.Configuration). Read (GET) and match endpoints are not audited.

Module Location

src/Remediation/
|- StellaOps.Remediation.Core/                  - Domain models, interfaces, services (verifier, trust scorer, proof validator)
|- StellaOps.Remediation.WebService/            - API endpoints, Program.cs, contract records
|- StellaOps.Remediation.Persistence/           - EF Core DbContext, Postgres repositories, embedded SQL migrations
|- __Tests/StellaOps.Remediation.Tests/         - Repository/domain unit tests (Postgres fixture-backed)
`- __Tests/StellaOps.Remediation.WebService.Tests/ - Endpoint, audit-emission, and startup-contract tests

Persistence is EF Core (RemediationDbContext) over Npgsql. Embedded migrations are applied at startup via AddStartupMigrations(schemaName, "Remediation", typeof(RemediationDataSource).Assembly), in filename order:

MigrationPurpose
Migrations/001_remediation_registry_schema.sqlBase schema: fix_templates, pr_submissions, contributors, marketplace_sources.
Migrations/002_tenant_scoped_registry.sqlTEN-4: adds tenant_id to the first three tables (backfilled to default), replaces the global UNIQUE(username) on contributors with a per-tenant unique index, and adds tenant-leading composite indexes.

Repositories (PostgresFixTemplateRepository, PostgresPrSubmissionRepository, PostgresMarketplaceSourceRepository, PostgresContributorRepository) are all Postgres-only, require RemediationDataSource, and take the tenant as a required first parameter (see the tenancy contract above).

Advisory Gap Status (2026-03-04 Batch)

Status:

Closed behaviors:

Verification evidence:

Tracking sprint: