Policy Engine Architecture (v2)

Audience: Policy Engine service owners, platform engineers, and integrators wiring CI/CD release gates. Ownership: Policy Guild • Platform Guild
Services: StellaOps.Policy.Engine (Minimal API + worker host)
Data Stores: PostgreSQL (policy.* schemas for packs, runs, exceptions, receipts), Object storage (explain bundles), optional queue
Related docs: Policy overview, DSL, SPL v1, Lifecycle, Runtime, Governance, REST API, Policy CLI, Architecture overview, AOC reference, AI Code Guard policy Origin: Epic 2 (Policy Engine & Policy Editor) and Epic 4 (Policy Studio).

This dossier describes the internal structure of the Policy Engine service delivered in Epic 2. It focuses on module boundaries, deterministic evaluation, orchestration, and integration contracts with Concelier, Excititor, SBOM Service, Authority, Scheduler, and Observability stacks.

Tenant identity (envelope-bound, 2026-04-30). Policy Engine endpoints resolve tenant identity exclusively from the envelope-attached stellaops:tenant claim via IStellaOpsTenantAccessor. The legacy X-Tenant-Id header is stripped at ingress and is never honoured as a tenancy source. Literal "default" tenant fallbacks have been removed from AirGap/PostgresAirGapStateStores.cs, Audit/PolicyAuditBeforeStateProvider.cs, Audit/PolicyAuditResourceEnricher.cs, ConsoleExport/PostgresConsoleExportStores.cs, Endpoints/AirGapNotificationEndpoints.cs, Endpoints/DeterminizationConfigEndpoints.cs, Endpoints/SealedModeEndpoints.cs, Endpoints/StalenessEndpoints.cs, and the affected gateway endpoints; requests without a resolved tenant fail closed with HTTP 401. See SPRINT_20260430_007_MultiTenant_identity_sweep (TENANT-POLICY-01).

The service operates strictly downstream of the Aggregation-Only Contract (AOC). It consumes immutable advisory_raw and vex_raw documents emitted by Concelier and Excititor, derives findings inside Policy-owned collections, and never mutates ingestion stores. Refer to the architecture overview and AOC reference for system-wide guardrails and provenance obligations.

Exception counts and filtered reads (2026-09-09). GET /api/policy/exceptions/counts and the ordinary exception list require policy:read and the authenticated request tenant. They preserve that tenant as text, including non-UUID identifiers, in both the database session and an explicit tenant_id predicate. Missing tenant identity is refused; a forged raw header cannot select another tenant for an ordinary principal. Counts are tenant-wide lifecycle totals, independent of the list page size. active counts rows whose lifecycle status is active; expiringSoon counts active rows due within seven days, including overdue active rows.

The read contract uses a required text tenant for GetCountsAsync and a text tenant on ExceptionFilter. This correction does not migrate the separate ExceptionScope write model: its nullable UUID tenant member still cannot represent a non-UUID identity. Reading an existing text-tenant row therefore leaves that member null rather than inventing a UUID. Counts and read isolation do not depend on that member; broader exception CRUD tenancy remains separate work.


1 · Responsibilities & Constraints

Non-goals: policy authoring UI (handled by Console), ingestion or advisory normalisation (Concelier), VEX consensus (Excititor), runtime enforcement (Zastava).

1.1 · Localization runtime contract (Sprint 20260224_002)

1.2 · Simulation compatibility contract (Sprint 20260309_011)

1.3 · Durable pack lifecycle versus Registry governance

Verified against source 00f64ca4d165fc54e7c291795b65326869b7627d (2026-08-31): Engine Program.cs registers IPolicyPackRepository as PostgresPolicyPackRepository and calls MapPolicyPacks(). The registered operational sequence is explicit Draft metadata, bundle compilation/storage, explicit Approved metadata upsert, then activation. These operations use /api/policy/packs, policy:edit for metadata/bundles, policy:read for listing/evaluation, and policy:activate for activation. Revision states are only Draft, Approved and Active.

The pack group’s tenant endpoint filter bridges the validated shared Router identity into Policy’s request-local accessor and clears it after dispatch. Native HTTP uses the same identity contract through middleware. Repository operations require that context before opening a connection and explicitly filter pack queries by its tenant; no implicit public selection is permitted. Verify this boundary with PolicyPackRouterTenantContextTests, including the database-owner/RLS-bypass case and the registered :activate route.

Batch evaluation applies the same transport bridge and rejects a body tenant that differs from validated identity before invoking its evaluator. Both transports normalize only the exact operator project wildcard * to no project refinement. Pack evaluation captures the validated tenant once and uses a typed cache identity containing tenant, pack, version, bundle digest, subject and findings digest. Correlation IDs hash the canonical JSON of that identity; identical policy content cannot reuse another tenant’s attestation reference or another revision’s response. This changes future correlation IDs, not historical stored receipts.

Approved is metadata set by the edit-scoped endpoint, not a reviewer decision. Compilation does not approve a Draft. The two-person requirement is resolved from PolicyEngine:Activation:ForceTwoPersonApproval, the request and DefaultRequiresTwoPersonApproval when metadata is first created; existing revision upserts do not update it. Activation checks Approved state and distinct actor IDs when required, returning 202 while a second approval is pending. It does not enforce author/reviewer separation, shadow/coverage evidence or bundle presence, and does not enqueue a run or promote an environment. Metadata and bundle fields are updateable; preservation of the reviewed revision/digest is an operational obligation, not storage-enforced content immutability.

Registry submission/review/publication services are not registered by the Engine host. GET /policy/capabilities reports simulation and shadow availability (both source defaults false); the compatibility handlers remain 501. Required reviewer, shadow, determinism and publication evidence is not waived: hold the governed operation when the approved procedure cannot supply it. PolicyBundleService.Sign is a deterministic offline-testing signature stub, not a DSSE or operator-approval attestation; this is separate from post-evaluation verdict attestation.

Re-verify with rg -n 'PostgresPolicyPackRepository|MapPolicyPacks|MapPolicyCapabilitiesEndpoints|MapPolicySimulationEndpoints' src/Policy/StellaOps.Policy.Engine/Program.cs, then inspect the enclosing endpoint/repository methods and the lifecycle source checks. This source boundary does not claim any current deployment or policy has completed governance.


2 · High-Level Architecture

graph TD
    subgraph Clients
        CLI[stella CLI]
        UI[Console Policy Editor]
        CI[CI Pipelines]
    end
    subgraph PolicyEngine["StellaOps.Policy.Engine"]
        API[Minimal API Host]
        Orchestrator[Run Orchestrator]
        WorkerPool[Evaluation Workers]
        Compiler[DSL Compiler Cache]
        Materializer[Effective Findings Writer]
    end
    subgraph RawStores["Raw Stores (AOC)"]
        AdvisoryRaw[(PostgreSQL
advisory_raw)] VexRaw[(PostgreSQL
vex_raw)] end subgraph Derived["Derived Stores"] PG[(PostgreSQL `policy.*`
packs / pack_versions / evaluation_runs / explanations / cvss_receipts / ...)] Blob[(Object Store / Evidence Locker)] Queue[(PostgreSQL policy job state
orchestrator_jobs / worker_results)] Cache[(Signed messaging cache backend
Router-selected / standalone Postgres)] end Concelier[(Concelier APIs)] Excititor[(Excititor APIs)] SBOM[(SBOM Service)] Authority[(Authority / DPoP Gateway)] CLI --> API UI --> API CI --> API API --> Compiler API --> Orchestrator Orchestrator --> Queue Queue --> WorkerPool Concelier --> AdvisoryRaw Excititor --> VexRaw WorkerPool --> AdvisoryRaw WorkerPool --> VexRaw WorkerPool --> SBOM WorkerPool --> Materializer Materializer --> PG WorkerPool --> Blob API --> PG API --> Cache API --> Blob API --> Authority Orchestrator --> PG WorkerPool --> Cache Authority --> API

Key notes:

2.0a · Messaging-backend plugin decouple (c1922c1ce5)

The Policy Engine and Policy Gateway hosts no longer compile-time link the PostgreSQL messaging transport. Previously both Program.cs files ProjectReferenced StellaOps.Messaging.Transport.Postgres and hand-registered PostgresConnectionFactory / PostgresTransportOptions / IDistributedCacheFactory → PostgresCacheFactory from the host project graph (Gateway also carried the deferred Router.Transport.Messaging reference). As of c1922c1ce5 both references are dropped and the messaging cache transport is selected at runtime from a signed mounted bundle via AddMessagingPlugins(...) (StellaOps.Messaging.DependencyInjection, RequireTransport = true). The transport plugin registers its connection, queue, and cache factories.

OSK-P1R makes ownership explicit for the merged Policy Engine host. When Router is enabled, Policy reuses Router’s one signed messaging-backend registration; the canonical Compose profile selects base / Valkey. Policy does not mount or register a second transport. When Router is disabled, the standalone host mounts one Policy-owned transport and defaults it to Postgres. Startup fails before provider build unless exactly one unkeyed IDistributedCacheFactory is present, so the effective cache backend can no longer depend on service-registration order. These stores contain bounded evaluation and reachability cache entries only; authoritative Policy state and signing projections remain PostgreSQL-owned.

Current canonical Compose is Router-enabled and uses the admitted signed base / Valkey bundle. A signed recommended / Postgres bundle remains an operator-supplied prerequisite only for Router-disabled standalone deployment; it is not present in the current local mounted profile (verified 2026-07-19).

2.0b · Fail-closed messaging-plugin loader (c1922c1ce5)

Loading a transport bundle from a mounted directory is a supply-chain boundary, so the shared MessagingPluginLoader.LoadFromDirectory(...) (src/Router/__Libraries/StellaOps.Messaging/Plugins/MessagingPluginLoader.cs) is now fail-closed (it was previously fail-open). Every mounted bundle must clear the shared SignedRuntimePluginAdmission chain before its assembly is handed to the loader:

A bundle that fails admission registers zero transports. Enforcement also requires a configured trust root: with EnforceSignatureVerification = true and no TrustRootPath, the loader rejects rather than falling open. Because this is the shared loader, the same hardening closes the Gateway.WebService exemplar’s hole as well. EnforceSignatureVerification defaults to true (MessagingPluginOptions); image-resident transports discovered from the Default ALC are intentionally not re-gated here (they are signed at build time and ride the host image’s trust boundary — see docs/modules/router/architecture.md). Coverage: MessagingPluginLoaderAdmissionTests (6 reject paths admit zero transports) plus a live accept / wrong-trust-root forcing-function on the real signed Postgres bundle (StagedMessagingTransportBundleAdmissionTests).

The admission primitive itself (SignedRuntimePluginAdmission) was promoted into StellaOps.Plugin/Security as a parameterized cross-module chokepoint (module + capability + contract version are caller inputs) so a single hardened implementation serves messaging, AdvisoryAI, the SM crypto pack loader, and the BinaryIndex/ExportCenter/Doctor mounted loaders without per-module forks.


2.1 · AOC inputs & immutability


3 · Module Breakdown

Path note. Paths below are relative to src/Policy/StellaOps.Policy.Engine/ unless they name a sibling project (StellaOps.PolicyDsl/ is a direct sibling under src/Policy/; StellaOps.Policy.Determinization/ lives under src/Policy/__Libraries/) or a shared library (the StellaOps.Policy gates library is also under src/Policy/__Libraries/). The merged engine host (Program.cs) wires all of these plus the former standalone gateway endpoints under Endpoints/Gateway/.

ModuleResponsibilityNotes
Configuration (Options/)Bind PolicyEngine settings (Authority, Workers, ResourceServer, Compilation, Activation, Telemetry, Entropy, RiskProfile, caches, exception lifecycle) plus Postgres:Policy, AirGap, Determinization; validate on start (PolicyEngineOptions.Validate, ValidateOnStart).Strict schema; production guards fail fast on missing/insecure secrets.
Authority / resource server (Program.cs, DependencyInjection/)Acquire client-credentials tokens, enforce policy:* / effective:write scopes via AddStellaOpsResourceServerAuthentication + scope handler.Only service identity uses effective:write. Accepts aud=stellaops plus configured audiences.
DSL Compiler (StellaOps.PolicyDsl/ + Compilation/)Tokenize/parse stella-dsl@1 source (DslTokenizer, PolicyParser), build/serialize IR (PolicyIr, PolicyIrSerializer, PolicyCompiler); engine-side complexity/metadata analysis + caching (PolicyCompilationService, PolicyComplexityAnalyzer, PolicyMetadataExtractor).DSL lives in the sibling StellaOps.PolicyDsl project, not an engine subdir. Reload recompiles persisted source text.
Selection / join (SelectionJoin/, ReachabilityFacts/)SBOM ↔ advisory ↔ VEX joiners; reachability-facts joining (ReachabilityFactsJoiningService); apply equivalence tables; support incremental cursors.Deterministic ordering (SBOM → advisory → VEX).
Evaluator (Evaluation/)Execute IR with first-match semantics, compute severity/trust/reachability weights, record rule hits, and emit a unified confidence score with factor breakdown (reachability/runtime/VEX/provenance/policy).Stateless; all inputs provided by selection layer.
Signals (Signals/)Normalizes reachability, trust, entropy, uncertainty, runtime hits into a single dictionary passed to Evaluator; supplies default unknown values when signals missing. Entropy penalties are derived from Scanner layer_summary.json/entropy.report.json (K=0.5, cap=0.3, block at image opaque ratio > 0.15 w/ unknown provenance) and exported via policy_entropy_penalty_value / policy_entropy_image_opaque_ratio; SPL scope entropy.* exposes penalty, image_opaque_ratio, blocked, warned, capped, top_file_opaque_ratio.Aligns with signals.* namespace in DSL.
Materialiser (Materialization/, EffectiveDecisionMap/)Upsert effective decision/findings state, append history, manage explain bundle exports.PostgreSQL transactions per SBOM chunk.
Orchestrator (Orchestration/, IncrementalOrchestrator/, Workers/)Change-stream ingestion, fairness, retry/backoff, persisted orchestrator-job queue (PersistedOrchestratorJobStore), worker host (PolicyOrchestratorJobWorkerHost, PolicyWorkerService).Jobs persist to policy.orchestrator_jobs / policy.worker_results; works with Scheduler Models DTOs.
API (Endpoints/, merged gateway under Endpoints/Gateway/)Minimal API endpoints, DTO validation, problem responses, idempotency, rate limiting on simulation routes.Generated clients for CLI/UI; OpenAPI 3.1 document at /openapi/v1.json.
Observability (Telemetry/)Metrics (policy_run_seconds, policy_rules_fired_total, gate metrics), traces, structured logs.Sampled rule-hit logs with redaction.
Air-gap / offline adapter (AirGap/)Bundle import (PolicyPackBundleImportService), sealed-mode state (SealedModeService), staleness signaling, air-gap notifications, risk-profile air-gap export.Uses DSSE/trust-root verifier; signed bundles require an explicit trust-roots path or fail closed.
VEX Decision Emitter (Vex/)Build OpenVEX statements (VexDecisionEmitter), attach reachability evidence hashes, request DSSE signing (VexDecisionSigningService), persist artifacts for Export Center / bench repo.Registered via AddVexDecisionEmitter() (POLICY-VEX-401-006) — which registers exactly IPolicyGateEvaluator and IVexDecisionEmitter; integrates with Signer predicate stella.ops/vexDecision@v1 and Attestor Rekor logging. Proof-spine handling is NOT part of this component (corrected 2026-08-18): VexProofSpineService was listed here as if registered, and it never was — no host registered it and no caller invoked it. It was deleted under SPRINT_20260722_007 POL-F3 because it was the only reason policy-engine compiled StellaOps.Scanner.ProofSpine, and through it the whole Attestor family. A revived VEX proof-spine feature rides a Scanner-owned seam, not a compile edge.
Determinization (StellaOps.Policy.Determinization/)Scores uncertainty/trust based on signal completeness and age; calculates entropy (0.0 = complete, 1.0 = no knowledge), confidence decay (exponential half-life), and aggregated trust scores; emits metrics for uncertainty/decay/trust; supports VEX-trust integration.Sibling library consumed by Signals and VEX subsystems; configuration via Determinization section.

3.1 · Determinization Configuration

The Determinization subsystem calculates uncertainty scores based on signal completeness (entropy), confidence decay based on observation age (exponential half-life), and aggregated trust scores. Configuration options in appsettings.json under Determinization:

{
  "Determinization": {
    "SignalWeights": {
      "VexWeight": 0.35,
      "EpssWeight": 0.10,
      "ReachabilityWeight": 0.25,
      "RuntimeWeight": 0.15,
      "BackportWeight": 0.10,
      "SbomLineageWeight": 0.05
    },
    "PriorDistribution": "Conservative",
    "ConfidenceHalfLifeDays": 14.0,
    "ConfidenceFloor": 0.1,
    "ManualReviewEntropyThreshold": 0.60,
    "RefreshEntropyThreshold": 0.40,
    "StaleObservationDays": 30.0,
    "EnableDetailedLogging": false,
    "EnableAutoRefresh": true,
    "MaxSignalQueryRetries": 3
  }
}
OptionTypeDefaultDescription
SignalWeightsObjectSee aboveRelative weights for each signal type in entropy calculation. Weights are normalized to sum to 1.0. VEX carries highest weight (0.35), followed by Reachability (0.25), Runtime (0.15), EPSS/Backport (0.10 each), and SBOM lineage (0.05).
PriorDistributionEnumConservativePrior distribution for missing signals. Options: Conservative (pessimistic), Neutral, Optimistic. Affects uncertainty tier classification when signals are unavailable.
ConfidenceHalfLifeDaysDouble14.0Half-life period for confidence decay in days. Confidence decays exponentially: exp(-ln(2) * age_days / half_life_days).
ConfidenceFloorDouble0.1Minimum confidence value after decay (0.0-1.0). Prevents confidence from decaying to zero, maintaining baseline trust even for very old observations.
ManualReviewEntropyThresholdDouble0.60Entropy threshold for triggering manual review (0.0-1.0). Findings with entropy ≥ this value require human intervention due to insufficient signal coverage.
RefreshEntropyThresholdDouble0.40Entropy threshold for triggering signal refresh (0.0-1.0). Findings with entropy ≥ this value should attempt to gather more signals before verdict.
StaleObservationDaysDouble30.0Maximum age before an observation is considered stale (days). Used in conjunction with decay calculations and auto-refresh triggers.
EnableDetailedLoggingBooleanfalseEnable verbose logging for entropy/decay/trust calculations. Useful for debugging but increases log volume significantly.
EnableAutoRefreshBooleantrueAutomatically trigger signal refresh when entropy exceeds RefreshEntropyThreshold. Requires integration with signal providers.
MaxSignalQueryRetriesInteger3Maximum retry attempts for failed signal provider queries before marking signal as unavailable.

Metrics emitted:

Usage in policies:

Determinization scores are exposed to SPL policies via the signals.trust.* and signals.uncertainty.* namespaces. Use signals.uncertainty.entropy to access entropy values and signals.trust.score for aggregated trust scores that combine VEX, reachability, runtime, and other signals with decay/weighting.

Weight Manifests:

EWS weights are externalized to versioned JSON manifests in etc/weights/. The unified score facade (IUnifiedScoreService) loads weights from these manifests rather than using compiled defaults, enabling auditable weight changes without code modifications. See Unified Score Architecture §4 for manifest schema and versioning rules.

3.1.1 · Trust Score Algebra Facade

The TrustScoreAlgebraFacade (ITrustScoreAlgebraFacade) provides a unified entry point composing TrustScoreAggregator + K4Lattice + ScorePolicy into a single deterministic scoring pipeline.

public interface ITrustScoreAlgebraFacade
{
    Task<TrustScoreResult> ComputeTrustScoreAsync(TrustScoreRequest request, CancellationToken ct);
    TrustScoreResult ComputeTrustScore(TrustScoreRequest request);
}

Pipeline steps:

  1. Calculate uncertainty entropy from signal snapshot
  2. Aggregate weighted signal scores via TrustScoreAggregator
  3. Compute K4 lattice verdict (Unknown/True/False/Conflict)
  4. Extract dimension scores (BaseSeverity, Reachability, Evidence, Provenance)
  5. Compute weighted final score in basis points (0-10000)
  6. Determine risk tier (Info/Low/Medium/High/Critical)
  7. Produce Score.v1 predicate for DSSE attestation

Score.v1 Predicate Format:

All numeric scores use basis points (0-10000) for bit-exact determinism:

{
  "predicateType": "https://stella-ops.org/predicates/score/v1",
  "artifactId": "pkg:maven/com.example/mylib@1.0.0",
  "vulnerabilityId": "CVE-2024-1234",
  "trustScoreBps": 7250,
  "tier": "High",
  "latticeVerdict": "True",
  "uncertaintyBps": 2500,
  "dimensions": {
    "baseSeverityBps": 5000,
    "reachabilityBps": 10000,
    "evidenceBps": 6000,
    "provenanceBps": 8000,
    "epssBps": 3500,
    "vexBps": 10000
  },
  "weightsUsed": {
    "baseSeverity": 1000,
    "reachability": 4500,
    "evidence": 3000,
    "provenance": 1500
  },
  "policyDigest": "sha256:abc123...",
  "computedAt": "2026-01-15T12:00:00Z",
  "tenantId": "tenant-123"
}

Risk Tier Mapping:

Score (bps)Tier
≥ 9000Critical
≥ 7000High
≥ 4000Medium
≥ 1000Low
< 1000Info

3.2 - License compliance configuration

License compliance evaluation runs during SBOM evaluation when enabled in licenseCompliance settings.

{
  "licenseCompliance": {
    "enabled": true,
    "policyPath": "policies/license-policy.yaml"
  }
}

3.3 - NTIA compliance configuration

NTIA minimum-elements validation runs when enabled under ntiaCompliance.

{
  "ntiaCompliance": {
    "enabled": true,
    "enforceGate": false,
    "policyPath": "policies/ntia-policy.yaml"
  }
}

4 · Data Model & Persistence

Policy Engine persists exclusively to a PostgreSQL policyschema (plus a policy_app role schema). There is no MongoDB store and no per-policy effective_finding_{policyId} collections — those were design-era artefacts. Schema is owned by StellaOps.Policy.Persistence and applied via forward-only startup migrations (AddStartupMigrations(PolicyDataSource.DefaultSchemaName, "Policy.Persistence", typeof(PolicyDataSource).Assembly) — schema policy, module name Policy.Persistence — wired in PolicyPersistenceExtensions:36 / AddPolicyPostgresStorage). Migration SQL files are embedded resources (Migrations\**\*.sql), with Migrations\_archived\** explicitly excluded from embedding. The live baseline 001_v1_policy_baseline.sql is idempotent (CREATE … IF NOT EXISTS) so it converges on reused local volumes.

4.1 Tables (by migration)

Migration-filename note (2026-07-12). The live migration set is 001_v1_policy_baseline.sql, 002_operator_decision_signing_columns.sql, 003_operator_decision_signing_view.sql, 004_risk_override_store.sql. The pre-1.0 numbering below (001_initial_schema.sql, 002–015) is provenance of DDL folded into the baseline, not live filenames — those files sit under Migrations/_archived/pre_1.0/ (and .../mig061/) and are excluded from embedding. Note the leaf-name collision hazard: 001_initial_schema.sql exists in the archived tree only.

Consolidated baseline 001_v1_policy_baseline.sql (folds in the pre-1.0 001_initial_schema.sql):

(policy.risk_overrides is not in the baseline — it is created by the live post-baseline migration 004_risk_override_store.sql; see below.)

Folded-in pre-1.0 migrations (archived filenames — their DDL now lives inside the baseline):

Post-baseline forward-only migrations — these are the live 002–007 files on disk (a separate physical sequence appended after the consolidated 001_v1_policy_baseline.sql; their numbers are distinct from the archived pre-1.0 numbering listed above, which is folded into the baseline). 002–004 add the operator-signed governance-decision support (ADR-025, sprint SPRINT_20260608_015); 005–007 are later additions listed after them:

All of them are idempotent (CREATE TABLE/ADD COLUMN/CREATE INDEX … IF NOT EXISTS, DROP POLICY IF EXISTS + CREATE POLICY), embedded resources picked up by AddStartupMigrations, and converge on a fresh database (CLAUDE.md §2.7). Nothing is written under Migrations/_archived/**.

4.2 Schema Highlights

4.3 Indexing


5 · Evaluation Pipeline

sequenceDiagram
    autonumber
    participant Worker as EvaluationWorker
    participant Compiler as CompilerCache
    participant Selector as SelectionLayer
    participant Eval as Evaluator
    participant Mat as Materialiser
    participant Expl as ExplainStore

    Worker->>Compiler: Load IR (policyId, version, digest)
    Compiler-->>Worker: CompiledPolicy (cached or compiled)
    Worker->>Selector: Fetch tuple batches (sbom, advisory, vex)
    Selector-->>Worker: Deterministic batches (1024 tuples)
    loop For each batch
        Worker->>Eval: Execute rules (batch, env)
        Eval-->>Worker: Verdicts + rule hits
        Worker->>Mat: Upsert effective findings
        Mat-->>Worker: Success
        Worker->>Expl: Persist sampled explain traces (optional)
    end
    Worker->>Mat: Append history + run stats
    Worker-->>Worker: Compute determinism hash
    Worker->>+Mat: Finalize transaction
    Mat-->>Worker: Ack

Determinism guard instrumentation wraps the evaluator, rejecting access to forbidden APIs and ensuring batch ordering remains stable.

5.1 · Release-time findings lookup (ecosystem-aware PURL join)

PostgresFindingsLookup resolves a release subject (release:<id>|env:<name>|...) by calling ReleaseOrchestrator’s authenticated tenant-scoped component owner API, then Scanner’s bounded digest-detail owner API. It does not read release_orchestrator.* or scanner.* through PolicyDataSource. RO supplies the persisted digest, nullable package PURL and ecosystem; Policy preserves curated name/version semantics, including tag fallback for an explicitly null version. Empty releases, absent or malformed owner metadata, authentication failure and transport failure are unavailable inputs, not clean releases. These paths fail before opening Policy’s own database connection.

PolicyEngine:ReleaseComponents declares the direct owner origin and bounded request timeout, response bytes and component count. Each request uses an exact-tenant release:read token; redirects and cookies are disabled, and response URLs never select a request destination. The existing policy-engine service identity needs that read grant, not release mutation or publication scopes. POLICY_RELEASE_COMPONENTS_BASE_ADDRESS selects the Compose origin. The source contract is covered by ReleaseComponentsClientTests, ReleaseComponentsLookupIntegrationTests and RO’s ReleaseComponentsParityTests / ReleaseComponentsOpenApiTests; deployment and live consumer acceptance remain separate gates.

Scanner responses separate latest-attempt freshness from latest successful evidence. Policy validates the exact tenant/digest and ScanFactsDigest hashes before joining. Its advisory source is Policy’s own policy.vuln_gate_current projection, and only that: there is no operator mode key selecting a source.

Retirement note (2026-09-14, SPRINT_20260914_001 VRP-2, ownership-matrix X3). The SPRINT_20260722_008 G1 window dropped the vuln schema on 2026-08-04. The PolicyEngine:FindingsLookup:AdvisorySource selector (Legacy / GateDecision / PolicyProjection), the PolicyEngine:FindingsLookup:ShadowCompare parity pass and the two readers of vuln.advisory_affected and vuln.issue_gate_decisions behind them were deleted, not re-homed: the tables existed in none of the estate’s databases, Policy connects only to stellaops_policy, and selecting either retired source produced 42P01 and a deny. A host that still receives AdvisorySource (other than the surviving PolicyProjection value) or any ShadowCompare refuses to start; the failure names the key and the retiring row (FindingsAdvisorySourceOptionsValidator). The source-contract pin ReleaseComponentsLookupIntegrationTests.SourceContract_HasOwnerClientAndNoForeignSql rejects any FROM/JOIN against release_orchestrator.* or vuln.* in the lookup.

PolicyEngine:FindingsLookup:OnUnavailable decides what a gate does when the findings source cannot answer, as distinct from answering “nothing affects this release”. Both arrive as an empty CVE set, and conflating them is how a gate fails open — no CVEs resolved means no denylist rule matches, which reads as allow. Deny (the default, and the recorded steady-state posture for production-class environments) blocks with the reason on the evidence record; WarnAndAllow is the documented opt-DOWN for lower environments and still refuses to describe the verdict as clean. An unconfigured host fails closed. Two conditions raise the flag: any exception from the lookup, and a projection that has never been fed, witnessed by the absence of the projector’s durable consumer checkpoint in eventing.consumer_checkpoints (the checkpoint is the right witness and a row count is not: an empty table is legitimate for a clean tenant, whereas a missing checkpoint proves no hub event was ever applied). That readiness guard is unconditional.

The compose operator control is POLICY_FINDINGS_ON_UNAVAILABLE (default Deny); it binds through the service-prefixed configuration key and requires a Policy container recreate to change.

The projection that feeds PolicyProjectionis driven by VulnGateProjectionHost (src/Policy/__Libraries/StellaOps.Policy.Persistence/Postgres/VulnGate/VulnGateProjectionHost.cs), hosted in policy-engine by VulnGateProjectionWorker. It acquires the policy.vuln_gate.projector lease, drains the hub’s vuln.consensus stream over the owner API (never a connection to stellaops_vuln — CoC §8.2), and renews the lease between batches so the liveness requirement is one batch per TTL rather than the whole backlog per TTL. That heartbeat is not optional politeness: renewing only after an unbounded drain is G0 finding F10, where the hub’s equivalent host fenced itself roughly once per TTL with no competitor present and converged on 51 % of its stream. It is opt-in via PolicyEngine:VulnGateProjection:Enabled (default false) plus PolicyEngine:VulnGateProjection:Hub:BaseAddress (required, no default); when disabled the registration extension registers none of the projector’s dependencies, so a host that never opted in cannot fail to start over a hub address it does not need, and the disabled state is logged rather than left to be inferred from an empty table.

The projection query keys on the normalized package_name and ecosystem columns and passes effective_affected_range through the shared EcosystemVersionFilter. not_affected, withdrawn, and rejected decisions are always suppressed. fixed and resolved suppress only range-less decisions: when an affected range exists they mean that patched versions exist, while installed versions inside that range remain vulnerable. The shared GateDecisionStatusSemantics contract keeps Policy and compact export aligned. The 2026-07-15 CANON-D6 parity, benchmark, and live forcing-function gates closed with zero unexplained recall regression when gate decisions first became authoritative; the projection inherited that row shape and status semantics unchanged.

Sprint 20260531.036 / D1.1 — ecosystem-aware join (read-side). Until this sprint the SQL join was lower(af.package_name) = ANY(@packages) — text-only on the package name. That collapsed same-name-different-ecosystem packages into the same advisory bucket and produced FALSE-POSITIVE / FALSE-NEGATIVE matches:

Component shapeAdvisory shapePre-036 resultPost-036 result
Alpine openssl 3.1.4-r1 (apk)Debian openssl (deb)FALSE-POSITIVE matchno match
Debian libssl1.1 (deb)Alpine openssl (apk)FALSE-POSITIVE matchno match
npm @scope/pkgnpm short pkgFALSE-POSITIVE matchno match
Maven org.apache.commons:commons-lang3short commons-lang3 (maven)FALSE-POSITIVE matchno match
PyPi DjangoDebian python3-djangoFALSE-POSITIVE matchno match
OCI base image redisPyPi redisFALSE-POSITIVE matchno match

Contract after Sprint 20260531.036:

  1. Producer (PostgresReleaseTruthRepository.UpsertComponentAsync) writes two new nullable columns to every release_components row: package_purl (canonical PURL string) + package_ecosystem (lowercased PURL type — apk, deb, npm, maven, pypi, oci, …). OCI image components default to package_ecosystem = "oci" and a synthetic PURL pkg:oci/<image-ref>@<digest>. The owner API preserves explicit null metadata; components whose ecosystem remains unresolved retain the advisory planner’s name-only admission semantics. The former per-row policy.findings_lookup.legacy_name_match debug message was removed with Policy’s direct release-table reader. The same event name remains as an aggregate message only inside LoadLegacyCvesForComponentsAsync when that advisory branch reaches its name-only pass; neither an owner read nor the PolicyProjection path guarantees it. This describes retained source branches, not permission to reactivate the retired advisory database plane.
  2. Release Orchestrator persistence adds the columns + partial indexes (ix_release_components_purl / ix_release_components_ecosystem_name) in its consolidated baseline src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Persistence/Migrations/001_v1_releaseorchestrator_baseline.sql:905 (folded in from the archived 014_release_components_purl_ecosystem.sql). Forward-only per ADR-004. Re-runnable.
  3. Consumer (PostgresFindingsLookup) runs TWO SQL passes for either configured advisory source:
    • Scoped pass — fires only when at least one component carries a canonical ecosystem; SQL has both lower(af.package_name) = ANY(@packages) AND lower(af.ecosystem) = ANY(@ecosystems). @ecosystems is the FAN-OUT of every equivalent spelling per EcosystemEquivalence (so an advisory tagged "alpine" still matches a component typed apk, and vice versa).
    • Legacy pass — fires only when at least one component has NO resolvable ecosystem; SQL is name-only. A component WITH an ecosystem is NEVER consumed via this pass (would re-introduce the false-positive class).
  4. EcosystemEquivalence.Canonicalisemaps {"alpine","apk"} → "apk", {"debian","ubuntu","deb"} → "deb", {"redhat","rhel","centos","fedora","amzn","suse","opensuse","rpm"} → "rpm", {"go","golang"} → "golang", {"docker","oci","container"} → "oci", etc. The EquivalentSpellings() reverse lookup is what backs the SQL ANY(@ecosystems) parameter.
  5. EcosystemAwareJoinPlan (src/Vulnerabilities/__Libraries/StellaOps.VulnMatch.Core/EcosystemAwareJoinPlan.cs) is the pure-function planner extracted from PostgresFindingsLookup so the bucket-key + ecosystem fan-out semantics are unit-testable without a live PostgreSQL fixture. It lives in the Vulnerabilities-owned DC-30 artifact-consumer SDK StellaOps.VulnMatch.Core (alongside EcosystemEquivalence, same directory), not under StellaOps.Policy.Engine/Services; PostgresFindingsLookup merely consumes it. The runtime delegates bucket construction + advisory-row admission to it.
  6. PURL name shapes per ecosystem (preserved by PurlComponentExtractor.TryParseFull):
    • npm scoped — pkg:npm/%40babel/core@7.0.0 / pkg:npm/@scope/widget@1.0.0 → name @scope/widget.
    • Maven — pkg:maven/org.apache.commons/commons-lang3@3.12.0 → name org.apache.commons:commons-lang3 (groupId+artifactId joined with :).
    • Golang — pkg:golang/github.com/foo/bar@v1.2.3 → name github.com/foo/bar (full module path).
    • composer / pub / swift — pkg:composer/api-platform/core@v3.4.17 → name api-platform/core (the vendor namespace is preserved). Collapsing to the bare core cross-matched every vendor’s */core advisory (e.g. drupal/core vs api-platform/core), the ~460 false-positive class closed by the composer-namespace precision fix. Single-segment composer/pub/swift PURLs keep their bare name. AdvisoryConverter derives vuln.advisory_affected.package_name from the SAME extractor, so the deployed and advisory keys never drift; the live Concelier migration src/Concelier/__Libraries/StellaOps.Concelier.Persistence/Migrations/009_composer_namespaced_package_name_backfill.sql re-derives existing rows from their retained PURL.
    • Everything else (apk/deb/rpm/pypi/nuget/oci/generic) — only the trailing path segment.

The post-hoc EcosystemVersionFilter C# pass remains in place as a secondary version-range discriminator; it is no longer the only ecosystem-awareness layer.

5.2 · Scanner Policy-owner seam

Policy owns the Scanner report policy contract at /api/v1/policy/scanner/{schema,diagnostics,preview,rationale}. The wire shapes live in the producer-owned, BCL-only StellaOps.Policy.Scanner.Contracts; Scanner calls them through its IPolicyOwnerClient and never references Policy implementation or storage. Every request carries an explicit tenant, and Policy rejects a tenant that is missing or differs from the authenticated stellaops:tenant claim.

These four machine endpoints require the dedicated policy:preview:invoke scope. They deliberately exclude the resource server’s broader configured policy:read default: the stellaops-scanner-web client is granted the dedicated capability and is intentionally denied broad Policy read authority. The named shared authorization helper preserves bearer authentication, tenant enforcement and authority.resource.authorize auditing; it changes only default-scope composition for these four conformance-pinned routes. All ordinary Policy endpoints continue to combine their endpoint requirements with configured resource defaults. A missing, rejected or unavailable owner call fails the Scanner report path closed rather than falling back to local Policy logic or database access.


6 · Run Orchestration & Incremental Flow


6.1 · VEX decision attestation pipeline

  1. Verdict capture. Each evaluation result contains {findingId, cve, productKey, reachabilityState, evidenceRefs} plus SBOM and runtime CAS hashes.
  2. OpenVEX serialization. VexDecisionEmitter builds an OpenVEX document with one statement per (cve, productKey) and fills:
    • status, justification, status_notes, impact_statement, action_statement.
    • products (purl) and evidence array referencing reachability.json, sbom.cdx.json, runtimeFacts.
  3. DSSE signing. The emitter calls Signer POST /api/v1/signer/sign/dsse with predicate stella.ops/vexDecision@v1. Signer verifies PoE + scanner integrity and returns a DSSE envelope (decision.dsse.json).
  4. Transparency (optional). When Rekor integration is enabled, Attestor logs the DSSE payload and returns {uuid, logIndex, checkpoint} which we persist next to the decision. Hosts that do not configure Rekor anchoring must fail truthfully with an unsupported submission path; they must never fabricate a successful transparency receipt.
  5. Export. API/CLI endpoints expose decision.openvex.json, decision.dsse.json, rekor.txt, and evidence metadata so Export Center + bench automation can mirror them into bench/findings/** as defined in the VEX Evidence Playbook.

Policy-owned evidence consumers fail closed when required trust material cannot be verified. VEX verification rejects base64-only signatures unless an explicit local development signing mode is enabled for tests. VEX proof gates also reject incomplete proof metadata: required confidence tier, statement count, signed-statement status, proof timestamp, DSSE envelope digest, and Rekor log index must be present when the policy asks for them. PoE validation rejects required DSSE and Rekor evidence until a real verifier/trust-root integration is configured. CVSS receipt creation enforces declared DSSE, Rekor, signer, and trust-level requirements instead of silently producing unsigned receipts. Policy pack bundle import and sealed-mode bundle verification do not infer trust from signature metadata alone: signed bundles require an explicit trust-roots path plus a configured DSSE/trust-root verifier, and absent verifier/trust roots or verifier rejection leaves the import failed or the verification response invalid.

Replay, Unknowns, what-if, attestation verification, and overlay publication share the same truthfulness rule. The Unknowns gate queries the configured Unknowns API and blocks with unknowns_source_unavailable when that source is unavailable; it must not synthesize HOT unknown IDs or random unknown states. Replay returns ReplayFailed when no replay evaluator and original verdict store are configured; hash-derived verdicts are forbidden. What-if simulation requires an explicit baseline policy pack and fails unavailable until a real evaluator is wired for hypothetical SBOM diffs and draft policies. Policy decision attestation verification returns invalid/unavailable without a DSSE verifier and attestation store, and overlay change publication fails when only the log sink is registered rather than a durable event bus.

All payloads are immutable and include analyzer fingerprints (scanner.native@sha256:..., policyEngine@sha256:...) so replay tooling can recompute identical digests. Determinism tests cover both the OpenVEX JSON and the DSSE payload bytes.


6.2 · CI/CD Release Gate API

The Policy Engine exposes a gate evaluation API for CI/CD pipelines to validate images before deployment.

Gate Endpoint

POST /api/v1/policy/gate/evaluate

Request (GateEvaluateRequest; only imageDigest is required; tenant is resolved from the envelope-bound stellaops:tenant claim, not a request field):

{
  "imageDigest": "sha256:abc123def456",
  "repository": "registry.example.com/app",
  "tag": "1.2.0",
  "baselineRef": "sha256:baseline789",
  "missingBaselineMode": "block",
  "policyId": "production-gate",
  "allowOverride": false,
  "overrideJustification": null,
  "source": "cli",
  "ciContext": "github-actions",
  "context": {
    "branch": "main",
    "commitSha": "abc123",
    "pipelineId": "run-456",
    "environment": "production",
    "actor": "deploy-service@example.com"
  }
}

Response (GateEvaluateResponse; status is the GateStatus enum Pass|Warn|Fail; severity deltas live under the optional deltaSummary object, not as top-level counts):

{
  "decisionId": "decision-uuid",
  "status": "Pass",
  "exitCode": 0,
  "imageDigest": "sha256:abc123def456",
  "baselineRef": "sha256:baseline789",
  "baselineStatus": "resolved",
  "evidence": { "verdictHash": "sha256:..." },
  "decidedAt": "2025-12-26T12:00:00Z",
  "summary": "No new critical vulnerabilities",
  "advisory": null,
  "gates": [
    { "name": "CvssThresholdGate", "result": "pass", "reason": "below threshold" }
  ],
  "blockedBy": null,
  "blockReason": null,
  "suggestion": null,
  "overrideApplied": false,
  "deltaSummary": {
    "totalChanges": 0,
    "riskIncreasing": 0,
    "riskDecreasing": 0,
    "neutral": 0,
    "riskScore": 0,
    "riskDirection": "none"
  }
}

Gate Status Values

StatusExit CodeDescription
Pass0No blocking issues; safe to deploy
Warn1Non-blocking issues detected; configurable pass-through
Fail2Blocking issues; deployment should be halted

Every gate response is durably recorded in policy.gate_decisions after the response object is constructed. If the public decisionId is not UUID-form, Policy derives a stable UUID for the history table from the decision id, digest, status, and decision timestamp while preserving the public string decision id in the API response. The request image digest is stored in image_digest, and Integrations reads the latest row per tenant/digest to project registry-browser hasDecision/verdict fields.

Webhook Integration

The Policy Gateway accepts webhooks from container registries for automated gate evaluation:

Docker Registry v2:

POST /api/v1/webhooks/registry/docker

Harbor:

POST /api/v1/webhooks/registry/harbor

Generic (Zastava events):

POST /api/v1/webhooks/registry/generic
Webhook signature contract (Sprint 001 POLICY-WHK-03…05; wired by SPRINT_017)

All three registry webhook endpoints are HMAC-only. Unsigned requests are rejected with HTTP 401. There is no bearer/cookie auth on these routes — the per-registry shared secret is the sender proof. The signature filter is wired via RegistryWebhookSignatureFilter and validated through the regional crypto plug-in interface StellaOps.Cryptography.IHmacAlgorithm, so FIPS / GOST / SM / eIDAS deployments substitute their compliant implementation without source edits to the gateway.

EndpointHeaderAlgorithm
/dockerX-StellaOps-Signature: sha256=<hex> (preferred) or Docker Authorization: Basic <user:hex>HMAC-SHA256 over raw body
/harborX-Harbor-Signature: sha256=<hex>HMAC-SHA256 over raw body
/genericX-StellaOps-Signature: sha256=<hex> (optional X-StellaOps-Key-Id)HMAC-SHA256 over raw body

Rejection reasons (returned in the problem+json detail on 401) include validator_disabled, secret_not_configured, missing_signature_header, signature_scheme_unsupported, signature_malformed, signature_invalid, bearer_not_supported, unknown_key_id, missing_key_id. Operators MUST NOT expose secret material in problem responses; the validators only emit the stable reason token.

Configuration. Operators set the per-registry shared secret in an environment variable referenced by Policy:Gateway:RegistryWebhooks:<Kind>:SharedSecret:EnvVar. The compose defaults reference POLICY_REGISTRY_WEBHOOK_DOCKER_SECRET, POLICY_REGISTRY_WEBHOOK_HARBOR_SECRET, and POLICY_REGISTRY_WEBHOOK_GENERIC_SECRET; populating those env vars from a sealed-secret / Vault / SOPS source is the recommended deployment path. The AllowUnsigned rollout toggle (Policy:Gateway:RegistryWebhooks:AllowUnsigned) exists only for migration; RegistryWebhookProductionGuard fails the host at startup if AllowUnsigned=true while ASPNETCORE_ENVIRONMENT=Production.

Body buffering. The gateway pipeline calls UseRegistryWebhookBodyBuffering() early (before authentication middleware) so that the signature filter — which fires after minimal-API parameter binding — can rewind the request body and hash the original bytes. Without that middleware, in current ASP.NET Core minimal-API pipelines the [FromBody] binder drains the body before any endpoint filter runs, the filter sees zero bytes, and the HMAC always fails.

Operator runbook. Per-registry sender-side wiring recipes (Docker notifications.endpoints[].headers, Harbor webhook auth header, generic curl), failure-mode reason tokens, and the migration path for unsigned deployments live at docs/operations/policy-registry-webhooks.md.

Webhook push handlers now use a runtime-selected async gate-evaluation path. When Postgres:Scheduler is not configured, both Policy hosts resolve IGateEvaluationQueue to an explicit unsupported runtime adapter and return 501 problem+json instead of fabricating queued work. When scheduler persistence is configured, both hosts register the shared scheduler-backed queue runtime, auto-migrate the scheduler schema, enqueue deduplicated policy.gate-evaluation jobs, dispatch them through the worker service, and expose persisted request/decision status from GET /api/v1/policy/gate/jobs/{jobId}. The previous process-local InMemoryGateEvaluationQueue and background worker path was removed because it fabricated “no drift” gate contexts and fake job IDs instead of dispatching real work.

Gate Bypass Auditing

Bypass attempts are logged to policy.gate_bypass_audit:

Runtime Snapshots And Ledger Exports

{
  "bypassId": "bypass-uuid",
  "imageDigest": "sha256:abc123",
  "actor": "deploy-service@example.com",
  "justification": "Emergency hotfix - JIRA-12345",
  "ipAddress": "10.0.0.100",
  "ciContext": {
    "provider": "github-actions",
    "runId": "12345678",
    "workflow": "deploy.yml"
  },
  "createdAt": "2025-12-26T12:00:00Z"
}

CLI Integration

# Evaluate gate
stella gate evaluate --image sha256:abc123 --baseline sha256:baseline

# Gate status lookup is not implemented; this command prints that fact and exits 9
stella gate status --decision-id <decision-id>

# Override with justification
stella gate evaluate --image sha256:abc123 \
  --allow-override \
  --justification "Emergency hotfix approved by CISO - JIRA-12345"

See also: CI/CD gate samples (GitHub Actions; generate with stella ci init), CI/CD gate flow, Signer dossier


6.3 · Trust Lattice Policy Gates

The Policy Engine evaluates Trust Lattice gates after claim score merging to enforce trust-based constraints on VEX verdicts.

Gate Interface

public interface IPolicyGate
{
    Task<GateResult> EvaluateAsync(
        MergeResult mergeResult,
        PolicyGateContext context,
        CancellationToken ct = default);
}

public sealed record GateResult
{
    public required string GateName { get; init; }
    public required bool Passed { get; init; }
    public string? Reason { get; init; }
    public ImmutableDictionary<string, object> Details { get; init; }
}

Available Gates

GatePurposeConfiguration Key
MinimumConfidenceGateReject verdicts below confidence threshold per environmentgates.minimumConfidence
UnknownsBudgetGateFail scan if unknowns exceed budgetgates.unknownsBudget
SourceQuotaGatePrevent single-source dominance without corroborationgates.sourceQuota
ReachabilityRequirementGateRequire reachability proof for critical CVEsgates.reachabilityRequirement
EvidenceFreshnessGateReject stale evidence below freshness thresholdgates.evidenceFreshness
CvssThresholdGateBlock findings above CVSS score thresholdgates.cvssThreshold
SbomPresenceGateRequire valid SBOM for release artifactsgates.sbomPresence
SignatureRequiredGateRequire signatures on specified evidence typesgates.signatureRequired
FacetQuotaGateEnforce per-facet drift quotas from a pre-computed FacetDriftReport (block/warn/seal-action when report absent)FacetQuotaGateOptions
FixChainGateRequire fix-chain verification for critical vulnerabilities (see Fix Chain Gates)FixChainGateOptions
VexProofGateValidate VEX proof objects meet policy requirements (confidence tier, signed-statement status, DSSE digest, Rekor index)VexProofGateOptions

All eleven gates implement IPolicyGate and live under src/Policy/__Libraries/StellaOps.Policy/Gates/. The five threshold/presence gates and MinimumConfidence/Unknowns/SourceQuota/Reachability/EvidenceFreshness are covered by etc/policy-gates.yaml.sample; FacetQuotaGate, FixChainGate, and VexProofGate bind their own options classes and are not yet represented in that sample file.

MinimumConfidenceGate

Requires minimum confidence threshold for suppression verdicts:

gates:
  minimumConfidence:
    enabled: true
    thresholds:
      production: 0.75    # High bar for production
      staging: 0.60       # Moderate for staging
      development: 0.40   # Permissive for dev
    applyToStatuses:
      - not_affected
      - fixed

UnknownsBudgetGate

Limits exposure to unknown/unscored dependencies:

gates:
  unknownsBudget:
    enabled: true
    maxUnknownCount: 5
    maxCumulativeUncertainty: 2.0
    escalateOnExceed: true

SourceQuotaGate

Prevents single-source verdicts without corroboration:

gates:
  sourceQuota:
    enabled: true
    maxInfluencePercent: 60
    corroborationDelta: 0.10
    requireCorroboration: true

ReachabilityRequirementGate

Requires reachability proof for high-severity vulnerabilities:

gates:
  reachabilityRequirement:
    enabled: true
    applySeverities:
      - critical
      - high
    exemptStatuses:
      - not_affected
    bypassReasons:
      - component_not_present

CvssThresholdGate

Sprint: SPRINT_20260112_017_POLICY_cvss_threshold_gate

Blocks findings above a configurable CVSS score threshold per environment:

gates:
  cvssThreshold:
    enabled: true
    priority: 15
    defaultThreshold: 7.0
    thresholds:
      production: 7.0
      staging: 8.0
      development: 9.0
    cvssVersionPreference: highest  # v3.1, v4.0, or highest
    failOnMissingCvss: false
    requireAllVersionsPass: false
    allowlist:
      - CVE-2024-XXXXX  # False positive
    denylist:
      - CVE-2024-YYYYY  # Always block

SbomPresenceGate

Sprint: SPRINT_20260112_017_POLICY_sbom_presence_gate

Requires valid SBOM presence for release artifacts:

gates:
  sbomPresence:
    enabled: true
    priority: 5
    enforcement:
      production: required
      staging: required
      development: optional
    acceptedFormats:
      - spdx-2.3
      - spdx-3.0.1
      - cyclonedx-1.5
      - cyclonedx-1.6
    minimumComponents: 1
    requireSignature: true
    schemaValidation: true
    requirePrimaryComponent: true

SignatureRequiredGate

Sprint: SPRINT_20260112_017_POLICY_signature_required_gate

Requires cryptographic signatures on specified evidence types:

gates:
  signatureRequired:
    enabled: true
    priority: 3
    evidenceTypes:
      sbom:
        required: true
        trustedIssuers:
          - "*@company.com"
          - "build-service@ci.example.com"
        acceptedAlgorithms:
          - ES256
          - RS256
      vex:
        required: true
        trustedIssuers:
          - "*@vendor.com"
      attestation:
        required: true
    enableKeylessVerification: true
    fulcioRoots: /etc/stella/trust/fulcio-roots.pem
    requireTransparencyLogInclusion: true
    environments:
      development:
        requiredOverride: false
        skipEvidenceTypes:
          - sbom

Gate Registry

Gates are registered via DI and evaluated in sequence:

public interface IPolicyGateRegistry
{
    IEnumerable<IPolicyGate> GetEnabledGates(string environment);
    Task<GateEvaluationResult> EvaluateAllAsync(
        MergeResult mergeResult,
        PolicyGateContext context,
        CancellationToken ct = default);
}

Gate Metrics

PolicyEngineTelemetry emits gate-job SLI instruments from the async gateway executor:

Gate Implementation Reference

GateSource File
MinimumConfidenceGatesrc/Policy/__Libraries/StellaOps.Policy/Gates/MinimumConfidenceGate.cs
UnknownsBudgetGatesrc/Policy/__Libraries/StellaOps.Policy/Gates/UnknownsBudgetGate.cs
SourceQuotaGatesrc/Policy/__Libraries/StellaOps.Policy/Gates/SourceQuotaGate.cs
ReachabilityRequirementGatesrc/Policy/__Libraries/StellaOps.Policy/Gates/ReachabilityRequirementGate.cs
EvidenceFreshnessGatesrc/Policy/__Libraries/StellaOps.Policy/Gates/EvidenceFreshnessGate.cs
CvssThresholdGatesrc/Policy/__Libraries/StellaOps.Policy/Gates/CvssThresholdGate.cs
SbomPresenceGatesrc/Policy/__Libraries/StellaOps.Policy/Gates/SbomPresenceGate.cs
SignatureRequiredGatesrc/Policy/__Libraries/StellaOps.Policy/Gates/SignatureRequiredGate.cs
FacetQuotaGatesrc/Policy/__Libraries/StellaOps.Policy/Gates/FacetQuotaGate.cs
FixChainGatesrc/Policy/__Libraries/StellaOps.Policy/Gates/FixChainGate.cs
VexProofGatesrc/Policy/__Libraries/StellaOps.Policy/Gates/VexProofGate.cs

See etc/policy-gates.yaml.sample for the configuration options covered by that sample (the three additional gates above bind their own options classes).

Related Documentation:


6.4 · Exception Approval Workflow

The Policy Engine provides a role-based exception approval workflow for gate overrides, allowing teams to request time-limited exceptions with proper audit trails.

Exception Model

public enum ApprovalRequestStatus
{
    Pending,     // Awaiting approvals
    Partial,     // Some approvals received, not yet complete
    Approved,    // All required approvals obtained
    Rejected,    // At least one rejection
    Expired,     // TTL exceeded without full approval
    Cancelled    // Requestor cancelled
}

public enum ExceptionReasonCode
{
    FalsePositive,         // CVE does not apply to this usage
    AcceptedRisk,          // Risk accepted with compensating controls
    CompensatingControl,   // Mitigation in place (WAF, network isolation)
    TestOnly,              // Affects only test/non-shipping code paths
    VendorNotAffected,     // Vendor VEX states product not affected
    ScheduledFix,          // Fix scheduled within TTL window
    DeprecationInProgress, // Component being removed/replaced
    RuntimeMitigation,     // Mitigated at runtime (e.g. sandbox, seccomp)
    NetworkIsolation,      // Reachability removed via network isolation
    Other                  // Other documented justification
}

Gate-Level Approval Requirements

Gate LevelDescriptionRequired Approvers
G0DevelopmentAuto-approve (no approval needed)
G1Testing/QA1 peer reviewer
G2StagingCode owner approval
G3Pre-productionDelivery Manager + Product Manager
G4ProductionCISO + Delivery Manager + Product Manager

Exception Request Endpoint

POST /api/v1/policy/exception/request

See Implementation Reference below for full API contract details.

Operator signing-key readiness (OSK-P1)

GET /api/v1/policy/exception/operator-key/status resolves the authenticated operator’s current DecisionSigning keys through IssuerDirectory and returns a provider-compatible readiness result plus enrollmentState (pending, enrolled, or re_enroll_required) and the stable enrolment route /administration/profile. The read is tenant- and subject-bound, filters to active keys inside their validity window, plus a provider-change-retired key strictly before its future retiredAt grace cutoff, and verifies algorithm support through ICryptoProviderRegistry; Policy does not perform bare cryptography. A grace key resolves through its enrolled providerHint, may satisfy request readiness, and still projects re_enroll_required rather than enrolled. At or after the cutoff it no longer satisfies readiness. No key history, or a key awaiting approval, projects pending; a compatible current key projects enrolled; incompatible, non-grace retired, revoked, or expired history projects re_enroll_required. A missing key is an available status with hasKey=false. Missing IssuerDirectory client or issuer configuration, and dependency failures, return 503 fail-closed instead of being misreported as a keyless operator.

OSK-P2 removed the transitional Policy:ExceptionApprovalSigning:ProviderHint source from enrollment and response decisions. Policy now calls Platform’s tenant-bound /api/v1/admin/crypto-providers/compliance-profile/operator-signing-enrollment projection, forwards the tenant and bearer identity, and uses its exact provider and algorithm when checking active IssuerDirectory keys. Exception queue and approval-request DTO signing metadata use that same live provider. An unavailable or incomplete Platform projection fails closed (503) instead of falling back to a stale static hint. OSK-P3 adds the pending | enrolled | re_enroll_required projection; none of these read paths enables signing-required behavior by itself.

The production enforcement source is Policy:OperatorSigningRequirement:Tenants:<tenant>=true; the map is empty by default and has no global enable. When required, POST /api/v1/policy/exception/request checks key presence before validation or persistence and returns HTTP 400 with code=needs-enrollment, the active provider, and /administration/profile for a keyless operator. Dependency failure returns 503 instead of bypassing the gate. If the rules would auto-approve a request into a grant, the request must include a valid operator-decision@v1 envelope bound to artifactDigest; unsigned grants are rejected and a verified envelope is persisted with the approved request.

Approval Actions

Approve:

POST /api/v1/policy/exception/{requestId}/approve

Reject:

POST /api/v1/policy/exception/{requestId}/reject

Approval → grant materialization (2026-07-21, I3-WAIVER-EXCEPTION-FLOW)

An approval request that reaches Approved (final M-of-N approve, or an eligible auto-approve at create time) is materialized into an ACTIVE ExceptionObjectby ExceptionApprovalMaterializer (src/Policy/StellaOps.Policy.Engine/Services/ExceptionApprovalMaterializer.cs): the grant is created through the real lifecycle (Created → Activated events in policy.exception_events), scoped from the request (vulnerability_id/purl_pattern/artifact_digest/environments), expires at the request’s exception_expires_at, carries metadata.approvalRequestId for provenance, and is back-linked onto the request row (exception_id, returned in the create/approve DTOs). Before this bridge the two stores were disjoint — an approved EAR was a signed record that neither policy evaluation (ExceptionAdapter) nor the Findings exposure reader (which consume policy.exceptions only) ever honored. Materialization failure after a recorded approval returns an explicit 500 naming the state (approval recorded, grant missing) rather than a silent success. The console’s per-finding Waivers/Exceptions tab (Vulnerabilities pillar) creates requests via POST /api/v1/policy/exception/request (scope exceptions:request, granted to stella-ops-ui by Authority migration 021) and lists them via GET /api/v1/policy/exception/requests.

Audit Trail

All exception actions are logged to policy.exception_approval_audit with actor, timestamp, and IP address.

TTL Enforcement

CLI Integration

# Request exception
stella exception request --cve CVE-2024-12345 --purl "pkg:npm/lodash@4.17.20" --reason-code CompensatingControl --rationale "WAF rules deployed" --ttl 30 --gate-level G3 --ticket JIRA-12345

# Approve exception
stella exception approve <request-id> --comment "Verified controls"

# Reject exception
stella exception reject <request-id> --reason "Insufficient mitigation"

# List pending approvals
stella exception list --status pending

# Check status
stella exception status <request-id>

Implementation Reference

ComponentSource File
Entitiessrc/Policy/__Libraries/StellaOps.Policy.Persistence/Postgres/Models/ExceptionApprovalEntity.cs
Repositorysrc/Policy/__Libraries/StellaOps.Policy.Persistence/Postgres/Repositories/ExceptionApprovalRepository.cs
Rules Servicesrc/Policy/StellaOps.Policy.Engine/Services/ExceptionApprovalRulesService.cs
API Endpoints (live, merged host)src/Policy/StellaOps.Policy.Engine/Endpoints/Gateway/ExceptionApprovalEndpoints.cs (mapped via app.MapExceptionApprovalEndpoints() in Program.cs)
API Endpoints (standalone gateway)src/Policy/StellaOps.Policy.Gateway/Endpoints/ExceptionApprovalEndpoints.cs
CLI Commandssrc/Cli/StellaOps.Cli/Commands/ExceptionCommandGroup.cs
Migrationpolicy.exception_approval_requests / _audit / _rules tables in src/Policy/__Libraries/StellaOps.Policy.Persistence/Migrations/001_v1_policy_baseline.sql (lines 730 / 770 / 789). Both the earlier 013_exception_approval.sql and the pre-1.0 001_initial_schema.sql are archived under Migrations/_archived/pre_1.0/ and are not embedded.

Related Documentation:

Governance Compatibility Endpoints

The console governance workspaces still target a tenant-scoped compatibility surface under /api/v1/governance/*, but that surface is now explicitly split between persisted read paths and truthful unsupported behavior.

Contract requirements:

Implementation reference:


7 · Security & Tenancy

Determinism enforcement (DOCS-POLICY-DET-01)


8 · Observability

Metrics are emitted from StellaOps.Policy.Engine/Telemetry/PolicyEngineTelemetry.cs (consult the source for the authoritative tag set). Representative instruments:


9 · Offline / Bundle Integration


10 · Testing & Quality


11 · Compliance Checklist


Archived advisories below provide historical design context, not current capability or runtime authority. Re-verify their claims against the owning source and current contracts before use.


13 · Policy Interop Layer

Sprint: SPRINT_20260122_041_Policy_interop_import_export_rego

The Interop Layer provides bidirectional policy exchange between Stella’s native C# gate engine and OPA/Rego. The C# engine remains primary; Rego serves as an interoperability adapter for teams using OPA-based toolchains.

13.1 · Supported Formats

FormatSchemaDirectionNotes
PolicyPack v2 (JSON)policy.stellaops.io/v2Import + ExportCanonical format with typed gates, environment overrides, remediation hints
PolicyPack v2 (YAML)policy.stellaops.io/v2Import + ExportDeterministic YAML with sorted keys; YAML→JSON roundtrip for validation
OPA/Regopackage stella.releaseExport (+ Import with pattern matching)Deny-by-default pattern, remediation output rules

13.2 · Architecture

graph TD
    subgraph Interop["StellaOps.Policy.Interop"]
        Exporter[JsonPolicyExporter / YamlPolicyExporter / RegoPolicyExporter]
        Importer[JsonPolicyImporter / YamlPolicyImporter / RegoPolicyImporter]
        DiffMerge[PolicyDiffMergeEngine]
        Validator[PolicySchemaValidator]
        Generator[RegoCodeGenerator]
        Resolver[RemediationResolver]
        OPA[EmbeddedOpaEvaluator]
        Detector[FormatDetector]
    end
    subgraph Consumers
        CLI[stella policy export/import/validate/evaluate/diff/merge]
        API[Policy Engine API /api/v1/policy/interop]
        UI[Policy Editor UI]
    end

    CLI --> Exporter
    CLI --> Importer
    CLI --> Validator
    CLI --> DiffMerge
    API --> Exporter
    API --> Importer
    API --> Validator
    API --> DiffMerge
    UI --> API

    Exporter --> Generator
    Exporter --> Resolver
    Importer --> Detector
    Importer --> OPA
    Generator --> Resolver

13.3 · Gate-to-Rego Translation

Each C# gate type maps to a Rego deny rule pattern:

Gate TypeRego PatternRemediation Code
CvssThresholdGateinput.cvss.score >= thresholdCVSS_EXCEED
SignatureRequiredGatenot input.dsse.verifiedSIG_MISS
EvidenceFreshnessGatenot input.freshness.tstVerifiedFRESH_EXPIRED
SbomPresenceGatenot input.sbom.canonicalDigestSBOM_MISS
MinimumConfidenceGateinput.confidence < thresholdCONF_LOW
UnknownsBudgetGateinput.unknownsRatio > thresholdUNK_EXCEED
ReachabilityRequirementGatenot input.reachability.statusREACH_REQUIRED

13.4 · Remediation Hints

When a gate blocks, the system resolves structured remediation hints:

Priority: Gate-defined hint > Built-in defaults > null

RemediationHint:
  Code:        Machine-readable (e.g., "CVSS_EXCEED")
  Title:       Human-readable summary
  Actions[]:   CLI command templates with {placeholders}
  References:  External documentation links
  Severity:    critical | high | medium | low

Placeholders ({purl}, {image}, {reason}) are resolved via RemediationContext at evaluation time.

13.5 · Determinism

All exports and evaluations are deterministic:

13.6 · YAML Format Support

Sprint: SPRINT_20260208_048_Policy_policy_interop_framework

YAML export/import operates on the same PolicyPackDocument model as JSON. The YAML format is useful for human-editable policy files and GitOps workflows.

Export (YamlPolicyExporter : IPolicyYamlExporter):

Import (YamlPolicyImporter):

Format Detection (FormatDetector):

13.7 · Policy Diff/Merge Engine

Sprint: SPRINT_20260208_048_Policy_policy_interop_framework

The PolicyDiffMergeEngine (IPolicyDiffMerge) compares and merges PolicyPackDocument instances structurally.

Diff produces PolicyDiffResult containing:

Merge applies one of three strategies via PolicyMergeStrategy:

StrategyBehavior
OverlayWinsOverlay values take precedence on conflict
BaseWinsBase values take precedence on conflict
FailOnConflictReturns error with conflict details

Merge output includes the merged PolicyPackDocument and a list of PolicyMergeConflict items (path, base value, overlay value).

13.8 · Implementation Reference

ComponentSource File
Contractssrc/Policy/__Libraries/StellaOps.Policy.Interop/Contracts/PolicyPackDocument.cs
Remediation Modelssrc/Policy/__Libraries/StellaOps.Policy.Interop/Contracts/RemediationModels.cs
Interfacessrc/Policy/__Libraries/StellaOps.Policy.Interop/Abstractions/
JSON Exportersrc/Policy/__Libraries/StellaOps.Policy.Interop/Export/JsonPolicyExporter.cs
YAML Exportersrc/Policy/__Libraries/StellaOps.Policy.Interop/Export/YamlPolicyExporter.cs
JSON Importersrc/Policy/__Libraries/StellaOps.Policy.Interop/Import/JsonPolicyImporter.cs
YAML Importersrc/Policy/__Libraries/StellaOps.Policy.Interop/Import/YamlPolicyImporter.cs
Rego Generatorsrc/Policy/__Libraries/StellaOps.Policy.Interop/Rego/RegoCodeGenerator.cs
Rego Importersrc/Policy/__Libraries/StellaOps.Policy.Interop/Import/RegoPolicyImporter.cs
Diff/Merge Enginesrc/Policy/__Libraries/StellaOps.Policy.Interop/DiffMerge/PolicyDiffMergeEngine.cs
Embedded OPAsrc/Policy/__Libraries/StellaOps.Policy.Interop/Evaluation/EmbeddedOpaEvaluator.cs
Remediation Resolversrc/Policy/__Libraries/StellaOps.Policy.Interop/Evaluation/RemediationResolver.cs
Format Detectorsrc/Policy/__Libraries/StellaOps.Policy.Interop/Import/FormatDetector.cs
Schema Validatorsrc/Policy/__Libraries/StellaOps.Policy.Interop/Validation/PolicySchemaValidator.cs
DI Registrationsrc/Policy/__Libraries/StellaOps.Policy.Interop/DependencyInjection/PolicyInteropServiceCollectionExtensions.cs
CLI Commandssrc/Cli/StellaOps.Cli/Commands/Policy/PolicyInteropCommandGroup.cs
Policy Engine APIsrc/Policy/StellaOps.Policy.Engine/Interop/Endpoints/PolicyInteropEndpoints.cs
JSON Schemadocs/schemas/policy-pack-v2.schema.json

13.9 · CLI Interface

# Export to Rego
stella policy export --file policy.json --format rego --output-file release.rego

# Import with validation
stella policy import --file external.rego --validate-only

# Validate policy document
stella policy validate --file policy.json --strict

# Evaluate with remediation hints
stella policy evaluate --policy baseline.json --input evidence.json --environment production

Exit codes: 0 = success/allow, 1 = warn, 2 = block/errors, 10 = input-error, 12 = policy-error.

13.10 · Policy Engine API

Group: /api/v1/policy/interop with tag PolicyInterop

MethodPathAuth PolicyDescription
POST/exportpolicy:readExport policy to format
POST/importpolicy:writeImport policy from format
POST/validatepolicy:readValidate policy document
POST/evaluatepolicy:readEvaluate policy against input
GET/formatspolicy:readList supported formats

13.11 · OPA Supply Chain Evidence Input

Sprint: SPRINT_0129_001_Policy_supply_chain_evidence_input

OPA policies can optionally access comprehensive supply chain evidence beyond basic VEX merge results. When PolicyGateContext.SupplyChainEvidence is populated, the following fields become available in the OPA input:

Input FieldTypeDescription
artifact.digeststringArtifact digest (e.g., sha256:abc...)
artifact.mediaTypestringOCI media type
artifact.referencestringFull artifact reference
sbom.digeststringSBOM content hash
sbom.formatstringFormat identifier (e.g., cyclonedx-1.7, spdx-3.0.1)
sbom.componentCountintNumber of components
sbom.contentobjectOptional inline SBOM JSON
attestations[]arrayAttestation references
attestations[].digeststringDSSE envelope digest
attestations[].predicateTypestringin-toto predicate type URI
attestations[].signatureVerifiedboolSignature verification status
attestations[].rekorLogIndexlongTransparency log index
transparency.rekor[]arrayRekor receipts
transparency.rekor[].logIdstringLog identifier
transparency.rekor[].uuidstringEntry UUID
transparency.rekor[].logIndexlongLog position
transparency.rekor[].integratedTimelongUnix timestamp
transparency.rekor[].verifiedboolReceipt verification status
vex.mergeDecisionobjectVEX merge decision
vex.mergeDecision.algorithmstringMerge algorithm (e.g., trust-weighted-lattice-v1)
vex.mergeDecision.inputs[]arraySource documents with trust weights
vex.mergeDecision.decisions[]arrayPer-vulnerability decisions with provenance

Code locations:

Example Rego policy using evidence:

package stella.supply_chain

default allow = false

# Require SBOM presence
allow {
    input.sbom.digest != ""
    input.sbom.componentCount > 0
}

# Require verified attestation with SLSA provenance
allow {
    some att in input.attestations
    att.predicateType == "https://slsa.dev/provenance/v1"
    att.signatureVerified == true
}

# Require transparency log entry within 24 hours
allow {
    some receipt in input.transparency.rekor
    receipt.verified == true
    time.now_ns() - (receipt.integratedTime * 1000000000) < 86400000000000
}

Last updated: 2026-02-09 (Sprint 049 — Proof Studio UX).

14 · Proof Studio (Explainable Confidence Scoring)

The Proof Studio UX provides visual, auditable evidence chains for every verdict decision. It bridges existing verdict rationale, score explanation, and counterfactual simulation data into composable views.

14.1 · Library Layout

StellaOps.Policy.Explainability/
├── VerdictRationale.cs              # 4-line structured rationale model
├── VerdictRationaleRenderer.cs      # Content-addressed render (text/md/JSON)
├── IVerdictRationaleRenderer.cs     # Renderer interface
├── ProofGraphModels.cs              # Proof graph DAG types
├── ProofGraphBuilder.cs             # Deterministic graph builder
├── ScoreBreakdownDashboard.cs       # Score breakdown dashboard model
├── ProofStudioService.cs            # Composition + counterfactual integration
├── ServiceCollectionExtensions.cs   # DI registration
└── GlobalUsings.cs

14.2 · Proof Graph

A proof graph is a directed acyclic graph (DAG) that visualizes the complete evidence chain from source artifacts to a final verdict decision.

Node TypeDepthPurpose
Verdict0Root: the final verdict + composite score
PolicyRule1Policy clause that triggered the decision
Guardrail1Score guardrail (cap/floor) that modified the score
ScoreComputation2Per-factor score contribution
ReachabilityAnalysis3Reachability evidence leaf
VexStatement3VEX attestation leaf
Provenance3Provenance attestation leaf
SbomEvidence3SBOM evidence leaf
RuntimeSignal3Runtime detection signal leaf
AdvisoryData3Advisory data leaf
Counterfactual0What-if hypothesis (overlay)

Edge relations: ProvidesEvidence, ContributesScore, Gates, Attests, Overrides, GuardrailApplied.

Graph IDs are content-addressed (pg:sha256:...) from deterministic sorting of node and edge identifiers.

14.3 · Score Breakdown Dashboard

The ScoreBreakdownDashboard exposes per-factor contributions with weighted contributions and percentages:

ScoreBreakdownDashboard
├── CompositeScore (int)
├── ActionBucket (string)
├── Factors[] → FactorContribution
│   ├── FactorId / FactorName
│   ├── RawScore, Weight → WeightedContribution (computed)
│   ├── Confidence, IsSubtractive
│   └── PercentageOfTotal
├── GuardrailsApplied[] → GuardrailApplication
│   ├── ScoreBefore → ScoreAfter
│   └── Reason, Conditions
├── PreGuardrailScore
├── Entropy
└── NeedsReview

14.4 · Counterfactual Explorer

The AddCounterfactualOverlay() method on IProofGraphBuilder adds hypothetical nodes to an existing proof graph. A CounterfactualScenario specifies factor overrides (factorId → hypothetical score) and an optional resulting composite score. The overlay:

  1. Creates a Counterfactual node at depth 0 with the scenario label.
  2. Connects overridden factor score nodes to the counterfactual node via Overrides edges.
  3. Recomputes the content-addressed graph ID, making each scenario distinctly identifiable.

14.5 · Proof Studio Service (Integration)

The IProofStudioService is the primary integration surface:

MethodInputOutput
Compose(request)ProofStudioRequest (rationale + optional score factors + guardrails)ProofStudioView (proof graph + optional score breakdown)
ApplyCounterfactual(view, scenario)Existing view + CounterfactualScenarioUpdated view with overlay

The service bridges ScoreFactorInput (from scoring engine) to FactorContribution models and formats factor names for UI display.

14.6 · DI Registration

services.AddVerdictExplainability();
// Registers:
//   IVerdictRationaleRenderer → VerdictRationaleRenderer
//   IProofGraphBuilder → ProofGraphBuilder
//   IProofStudioService → ProofStudioService

14.7 · OTel Metrics

MetricTypeDescription
stellaops.proofstudio.views_composed_totalCounterProof studio views composed
stellaops.proofstudio.counterfactuals_applied_totalCounterCounterfactual scenarios applied

14.8 · Tests

15 · Advisory Gap Status (2026-03-04 Batch)

Status: implementation delivered in Sprint 306.

Legacy fixture note:

Closure sprint:

Scanner report policy publication

Scanner report preview reads the authenticated tenant’s latest immutable report-policy snapshot from Policy-owned storage. It does not select a policy pack or reinterpret compiled policy DSL. Fresh installations publish the missing initial snapshot with POST /api/policy/scanner-report-snapshots, using both policy:edit and policy:activate. The request contains expectedRevision (zero only when absent) and a policy object conforming to the existing Policy JSON schema. Policy validates the document before writing, records the authenticated subject and timestamp, and emits the existing snapshot audit action. A stale revision returns 409; the existing unique tenant/policy/version constraint also prevents concurrent overwrites.

This endpoint appends report-policy versions in policy.snapshots; it does not change published policy packs, pack approval records, release bindings, or gate activation. Requests without a valid tenant, required scopes, or a valid policy remain refused. Scanner’s report response identifies the selected revision and policy digest, and its normal signing/attestation flow remains required.