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:tenantclaim viaIStellaOpsTenantAccessor. The legacyX-Tenant-Idheader is stripped at ingress and is never honoured as a tenancy source. Literal"default"tenant fallbacks have been removed fromAirGap/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. SeeSPRINT_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.
1 · Responsibilities & Constraints
- Compile and evaluate
stella-dsl@1policy packs into deterministic verdicts. - Join SBOM inventory, Concelier advisories, and Excititor VEX evidence via canonical linksets and equivalence tables.
- Evaluate SBOM license expressions against policy (SPDX AND/OR/WITH/+), emitting compliance findings and attribution requirements for gate decisions.
- Materialise effective findings into Policy-owned PostgreSQL tables (e.g.
policy.evaluation_runs+policy.explanations; see §4) with append-only history and produce explain traces. - Emit CVSS v4.0 receipts with canonical hashing and policy replay/backfill rules; store tenant-scoped receipts with RBAC; export receipts deterministically (UTC/fonts/order) and flag v3.1→v4.0 conversions (see Sprint 0190 CVSS-GAPS-190-014 /
docs/modules/policy/cvss-v4.md). - Emit per-finding OpenVEX decisions anchored to reachability evidence, forward them to Signer/Attestor for DSSE/Rekor, and publish the resulting artifacts for bench/verification consumers.
- Consume reachability lattice decisions (
ReachDecision,docs/modules/reach-graph/guides/lattice.md) to drive confidence-based VEX gates (not_affected / under_investigation / affected) and record the policy hash used for each decision. - Own the Assurance control-register schema, lifecycle stream, and read APIs: NIS2 is the first pack adapter, not a product-wide regional assumption. Policy documents may carry
complianceMappingsentries for the thirteen NIS2 thematic areas, Policy persistence stores control entries, replay events, and content-addressed snapshots for SoA export pinning,GET /api/policy/control-register/nis2preserves the NIS2 compatibility shape, andGET /api/policy/control-register/frameworks/nis2exposes the genericassurance-control-register-v1projection without downstream persistence coupling (see NIS2 control register). - Honor hybrid reachability attestations: graph-level DSSE is required input; when edge-bundle DSSEs exist, prefer their per-edge provenance for quarantine, dispute, and high-risk decisions. Quarantined edges (revoked in bundles or listed in Unknowns registry) must be excluded before VEX emission. See
docs/modules/reach-graph/guides/hybrid-attestation.mdfor verification runbooks and offline replay steps. - Enforce shadow + coverage gates for new/changed policies: shadow runs record findings without enforcement; promotion blocked until shadow and coverage fixtures pass (see lifecycle/runtime docs). CLI/Console enforce attachment of lint/simulate/coverage evidence.
- Operate incrementally: react to change streams (advisory/vex/SBOM deltas) with ≤ 5 min SLA.
- Provide simulations with diff summaries for UI/CLI workflows without modifying state.
- Enforce strict determinism guard (no wall-clock, RNG, network beyond allow-listed services) and RBAC + tenancy via Authority scopes.
- Enforce fail-closed gate integrity: missing release baselines deny by default, signing secrets must be explicitly configured, canonical verdict hashes cover decision content rather than wall-clock inputs, transparency mode is explicit for offline or Rekor-backed deployments, and evidence paths never treat unsigned, base64-only, or placeholder local signatures as trusted production evidence.
- Support sealed/air-gapped deployments with offline bundles and sealed-mode hints.
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)
- Policy Gateway now initializes StellaOps localization with
AddStellaOpsLocalization(...),AddTranslationBundle(...),AddRemoteTranslationBundles(),UseStellaOpsLocalization(), andLoadTranslationsAsync(). - Locale resolution order for request-facing strings is deterministic:
X-Localeheader ->Accept-Languageheader -> default locale (en-US). - Translation sources are layered deterministically: shared embedded
commonbundle -> Policy embedded bundle (Translations/*.policy.json) -> Platform runtime override bundle. - The rollout localizes selected request validation and readiness responses for
en-USandde-DE.
1.2 · Simulation compatibility contract (Sprint 20260309_011)
- The legacy compatibility surface under
/policyno longer fabricates tenant-scoped simulation state in live hosts. StellaOps.Policy.Gatewayand the mergedStellaOps.Policy.Enginegateway surface now return501 problem+jsonfor the/policysimulation compatibility routes until a durable Policy Registry backend is wired.- This explicitly covers the previous shadow-mode, simulation-history, simulation-compare, exception-compatibility, merge-preview, and batch-evaluation compatibility routes that had been backed by process-local dictionaries.
StellaOps.Policy.Registrystill contains in-memory store implementations for its internal test harness, but the public runtime DI helper that exposed those registrations was removed. Registry in-memory storage is now testing-only and is no longer advertised as a live host composition path.
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:
- API host exposes lifecycle, run, simulate, findings, gate, governance, and exception endpoints with OAuth bearer + scope enforcement (resource-server validation; DPoP, where required, is enforced upstream at Authority/gateway).
- Router-forwarded requests are authenticated from the gateway-signed identity envelope in both Policy executable hosts.
UseIdentityEnvelopeAuthentication()runs before bearer authentication, authorization, and tenant middleware, verifies the configured signing key and envelope validity window, and hydrates the subject, tenant, project, roles, andscopeclaims. A missing, invalid, expired, or unverifiable envelope remains unauthenticated, so protected routes fail closed with 401/403. Hydration does not grant authorization: each endpoint still requires its exact Policy scope (for example,policy:readfor the shadow-config read path), while direct callers may continue through normal bearer authentication. - The merged Policy Engine host maps both budget endpoint families before
TryRefreshStellaRouterEndpoints: unknown-budget configuration/status under/api/v1/policy/budgetsand release risk-budget ledger/enforcement under/api/v1/policy/budget. Both route groups applyRequireTenantContext()to every route. This endpoint filter is load-bearing for Valkey/Router dispatch, which invokes the matched ASP.NET endpoint without running Policy’s normal tenant middleware: it copies the already verified shared Router tenant identity into Policy’s request-local tenant accessor and clears it after the handler returns. Tenant-sensitive unknown-budget reads then resolve that identity throughIStellaOpsTenantResolver: canonical UUIDs pass through, while active named tenants such asdefaultresolve throughshared.tenantsto the directory UUID used by both the repository call and PostgreSQL RLS session. Missing context fails at the filter, while inactive or unknown identifiers fail closed before repository construction; there is no public/default fallback. Reads requirepolicy:read, while mutations retain their endpoint-specific operate/edit scopes. The host composes the existing scopedUnknownsRepository,PostgresBudgetStore,BudgetLedger, andBudgetConstraintEnforceroverPolicyDataSource;AddPolicyPostgresStorageremains the startup-migration authority for the shared Policy schema. Router advertisement is derived from the hostEndpointDataSource, so a defined endpoint extension that is not called fromProgram.csis not a live or routable contract. - Orchestrator manages run scheduling/fairness; writes run tickets to queue, leases jobs to worker pool.
- Workers evaluate policies using cached IR; join external services via tenant-scoped clients; pull immutable advisories/VEX from the raw stores; write derived overlays to PostgreSQL and optional explain bundles to blob storage.
- Observability (metrics/traces/logs) integrated via OpenTelemetry (not shown).
- Canonical policy pack runtime state is durable: pack containers, revision activation state, approval history, bundle digests, signed payloads, AOC metadata, and persisted DSL source now live in PostgreSQL (
policy.packs,policy.pack_versions). Runtime reload reconstructs compiled bundle documents by recompiling the persisted source text, so pack activation/evaluation survives host restart without an in-memory bundle cache. - Canonical violation runtime state is also durable: emitted violation events, fused severities, and persisted conflict records now live in PostgreSQL (
policy.violation_events,policy.violation_fusion_results,policy.conflicts). Restarted hosts reuse persisted event/fusion/conflict state through the/policy/violations/*API instead of rebuilding from an in-memory store. - Additional canonical Policy runtime state is now durable as well: verification policies, attestation reports, risk scoring jobs, console export jobs/executions/bundles, policy-pack bundle imports, and sealed mode state persist in PostgreSQL (
policy.verification_policies,policy.attestation_reports,policy.risk_scoring_jobs,policy.console_export_*,policy.policy_pack_bundle_imports,policy.sealed_mode_states) and survive host restart without in-memory recovery logic. - Human governance decisions are durable and can carry a verified operator signature (ADR-025). The exception/waiver approve (
policy.exception_approval_requests, M-of-N), the gate-bypass decision (policy.gate_bypass_audit, viaIGateBypassAuditor.RecordBypassAsync— not the 501PolicySimulationEndpointsstub), and the risk-override approve (policy.risk_overrides) each persist an optionaloperator-decision@v1DSSE envelope in nullable signature columns (operator_decision_envelope/_keyid/_digest+operator_signature_verified), verified throughIOperatorDecisionVerifier/ICryptoProviderRegistry(never bare BCL) and bound to the approver’s verified tokensub. Enforcement of a required signature is gated per-decision-type behind a default-off options flag (Policy:ExceptionApprovalSigning:RequireOperatorSignature,Policy:GateBypassAudit:RequireOperatorSignature,Policy:RiskOverrideSigning:RequireOperatorSignature), so existing flows are byte-for-byte unchanged until a signing client lands. The risk-override decision record was previously in-memory only; it is now durable inpolicy.risk_overrides(backingOverrideServicethrough theIRiskOverrideStore/PostgresRiskOverrideStorewrite-through seam), so an approve and its operator signature survive host restart, and the in-memory read model is rehydrated from the store on startup (RiskOverrideHydrationService). A parallel re-point applies to the VEX override decision in Findings (RealVexOverrideAttestorAdapter, SPRINT_20260608_015 POLICY-OPSIGN-005): when the operator supplies anoperator-decision@v1envelope, the produced VEX-override attestation is anchored to the operator’s enrolled key (verified throughIOperatorDecisionVerifier/ICryptoProviderRegistry, non-custodial) instead of the service signing key. - OSK-P1 supersedes the preceding global enforcement description for production composition. Required signing is resolved once per tenant from the default-empty
Policy:OperatorSigningRequirement:Tenantsmap; exception approval, gate bypass, and risk override all consume that same decision. A tenant is required only when its exact id/slug maps totrue; absent/false entries remain OFF, and the legacy per-decisionRequireOperatorSignaturebooleans cannot globally enable a production tenant. - Operator decision-signing key resolution is cache-first with Authority read-through.
PolicyTrustedKeyOperatorDecisionResolverfirst resolves the envelopekeyidfrompolicy.trusted_keys; on a miss, and only whenIssuerDirectory:Client:BaseAddress,Policy:ExceptionApprovalSigning:IssuerId, and a request tenant are configured, Policy pulls the public operator key from IssuerDirectory and upserts the read-sidepolicy.trusted_keysrow. Browser raw ES256DssePublicKeymaterial is normalized to SPKI before caching. Missing client/issuer/tenant or IssuerDirectory failure is fail-closed. - OSK-P5 bounds cached lifecycle state as well as key material. After
PostgresTrustedKeyRegistryOptions.CacheTtlSeconds(default 300 seconds), the operator-decision resolver refreshes an existing verification-view row from IssuerDirectory, persists provider-changeRetiredstatus +retiredAtcutoff, and invalidates the prior in-memory verify-only entry. A due refresh that cannot run or fails does not fall back to stale Active state. Historical attestation verification still evaluates the key against the signed decision timestamp, but each exception approval, risk override, and gate-bypass grant then applies a separate exact-key authorization at current server time. The old key can therefore verify append-only history while being hard-blocked from creating a new grant at or after its grace cutoff. - Promote-time CVE gates keep Scanner runtime status separate from Scanner evidence.
PostgresFindingsLookupreads the latestscanner.scan_runtime_staterow to surface a freshpending/runningscan asreachability.state=computing, but it reads component PURLs and completed reachability facts from the latest successfulscanner.scan_evidence_projectionsrow for the digest. A pending/running row older than six hours is treated as stale and degrades tounknown, so a dead worker cannot leave findings in a permanent computing state or hide the last successful SBOM projection. - Release-time policy evaluation caches by policy bundle digest, opaque subject, and a deterministic digest of the current resolved findings. The findings lookup runs before cache selection for CVE-aware packs, so advisory, match-state, severity, package, or reachability changes invalidate an earlier allow/deny response while an unchanged findings projection still reuses the cached decision and attestation.
- Assurance control-register state is durable in PostgreSQL (
policy.control_register_entries,policy.control_register_events,policy.control_register_snapshots) through the current NIS2 storage adapter. The event stream is tenant-scoped and replay ordered, and event details includeframeworkId/packIdmetadata. Runtime readers that need the legacy NIS2 shape useGET /api/policy/control-register/nis2, which returns exactly thirteen area summaries, owner/review/evidence/tenant-override refs, audit event ids, and deterministic cursor pages. Runtime readers that need the module-neutral contract useGET /api/policy/control-register/frameworks/nis2, which returnsassurance-control-register-v1with genericareaOrDomain,externalRefs, and evidence source labels. - CRA technical-documentation provenance is exposed through a Policy-owned projection, not by deriving values from generic Policy ledger or console exports. ExportCenter remains the owner of
cra-tech-file-v1bundle production and EvidenceLocker remains the owner of the EU regulatory artifact ledger. When the CRA export workflow supplies Policy with a provenance snapshot, Policy stores it inpolicy.cra_technical_documentation_provenanceand serves the latest tenant snapshot fromGET /api/policy/compliance/cra/technical-documentation/provenance/{tenantId}. The response contract is{ tenantId, exportId?, generatedAtUtc?, status, fields[], unprovenancedFieldCount }, wherestatusisok,unprovenanced, ormissing; field entries are sorted bypathand preserve nullsourceRef,signerRef, andsignedAtvalues.POST /api/policy/compliance/cra/technical-documentation/provenance/{tenantId}is the Policy ingestion boundary for the snapshot and uses source contractpolicy-cra-technical-documentation-provenance-v1:exportIdandgeneratedAtUtcare supplied by the CRA export run, while each field-levelsourceRef,signerRef, andsignedAtis supplied from the CRA technical-file signing/evidence provenance. Requests fail closed when the route tenant id does not match the envelope-boundstellaops:tenantclaim. - The standalone Policy Gateway no longer uses a process-local outbound token cache. Client-credentials token reuse now flows through
MessagingTokenCachebacked by PostgreSQL transport tables on the Policy DSN, matching the engine’s durable messaging-cache pattern. - The merged gateway governance surface is no longer backed by process-local dictionaries.
/api/v1/governance/sealed-mode/statusand/api/v1/governance/sealed-mode/toggleresolve through the persistedISealedModeService,/api/v1/governance/risk-profiles*now exposes only configuration-backed read-only projections plus stateless validation, and unsupported governance write/trust-weight/staleness/conflict/audit compatibility routes return truthful501responses instead of fabricated state. - The standalone Policy Gateway also no longer fabricates governance compatibility state. Its
/api/v1/governance/*compatibility routes now return truthful501responses unless a real governance backend is explicitly wired behind the host.
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.
- No standalone operator-facing key change. The standalone composition’s lazy config bridge (
AddPrefixAlias) maps the existingPostgres:Policy:*operator keys onto themessaging:postgres:*keys the Postgres transport plugin binds (ConnectionString,Schema,AutoCreateTables,CommandTimeoutSeconds), and defaultsmessaging:transport=postgres. The alias is resolved at bind time so config layering order is preserved (the plugin’s deferred.Bind()/ValidateOnStart()reads the final values, and a test host’s in-memoryPostgres:Policy:*overrides are still honoured). The plugin’s own[Required] ConnectionStringvalidation fails fast at startup when the DSN is genuinely missing. - Mounted-bundle discovery. The standalone composition resolves the signed bundle directory
<Messaging:BundleRoot>/<Messaging:Profile>(defaults/app/plugins/messaging/recommended) and trust root/app/etc/certificates/trust-roots/plugins/messaging/cosign.pub(overridable viaMessaging:PluginDirectory/Messaging:TrustRootPath/Messaging:EnforceSignatureVerification). Image-resident transports loaded into the Default ALC are not re-gated (signed at build time); the directory load path stays fail-closed (see §2.0b). - Composition coverage.
PolicyMessagingCompositionTestsproves the canonical Router-enabled Valkey registration is the single effective factory, proves the Router-disabled Postgres fallback, and rejects zero/duplicate factory ownership. The standalone API fixture still usesmessaging:transport=postgresplus a Default-ALCModuleInitializerto make fallback discovery deterministic.
Current canonical Compose is Router-enabled and uses the admitted signed
base/ Valkey bundle. A signedrecommended/ 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:
- manifest binding — manifest
id == bundle directory name, modulemessaging, contract versionmessaging.backend.v1, and the per-transport required capabilitymessaging:backend:<transport>(e.g.messaging:backend:postgres, derived from the bundle directory name); - per-assembly SHA-256 vs the manifest digest, plus a path-traversal guard;
- detached RSA-PKCS1-SHA256 verification of
<assembly>.sigviaOfflineDevRsaSha256PluginVerifierwithAllowUnsigned = falseagainst the configured trust root.
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
- Raw-only reads. Evaluation workers access
advisory_raw/vex_rawvia tenant-scoped PostgreSQL clients or the Concelier/Excititor raw APIs. No Policy Engine component is permitted to mutate these tables. - Guarded ingestion.
AOCWriteGuardrejects forbidden fields before data reaches the raw stores. Policy tests replay knownERR_AOC_00xviolations to confirm ingestion compliance. - Change streams as contract. Run orchestration stores resumable cursors for raw change streams. Replays of these cursors (e.g., after failover) must yield identical materialisation outcomes.
- Derived stores only. All severity, consensus, and suppression state lives in Policy-owned PostgreSQL tables (
policy.evaluation_runs,policy.explanations,policy.risk_scores,policy.violation_events, etc.; see §4) and explain bundles owned by Policy Engine. Provenance fields link back to raw document IDs so auditors can trace every verdict. - Authority scopes. Only the Policy Engine service identity holds
effective:write(StellaOpsScopes.EffectiveWrite, enforced on/api/v1/authority/effective-policies). Read access to materialised findings usesfindings:read(StellaOpsScopes.FindingsRead); there is noeffective:readscope. Ingestion identities retainadvisory:*/vex:*scopes, ensuring separation of duties enforced by Authority and the API Gateway.
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 undersrc/Policy/;StellaOps.Policy.Determinization/lives undersrc/Policy/__Libraries/) or a shared library (theStellaOps.Policygates library is also undersrc/Policy/__Libraries/). The merged engine host (Program.cs) wires all of these plus the former standalone gateway endpoints underEndpoints/Gateway/.
| Module | Responsibility | Notes |
|---|---|---|
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), proof-spine handling (VexProofSpineService), persist artifacts for Export Center / bench repo. | Registered via AddVexDecisionEmitter() (POLICY-VEX-401-006); integrates with Signer predicate stella.ops/vexDecision@v1 and Attestor Rekor logging. |
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
}
}
| Option | Type | Default | Description |
|---|---|---|---|
SignalWeights | Object | See above | Relative 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). |
PriorDistribution | Enum | Conservative | Prior distribution for missing signals. Options: Conservative (pessimistic), Neutral, Optimistic. Affects uncertainty tier classification when signals are unavailable. |
ConfidenceHalfLifeDays | Double | 14.0 | Half-life period for confidence decay in days. Confidence decays exponentially: exp(-ln(2) * age_days / half_life_days). |
ConfidenceFloor | Double | 0.1 | Minimum confidence value after decay (0.0-1.0). Prevents confidence from decaying to zero, maintaining baseline trust even for very old observations. |
ManualReviewEntropyThreshold | Double | 0.60 | Entropy threshold for triggering manual review (0.0-1.0). Findings with entropy ≥ this value require human intervention due to insufficient signal coverage. |
RefreshEntropyThreshold | Double | 0.40 | Entropy threshold for triggering signal refresh (0.0-1.0). Findings with entropy ≥ this value should attempt to gather more signals before verdict. |
StaleObservationDays | Double | 30.0 | Maximum age before an observation is considered stale (days). Used in conjunction with decay calculations and auto-refresh triggers. |
EnableDetailedLogging | Boolean | false | Enable verbose logging for entropy/decay/trust calculations. Useful for debugging but increases log volume significantly. |
EnableAutoRefresh | Boolean | true | Automatically trigger signal refresh when entropy exceeds RefreshEntropyThreshold. Requires integration with signal providers. |
MaxSignalQueryRetries | Integer | 3 | Maximum retry attempts for failed signal provider queries before marking signal as unavailable. |
Metrics emitted:
stellaops_determinization_uncertainty_entropy(histogram, unit: ratio): Uncertainty entropy score per CVE/PURL pair. Tags:cve,purl.stellaops_determinization_decay_multiplier(histogram, unit: ratio): Confidence decay multiplier based on observation age. Tags:half_life_days,age_days.
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:
- Calculate uncertainty entropy from signal snapshot
- Aggregate weighted signal scores via TrustScoreAggregator
- Compute K4 lattice verdict (Unknown/True/False/Conflict)
- Extract dimension scores (BaseSeverity, Reachability, Evidence, Provenance)
- Compute weighted final score in basis points (0-10000)
- Determine risk tier (Info/Low/Medium/High/Critical)
- 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 |
|---|---|
| ≥ 9000 | Critical |
| ≥ 7000 | High |
| ≥ 4000 | Medium |
| ≥ 1000 | Low |
| < 1000 | Info |
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"
}
}
sbom.licenseexposes the compliance report (findings, conflicts, inventory).sbom.license_statusexposespass,warn, orfail(orunknownwhen disabled).- Failures set the policy verdict status to
blockedand emitlicense.*annotations. - Trademark notice obligations are tracked alongside attribution requirements and produce warn-level findings.
- License compliance reports support JSON, text/markdown/html, legal-review, and PDF outputs.
- Category breakdown includes percent totals and chart renderings (ASCII chart in text/markdown/legal-review/PDF, pie chart in HTML).
3.3 - NTIA compliance configuration
NTIA minimum-elements validation runs when enabled under ntiaCompliance.
{
"ntiaCompliance": {
"enabled": true,
"enforceGate": false,
"policyPath": "policies/ntia-policy.yaml"
}
}
sbom.ntiaexposes NTIA compliance details (elements, findings, supplier status).sbom.ntia_statusexposespass,warn,fail, orunknown.- NTIA compliance can be configured as an advisory-only check or a release gate via
enforceGate. - The NTIA policy supports element selection, supplier validation (placeholder patterns, trusted/blocked lists), and framework-specific requirements.
- Reports support JSON, text/markdown/html, and PDF output for regulatory submissions.
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 underMigrations/_archived/pre_1.0/(and.../mig061/) and are excluded from embedding. Note the leaf-name collision hazard:001_initial_schema.sqlexists in the archived tree only.
Consolidated baseline 001_v1_policy_baseline.sql (folds in the pre-1.0 001_initial_schema.sql):
policy.packs/policy.pack_versions/policy.rules— pack containers, immutable versions, and per-version rules (rule_type ∈ rego|json|yaml, severity, content hashes).pack_versionslater gains activation columns (requires_two_person_approval,activation_approvals_json,activated_at,bundle_digest,bundle_signature) via010.policy.risk_profiles/policy.risk_profile_history— versioned risk profiles (thresholds, scoring weights, exemptions) with append-only history.policy.evaluation_runs— run records (statuspending|running|completed|failed, resultpass|fail|warn|error, score, finding counts,input_hash, timings). This is the canonical “run” table — notpolicy_runs.policy.explanations— per-rule explain rows linked to a run (evaluation_run_id), with result/severity/message/remediation/resource path.policy.cvss_receipts— CVSS v3.1/v4.0 receipts (vector, base/threat/environmental/full scores, policy hash, canonical metrics, evidence/attestation refs, history, amend chain), unique by(tenant_id, input_hash).policy.epss_scores/policy.epss_history/policy.epss_thresholds,policy.risk_scores/policy.risk_score_history— EPSS cache and risk-score state.policy.snapshots,policy.violation_events,policy.conflicts,policy.ledger_exports,policy.worker_results,policy.unknowns,policy.recheck_policies,policy.evidence_hooks.- Exception subsystem:
policy.exceptions,policy.submitted_evidence,policy.exception_events,policy.exception_applications,policy.budget_ledger,policy.budget_entries,policy.exception_approval_requests,policy.exception_approval_audit,policy.exception_approval_rules. policy.audit— module audit rows.
(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):
002—policy.trusted_keys,policy.gate_bypass_audit.003—policy.gate_decisions;004—policy.replay_audit.005—policy.advisory_source_impacts,policy.advisory_source_conflicts;006— audit VEX columns.007—policy.engine_ledger_exports,policy.engine_snapshots(runtime snapshot/ledger persistence);008— snapshot artifact-identity columns;009—policy.orchestrator_jobs.011—policy.violation_fusion_results.012—policy.verification_policies,policy.attestation_reports,policy.risk_scoring_jobs,policy.console_export_jobs,policy.console_export_executions,policy.console_export_bundles,policy.policy_pack_bundle_imports,policy.sealed_mode_states.013— drops deprecated audit tables (the former013_exception_approval.sqlwas superseded; its tables live in the baseline at lines 730/770/789).014—policy.control_register_entries,policy.control_register_events,policy.control_register_snapshots(Assurance / NIS2 register).015—policy.cra_technical_documentation_provenance.
Post-baseline forward-only migrations — these are the live 002–004 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). These add the operator-signed governance-decision support (ADR-025, sprint SPRINT_20260608_015):
002_operator_decision_signing_columns.sql— adds the operator-decision signature columns (operator_decision_envelope/_keyid/_digestTEXT +operator_signature_verifiedBOOLEAN NOT NULL DEFAULT FALSE) topolicy.exception_approval_requestsandpolicy.gate_bypass_audit, mirroring thepolicy.submitted_evidence.dsse_envelope/signature_verifiedprecedent.003_operator_decision_signing_view.sql—policy.trusted_keysoperator decision-signing verification-view delta (subject_id,provider_hint,key_purpose,retired_at,status,replaces_key_id+ partial indexes) so an enrolled operator key resolves on the Policy verify path.004_risk_override_store.sql— createspolicy.risk_overrides(the durable risk-override decision record, replacing the former in-memory store: opaqueoverride_id,profile_id,override_type, predicate/action/tags as JSONB, priority, status, expiration, and flattened audit metadata; backsOverrideServicethroughIRiskOverrideStore, tenant is a string slug) with the same operator-decision signature columns + a partial signed index; RLS tenant isolation mirrorsgate_bypass_auditwithout the append-only immutability trigger (overrides are legitimately approved/disabled/deleted).
All three 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
- Runtime durability (per §2): pack/version/activation state, violation events + fusion + conflicts, verification policies, attestation reports, risk-scoring jobs, console-export jobs/executions/bundles, pack-bundle imports, sealed-mode state, engine snapshots/ledger exports, and orchestrator jobs / worker results all persist and survive host restart without in-memory recovery.
policy.gate_decisionsis the durable release-gate decision history. SynchronousPOST /api/v1/policy/gate/evaluateand scheduler-backed async gate evaluations both record a row after the response is built. Rows carry the canonical tenant UUID, evaluated image digest, BOM/reference string, gate status (Pass|Warn|Fail), verdict hash, optional policy bundle id/hash, CI context, actor, and evaluated timestamp. The table is also the short-term cross-domain read projection consumed by the Integrations registry image browser to show per-image Policy verdict state by digest.evaluation_runs.input_hashand the engine snapshot/ledger digests support replay verification; the signed messaging backend provides bounded reachability-facts overlay caching (Router-selected Valkey in canonical Compose, standalone Postgres otherwise).- Tables are tenant-scoped via
tenant_id(TEXT slug or UUID depending on table) alongsidecreated_at/updated_ataudit columns.
4.3 Indexing
- Representative indexes:
idx_packs_tenant,UNIQUE(tenant_id, name)onpacks;UNIQUE(pack_id, version)onpack_versions;idx_evaluation_runs_tenant/_project/_artifact/_statusonevaluation_runs;idx_explanations_runonexplanations;idx_cvss_receipts_tenant_vulnandUNIQUE(tenant_id, input_hash)oncvss_receipts; GIN index onrules.tags. - EPSS rows carry
expires_at(defaultNOW() + 7 days) for cache freshness rather than database TTL eviction.
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 (src/Policy/StellaOps.Policy.Engine/Services/PostgresFindingsLookup.cs) resolves the deny-able CVE set for a release subject (release:<id>|env:<name>|...) from the operator-curated release_orchestrator.release_components rows plus SBOM-derived components fanned in from scanner.scan_evidence_projections.component_purls. Its advisory authority is selected by PolicyEngine:FindingsLookup:AdvisorySource: GateDecision (the Link-Not-Merge gate projection in vuln.issue_gate_decisions, now authoritative by default) or Legacy (the one-release rollback path reading vuln.advisory_affected / vuln.advisories). PolicyEngine:FindingsLookup:ShadowCompare=true evaluates the non-authoritative source too and emits deterministic legacy-only, gate-decision-only, and metadata-changed classifications without changing the verdict. Shadow failure is loud in logs but cannot replace or weaken the configured authoritative result. The compose operator controls are POLICY_FINDINGS_ADVISORY_SOURCE (default GateDecision) and POLICY_FINDINGS_SHADOW_COMPARE (default true during the compatibility window); they bind through the service-prefixed configuration keys and require a Policy container recreate to change. Roll back by setting the source to Legacy; do not remove that selector until the compatibility release closes.
The gate-decision query uses migration 021’s normalized package_name and ecosystem keys and passes effective_affected_range through the same shared EcosystemVersionFilter used by the legacy path. 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. Thus source selection changes precedence authority, not PURL bucket or version-comparison semantics. The 2026-07-15 CANON-D6 parity, benchmark, and live forcing-function gates closed with zero unexplained recall regression before the default changed.
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 shape | Advisory shape | Pre-036 result | Post-036 result |
|---|---|---|---|
Alpine openssl 3.1.4-r1 (apk) | Debian openssl (deb) | FALSE-POSITIVE match | no match |
Debian libssl1.1 (deb) | Alpine openssl (apk) | FALSE-POSITIVE match | no match |
npm @scope/pkg | npm short pkg | FALSE-POSITIVE match | no match |
Maven org.apache.commons:commons-lang3 | short commons-lang3 (maven) | FALSE-POSITIVE match | no match |
PyPi Django | Debian python3-django | FALSE-POSITIVE match | no match |
OCI base image redis | PyPi redis | FALSE-POSITIVE match | no match |
Contract after Sprint 20260531.036:
- Producer (
PostgresReleaseTruthRepository.UpsertComponentAsync) writes two new nullable columns to everyrelease_componentsrow:package_purl(canonical PURL string) +package_ecosystem(lowercased PURL type —apk,deb,npm,maven,pypi,oci, …). OCI image components default topackage_ecosystem = "oci"and a synthetic PURLpkg:oci/<image-ref>@<digest>. The columns are NULLABLE; legacy rows that pre-date the migration fall back to a name-only match (logged aspolicy.findings_lookup.legacy_name_match). - Release Orchestrator persistence adds the columns + partial indexes (
ix_release_components_purl/ix_release_components_ecosystem_name) in its consolidated baselinesrc/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Persistence/Migrations/001_v1_releaseorchestrator_baseline.sql:905(folded in from the archived014_release_components_purl_ecosystem.sql). Forward-only per ADR-004. Re-runnable. - 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)ANDlower(af.ecosystem) = ANY(@ecosystems).@ecosystemsis the FAN-OUT of every equivalent spelling perEcosystemEquivalence(so an advisory tagged"alpine"still matches a component typedapk, 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).
- Scoped pass — fires only when at least one component carries a canonical ecosystem; SQL has both
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. TheEquivalentSpellings()reverse lookup is what backs the SQLANY(@ecosystems)parameter.EcosystemAwareJoinPlan(src/__Libraries/StellaOps.VulnMatch.Core/EcosystemAwareJoinPlan.cs) is the pure-function planner extracted fromPostgresFindingsLookupso the bucket-key + ecosystem fan-out semantics are unit-testable without a live PostgreSQL fixture. It lives in the sharedStellaOps.VulnMatch.Corelibrary (alongsideEcosystemEquivalence, same directory), not underStellaOps.Policy.Engine/Services;PostgresFindingsLookupmerely consumes it. The runtime delegates bucket construction + advisory-row admission to it.- 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→ nameorg.apache.commons:commons-lang3(groupId+artifactId joined with:). - Golang —
pkg:golang/github.com/foo/bar@v1.2.3→ namegithub.com/foo/bar(full module path). - composer / pub / swift —
pkg:composer/api-platform/core@v3.4.17→ nameapi-platform/core(the vendor namespace is preserved). Collapsing to the barecorecross-matched every vendor’s*/coreadvisory (e.g.drupal/corevsapi-platform/core), the ~460 false-positive class closed by the composer-namespace precision fix. Single-segment composer/pub/swift PURLs keep their bare name.AdvisoryConverterderivesvuln.advisory_affected.package_namefrom the SAME extractor, so the deployed and advisory keys never drift; the live Concelier migrationsrc/Concelier/__Libraries/StellaOps.Concelier.Persistence/Migrations/009_composer_namespaced_package_name_backfill.sqlre-derives existing rows from their retained PURL. - Everything else (apk/deb/rpm/pypi/nuget/oci/generic) — only the trailing path segment.
- npm scoped —
The post-hoc EcosystemVersionFilter C# pass remains in place as a secondary version-range discriminator; it is no longer the only ecosystem-awareness layer.
6 · Run Orchestration & Incremental Flow
- Change streams: Concelier and Excititor publish document changes to the scheduler queue (
policy.trigger.delta). Payload includestenant,source,linkset digests,cursor. - Orchestrator: Maintains per-tenant backlog; merges deltas until time/size thresholds met, then enqueues
PolicyRunRequest. - Queue: PostgreSQL queue with lease; each job assigned
leaseDuration,maxAttempts. - Workers: Lease jobs, execute evaluation pipeline, report status (success/failure/canceled). Failures with recoverable errors requeue with backoff; determinism or schema violations mark job
failedand raise incident event. - Fairness: Round-robin per
{tenant, policyId}; emergency jobs (priority=emergency) jump queue but limited via circuit breaker. - Replay: On demand, orchestrator rehydrates run via stored cursors and exports sealed bundle for audit/CI determinism checks.
- Batch evaluation service (
/api/policy/eval/batch): Stateless evaluator powering Findings Ledger and replay/offline workflows. Requests contain canonical ledger events plus optional current projection; responses return status/severity/labels/rationale without mutating state. Policy Engine enforces per-tenant cost budgets, caches results by(tenantId, policyVersion, eventHash, projectionHash), and falls back to inline evaluation when the remote service is disabled.
6.1 · VEX decision attestation pipeline
- Verdict capture. Each evaluation result contains
{findingId, cve, productKey, reachabilityState, evidenceRefs}plus SBOM and runtime CAS hashes. - OpenVEX serialization.
VexDecisionEmitterbuilds an OpenVEX document with one statement per(cve, productKey)and fills:status,justification,status_notes,impact_statement,action_statement.products(purl) andevidencearray referencingreachability.json,sbom.cdx.json,runtimeFacts.
- DSSE signing. The emitter calls Signer
POST /api/v1/signer/sign/dssewith predicatestella.ops/vexDecision@v1. Signer verifies PoE + scanner integrity and returns a DSSE envelope (decision.dsse.json). - 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. - Export. API/CLI endpoints expose
decision.openvex.json,decision.dsse.json,rekor.txt, and evidence metadata so Export Center + bench automation can mirror them intobench/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
| Status | Exit Code | Description |
|---|---|---|
Pass | 0 | No blocking issues; safe to deploy |
Warn | 1 | Non-blocking issues detected; configurable pass-through |
Fail | 2 | Blocking 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.
| Endpoint | Header | Algorithm |
|---|---|---|
/docker | X-StellaOps-Signature: sha256=<hex> (preferred) or Docker Authorization: Basic <user:hex> | HMAC-SHA256 over raw body |
/harbor | X-Harbor-Signature: sha256=<hex> | HMAC-SHA256 over raw body |
/generic | X-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/ops/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:
- The live
POST /api/v1/policy/gate/evaluateruntime now resolves gate-bypass auditing through the PostgreSQL-backedPostgresGateBypassAuditRepository, scoped by the current tenant context. When a tenant context is genuinely absent, the adapter falls back deterministically to tenantpublicrather than using a process-local in-memory store. StellaOps.Policy.Gatewaynow uses the same PostgreSQL-backed gate-bypass audit path through the unifiedIStellaOpsTenantAccessor, so the standalone gateway no longer keeps a separate in-memory audit repository for compatibility routes.
Runtime Snapshots And Ledger Exports
- The live snapshot surface now persists engine runtime state in PostgreSQL-owned tables
policy.engine_ledger_exportsandpolicy.engine_snapshots, reached throughPostgresLedgerExportStoreandPostgresSnapshotStorerather than the previous process-local in-memory stores. - Sync and async gate evaluation now materialize a tenant-scoped target snapshot in
policy.engine_snapshotsbefore delta computation. The runtime derives that target snapshot from the latest persistedpolicy.engine_ledger_exportsdocument (or the baseline snapshot’s export), stamps the artifact digest/repository/tag onto the snapshot row, and then passes the real snapshot identifier intoDeltaComputer. IOrchestratorJobStoreandIWorkerResultStorenow resolve to persisted adapters overpolicy.orchestrator_jobsandpolicy.worker_results, so Policy export/bootstrap logic survives host recreates instead of depending on process-local completed-job state.- Direct
/policy/orchestrator/jobssubmissions now use a real producer runtime.OrchestratorJobService.SubmitAsyncsignalsPolicyOrchestratorJobWorkerHost, the host leases the next queued job fromIOrchestratorJobStore, marks itrunning, executesPolicyWorkerService, persistspolicy.worker_results, and records terminalcompletedorfailedstate instead of requiring a separate manual/policy/worker/runcall. IReachabilityFactsOverlayCacheresolves toMessagingReachabilityFactsOverlayCachebacked by the host’s single signed messaging backend. Canonical Router-enabled Compose uses persistent Valkey; a Router-disabled standalone host uses transport-owned PostgreSQLmessaging.cache_*tables on the Policy DSN. The live invalidation path uses normalized logical keys instead of the old double-prefixedrf:rf:*shape that broke tenant invalidation after cutover.- The deterministic
/api/policy/eval/batchsurface remains stateless by contract. It returns evaluation results to callers but does not populatepolicy.orchestrator_jobsorpolicy.worker_results. - When a gate request omits an explicit baseline reference and the tenant has no persisted baseline snapshot yet, the engine denies by default with
baselineStatus: "missing:block"andblockedBy: "missing-baseline". Non-prod callers may requestmissingBaselineMode: "warn"for an auditable warning ormissingBaselineMode: "bootstrap"to create the first baseline. Explicit baseline references remain strict: if the caller asks for a missing snapshot ID, the runtime fails instead of inventing one. StellaOps.Policy.Persistencenow applies startup migrations for the Policy schema onpolicy-engineboot, and001_v1_policy_baseline.sqlis idempotent on reused local volumes so snapshot/export runtime convergence does not depend on a fresh database.- The merged gateway compatibility routes now register the unified StellaOps tenant accessor and middleware alongside the Policy-specific tenant context middleware. This keeps copied
RequireTenant()filters from failing pre-handler with500and allows the persisted delta compatibility path to reach the realDeltaComputer. - The live delta compatibility surface now projects persisted engine snapshots through
PersistedKnowledgeSnapshotStoreandDeltaSnapshotServiceAdapter, so tenant-scoped/api/policy/deltas/computerequests fail only on normal contract/data issues rather than process-local tenant or snapshot-store gaps. StellaOps.Policy.Gatewaynow uses the same persisted delta projection path for its standalone compatibility host:ISnapshotStoreresolves toPersistedKnowledgeSnapshotStoreandStellaOps.Policy.Deltas.ISnapshotServiceresolves to the engine-ownedDeltaSnapshotServiceAdapter, replacing the oldInMemorySnapshotStorepath that fabricated mostly-empty compatibility input.
{
"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
# Check gate status (looks up a previous synchronous decision by its decisionId)
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 Workflows, Keyless Signing Guide
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
| Gate | Purpose | Configuration Key |
|---|---|---|
| MinimumConfidenceGate | Reject verdicts below confidence threshold per environment | gates.minimumConfidence |
| UnknownsBudgetGate | Fail scan if unknowns exceed budget | gates.unknownsBudget |
| SourceQuotaGate | Prevent single-source dominance without corroboration | gates.sourceQuota |
| ReachabilityRequirementGate | Require reachability proof for critical CVEs | gates.reachabilityRequirement |
| EvidenceFreshnessGate | Reject stale evidence below freshness threshold | gates.evidenceFreshness |
| CvssThresholdGate | Block findings above CVSS score threshold | gates.cvssThreshold |
| SbomPresenceGate | Require valid SBOM for release artifacts | gates.sbomPresence |
| SignatureRequiredGate | Require signatures on specified evidence types | gates.signatureRequired |
| FacetQuotaGate | Enforce per-facet drift quotas from a pre-computed FacetDriftReport (block/warn/seal-action when report absent) | FacetQuotaGateOptions |
| FixChainGate | Require fix-chain verification for critical vulnerabilities (see Fix Chain Gates) | FixChainGateOptions |
| VexProofGate | Validate VEX proof objects meet policy requirements (confidence tier, signed-statement status, DSSE digest, Rekor index) | VexProofGateOptions |
All eleven gates implement
IPolicyGateand live undersrc/Policy/__Libraries/StellaOps.Policy/Gates/. The five threshold/presence gates andMinimumConfidence/Unknowns/SourceQuota/Reachability/EvidenceFreshnessare covered byetc/policy-gates.yaml.sample;FacetQuotaGate,FixChainGate, andVexProofGatebind 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
- Behavior:
affectedstatus bypasses this gate (conservative default). - Result:
confidence_below_thresholdwhen verdict confidence < environment threshold.
UnknownsBudgetGate
Limits exposure to unknown/unscored dependencies:
gates:
unknownsBudget:
enabled: true
maxUnknownCount: 5
maxCumulativeUncertainty: 2.0
escalateOnExceed: true
- Behavior: Fails when unknowns exceed count limit OR cumulative uncertainty exceeds budget.
- Cumulative uncertainty:
sum(1 - ClaimScore)across all verdicts.
SourceQuotaGate
Prevents single-source verdicts without corroboration:
gates:
sourceQuota:
enabled: true
maxInfluencePercent: 60
corroborationDelta: 0.10
requireCorroboration: true
- Behavior: Fails when single source provides > 60% of verdict weight AND no second source is within delta (0.10).
- Rationale: Ensures critical decisions have multi-source validation.
ReachabilityRequirementGate
Requires reachability proof for high-severity vulnerabilities:
gates:
reachabilityRequirement:
enabled: true
applySeverities:
- critical
- high
exemptStatuses:
- not_affected
bypassReasons:
- component_not_present
- Behavior: Fails when CRITICAL/HIGH CVE marked
not_affectedlacks reachability proof (unless bypass reason applies).
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
- Behavior: Fails when CVSS base score exceeds environment threshold.
- CVSS Versions: Supports both CVSS v3.1 and v4.0; preference configurable.
- Allowlist: CVEs that bypass threshold enforcement.
- Denylist: CVEs that always fail regardless of score.
- Offline: Operates without external lookups; uses injected or metadata scores.
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
- Enforcement Levels:
required(fail),recommended(warn),optional(pass). - Format Validation: Validates SBOM format against accepted list; normalizes format names.
- Schema Validation: Validates SBOM against bundled JSON schemas.
- Signature Requirement: Optionally requires signed SBOM.
- Minimum Components: Ensures SBOM has meaningful inventory.
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
- Per-Evidence-Type: Configure requirements per evidence type (SBOM, VEX, attestation).
- Issuer Constraints: Wildcard support (
*@domain.com) for email patterns. - Algorithm Enforcement: Limit accepted signature algorithms.
- Keyless (Fulcio): Support Sigstore keyless signatures with Fulcio certificate verification.
- Transparency Log: Optionally require Rekor inclusion proof.
- Environment Overrides: Relax requirements for non-production environments.
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:
policy_gate_evaluations_total{tenant,source,status}- Count of gate evaluation jobs by tenant, request source, and final status (Accepted,Rejected,ManualReview, orerror).policy_gate_evaluation_duration_seconds{tenant,source,status}- Gate evaluation job latency histogram with the same tags. Exceptions recordstatus=errorbefore the exception is rethrown so error-rate dashboards have a denominator.
Gate Implementation Reference
| Gate | Source File |
|---|---|
| MinimumConfidenceGate | src/Policy/__Libraries/StellaOps.Policy/Gates/MinimumConfidenceGate.cs |
| UnknownsBudgetGate | src/Policy/__Libraries/StellaOps.Policy/Gates/UnknownsBudgetGate.cs |
| SourceQuotaGate | src/Policy/__Libraries/StellaOps.Policy/Gates/SourceQuotaGate.cs |
| ReachabilityRequirementGate | src/Policy/__Libraries/StellaOps.Policy/Gates/ReachabilityRequirementGate.cs |
| EvidenceFreshnessGate | src/Policy/__Libraries/StellaOps.Policy/Gates/EvidenceFreshnessGate.cs |
| CvssThresholdGate | src/Policy/__Libraries/StellaOps.Policy/Gates/CvssThresholdGate.cs |
| SbomPresenceGate | src/Policy/__Libraries/StellaOps.Policy/Gates/SbomPresenceGate.cs |
| SignatureRequiredGate | src/Policy/__Libraries/StellaOps.Policy/Gates/SignatureRequiredGate.cs |
| FacetQuotaGate | src/Policy/__Libraries/StellaOps.Policy/Gates/FacetQuotaGate.cs |
| FixChainGate | src/Policy/__Libraries/StellaOps.Policy/Gates/FixChainGate.cs |
| VexProofGate | src/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 Level | Description | Required Approvers |
|---|---|---|
| G0 | Development | Auto-approve (no approval needed) |
| G1 | Testing/QA | 1 peer reviewer |
| G2 | Staging | Code owner approval |
| G3 | Pre-production | Delivery Manager + Product Manager |
| G4 | Production | CISO + 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
- Approved exceptions automatically expire after TTL
- Background worker marks expired requests as
Expired - Gate evaluation checks
expires_atbefore honoring exception
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
| Component | Source File |
|---|---|
| Entities | src/Policy/__Libraries/StellaOps.Policy.Persistence/Postgres/Models/ExceptionApprovalEntity.cs |
| Repository | src/Policy/__Libraries/StellaOps.Policy.Persistence/Postgres/Repositories/ExceptionApprovalRepository.cs |
| Rules Service | src/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 Commands | src/Cli/StellaOps.Cli/Commands/ExceptionCommandGroup.cs |
| Migration | policy.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.
GET /api/v1/governance/trust-weightsPUT /api/v1/governance/trust-weights/{weightId}POST /api/v1/governance/trust-weights/preview-impactGET /api/v1/governance/staleness/configPUT /api/v1/governance/staleness/config/{dataType}GET /api/v1/governance/staleness/statusGET /api/v1/governance/conflicts/dashboardGET /api/v1/governance/conflictsPOST /api/v1/governance/conflicts/analyzePOST /api/v1/governance/conflicts/{conflictId}/acknowledgePOST /api/v1/governance/conflicts/{conflictId}/resolvePOST /api/v1/governance/conflicts/{conflictId}/ignore
Contract requirements:
- All requests are tenant-scoped and may include an optional
projectId. - Console clients must resolve live tenant scope from the active session/context and must not rely on legacy placeholder aliases.
- The merged
StellaOps.Policy.Enginehost serves sealed-mode status/toggle from persisted sealed-mode state and servesrisk-profilesas configuration-backed read-only data. - Compatibility routes without a durable backend (
trust-weights,staleness,conflicts, governance audit/history, risk-budget dashboards, sealed-mode overrides, and risk-profile mutation/lifecycle actions) must return truthful501 Not Implementedresponses rather than fabricate tenant-local state. - The standalone
StellaOps.Policy.Gatewayhost does not own governance state and therefore returns truthful501responses across this compatibility surface unless a future sprint wires a real backend.
Implementation reference:
src/Policy/StellaOps.Policy.Engine/Endpoints/Gateway/GovernanceEndpoints.cssrc/Policy/StellaOps.Policy.Engine/Endpoints/Gateway/GovernanceCompatibilityEndpoints.cssrc/Policy/StellaOps.Policy.Gateway/Endpoints/GovernanceCompatibilityEndpoints.cssrc/Policy/StellaOps.Policy.Gateway/Endpoints/GovernanceEndpoints.cs
7 · Security & Tenancy
- Auth: Endpoints are protected by
AddStellaOpsResourceServerAuthentication(JWT bearer resource-server validation) plus the StellaOps scope handler; service-to-service callers use Authority client-credentials (policy-engineclient). The resource server accepts the platform-wideaud=stellaopsalongside any configured audiences, and bypass-network callers are handled byStellaOpsBypassEvaluator. (Note: the engine host does not itself enforce DPoP sender-constrained tokens; DPoP, where required, is enforced upstream at the Authority/gateway layer.) - Scopes: Mutations require the relevant
policy:*scope (policy:write,policy:author,policy:edit,policy:review,policy:submit,policy:approve,policy:operate,policy:publish,policy:promote,policy:audit,policy:run,policy:activate,policy:simulate); read access usespolicy:read. Materialised-finding reads usefindings:read;effective:writeis restricted to the Policy Engine service identity. Exception flows useexceptions:read/exceptions:write/exceptions:request/exceptions:approve; release-gate override usesrelease:bypass. - Tenancy: Tenant identity resolves from the envelope-bound
stellaops:tenantclaim viaIStellaOpsTenantAccessor/PolicyStellaOpsTenantResolver(slug →shared.tenants.idUUID); all queries filter by tenant and requests without a resolved tenant fail closed with HTTP 401. There is nopolicy:tenant-adminscope. - Secrets: Configuration loaded via environment variables or sealed secrets; runtime avoids writing secrets to logs.
- Determinism guard: Static analyzer prevents referencing forbidden namespaces; runtime guard intercepts
DateTime.Now,Random,Guid, HTTP clients beyond allow-list. - Sealed mode: Global flag disables outbound network except allow-listed internal hosts; watchers fail fast if unexpected egress attempted.
Determinism enforcement (DOCS-POLICY-DET-01)
- Inputs are ordered and frozen: Selector emits batches sorted deterministically by
(tenant, policyId, vulnerabilityId, productKey, source)with stable cursors; workers must not resort. - No ambient randomness or wall clocks: Policy code relies on injected
TimeProvider/IRandomshims; guards blockDateTime.Now,Guid.NewGuid,Randomwhen not injected. - Immutable evidence: SBOM/VEX inputs carry content hashes; evaluator treats payloads as read-only and surfaces hashes in logs for replay.
- Side effects prohibited: Evaluator cannot call external HTTP except allow-listed internal services (Authority, Storage) and must not write files outside temp workspace.
- Replay hash: Each batch computes
determinismHash = SHA256(policyVersion + batchCursor + inputsHash); included in logs and run exports. - Testing: Determinism tests run the same batch twice with seeded clock/GUID providers and assert identical outputs + determinismHash; add a test per policy package.
8 · Observability
Metrics are emitted from StellaOps.Policy.Engine/Telemetry/PolicyEngineTelemetry.cs (consult the source for the authoritative tag set). Representative instruments:
- Run / evaluation:
policy_run_seconds(histogram),policy_run_queue_depth,policy_rules_fired_total,policy_vex_overrides_total,policy_evaluations_total,policy_evaluation_latency_seconds,policy_findings_materialized_total,policy_concurrent_evaluations,policy_worker_utilization. - API:
policy_requests_total,policy_api_latency_seconds,policy_api_errors_total,policy_errors_total,policy_evaluation_failures_total,policy_rate_limit_exceeded_total. - SLO:
policy_slo_burn_rate,policy_error_budget_remaining,policy_slo_violations_total. - Entropy signals:
policy_entropy_penalty_total,policy_entropy_penalty_value,policy_entropy_image_opaque_ratio,policy_entropy_top_file_ratio. - Risk scoring:
policy_risk_scoring_jobs_created_total,policy_risk_scoring_triggers_skipped_total. - Determinization (
StellaOps.Policy.Determinization):stellaops_determinization_uncertainty_entropy,stellaops_determinization_decay_multiplier. Proof Studio (StellaOps.Policy.Explainability):stellaops.proofstudio.views_composed_total,stellaops.proofstudio.counterfactuals_applied_total. - Logs: Structured JSON with
traceId,policyId,version,runId,tenant,phase. Guard ensures no sensitive data leakage. - Traces: Spans
policy.select,policy.evaluate,policy.materialize,policy.simulate. Trace IDs surfaced to CLI/UI. - Incident mode toggles 100 % sampling and extended retention windows.
9 · Offline / Bundle Integration
- Imports: Offline Kit delivers policy packs, advisory/VEX snapshots, SBOM updates. Policy Engine ingests bundles via
offline import. - Exports:
stella policy bundle exportpackages policy, IR digest, simulations, run metadata; UI provides export triggers. - Sealed hints: Explain traces annotate when cached values used (EPSS, KEV). Run records mark
env.sealed=true. - Sync cadence: Operators perform monthly bundle sync; Policy Engine warns when snapshots > configured staleness (default 14 days).
10 · Testing & Quality
- Unit tests: DSL parsing, evaluator semantics, guard enforcement.
- Integration tests: Joiners with sample SBOM/advisory/VEX data; materialisation with deterministic ordering; API contract tests generated from OpenAPI.
- Property tests: Ensure rule evaluation deterministic across permutations.
- Golden tests: Replay recorded runs, compare determinism hash.
- Snapshot contract (Policy Engine tests): Snapshot assertions resolve to source-controlled
src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Snapshots/via caller-file path. Regenerate withUPDATE_SNAPSHOTS=1only when intentional fixture changes are reviewed. - API auth fixture contract (PolicyEngineApiHostTests): Test auth overrides run in fixture scope only, with deterministic in-memory resource-server settings (
Authority,RequireHttpsMetadata=false) and canonical tenant claimstellaops:tenantso tenancy middleware and scope policies both evaluate in tests. - Consumer API contract tests:
StellaOps.Policy.Engine.Contract.Testsuses managed in-memory HTTP handlers and source-defined JSON assertions instead of PactNet/libpact_ffi so the contract suite remains runnable on glibc Linux ARM64 runners. - Registry boundary tests: Focused Policy Engine tests should target
src/Policy/__Tests/StellaOps.Policy.Engine.Tests/StellaOps.Policy.Engine.Tests.csprojfor policy pack bundle import, trust verification, and confirmation that pack import cannot execute pack-provided code. Executable evaluator adapters are not a default Policy path; if a future sprint adds one, signed-bundle admission tests belong in that module-local sprint and must use the shared plugin admission chain. - Performance tests: Evaluate 100k component / 1M advisory dataset under warmed caches (<30 s full run).
- Chaos hooks: Optional toggles to simulate upstream latency/failures; used in staging.
11 · Compliance Checklist
- [ ] Determinism guard enforced: Static analyzer + runtime guard block wall-clock, RNG, unauthorized network calls.
- [ ] Incremental correctness: Change-stream cursors stored and replayed during tests; unit/integration coverage for dedupe.
- [ ] RBAC validated: Endpoint scope requirements match Authority configuration; integration tests cover deny/allow.
- [ ] AOC separation enforced: No code path writes to
advisory_raw/vex_raw; integration tests captureERR_AOC_00xhandling; read-only clients verified. - [ ] Effective findings ownership: Only Policy Engine identity holds
effective:write; unauthorized callers receiveERR_AOC_006. - [ ] Observability wired: Metrics/traces/logs exported with correlation IDs; dashboards include
aoc_violation_totaland ingest latency panels. - [ ] Offline parity: Sealed-mode tests executed; bundle import/export flows documented and validated.
- [ ] Schema docs synced: DTOs match Scheduler Models (
SCHED-MODELS-20-001); JSON schemas committed. - [ ] Security reviews complete: Threat model (including queue poisoning, determinism bypass, data exfiltration) documented; mitigations in place.
- [ ] Disaster recovery rehearsed: Run replay+rollback procedures tested and recorded.
12 · Related Product Advisories
The following product advisories provide strategic context for Policy Engine features:
Consolidated: Diff-Aware Release Gates and Risk Budgets — Master reference for risk budgets, delta verdicts, VEX trust scoring, and release gate policies. Key sections:
- §2 Risk Budget Model: Service tier definitions and RP scoring formulas
- §4 Delta Verdict Engine: Deterministic evaluation pipeline and replay contract
- §5 Smart-Diff Algorithm: Material risk change detection rules
- §7 VEX Trust Scoring: Confidence/freshness lattice for VEX source weighting
Consolidated: Deterministic Evidence and Verdict Architecture — Master reference for determinism guarantees, canonical serialization, and signing. Key sections:
- §3 Canonical Serialization: RFC 8785 JCS + Unicode NFC rules
- §5 Signing & Attestation: Keyless signing with Sigstore
- §6 Proof-Carrying Reachability: Minimal proof chains
- §8 Engine Architecture: Deterministic evaluation pipeline
Determinism Specification — Technical specification for all digest algorithms (VerdictId, EvidenceId, GraphRevisionId, ManifestId) and canonicalization rules.
Smart-Diff Technical Reference — Detailed algorithm specifications for reachability gates, delta computation, and call-stack analysis.
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
| Format | Schema | Direction | Notes |
|---|---|---|---|
| PolicyPack v2 (JSON) | policy.stellaops.io/v2 | Import + Export | Canonical format with typed gates, environment overrides, remediation hints |
| PolicyPack v2 (YAML) | policy.stellaops.io/v2 | Import + Export | Deterministic YAML with sorted keys; YAML→JSON roundtrip for validation |
| OPA/Rego | package stella.release | Export (+ 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 Type | Rego Pattern | Remediation Code |
|---|---|---|
CvssThresholdGate | input.cvss.score >= threshold | CVSS_EXCEED |
SignatureRequiredGate | not input.dsse.verified | SIG_MISS |
EvidenceFreshnessGate | not input.freshness.tstVerified | FRESH_EXPIRED |
SbomPresenceGate | not input.sbom.canonicalDigest | SBOM_MISS |
MinimumConfidenceGate | input.confidence < threshold | CONF_LOW |
UnknownsBudgetGate | input.unknownsRatio > threshold | UNK_EXCEED |
ReachabilityRequirementGate | not input.reachability.status | REACH_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:
- Same policy + same input = same output (hash-verifiable)
- Exports include SHA-256
digestfield - No time-dependent logic in deterministic mode
outputDigestin evaluation results enables replay verification
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):
- Converts
PolicyPackDocumentto aSortedDictionaryintermediate for deterministic key ordering - Serializes via YamlDotNet (CamelCaseNamingConvention, DisableAliases, OmitNull)
- Produces SHA-256 digest for replay verification
- Supports environment filtering and remediation stripping (same options as JSON)
Import (YamlPolicyImporter):
- Deserializes YAML via YamlDotNet, then re-serializes as JSON
- Delegates to
JsonPolicyImporterfor validation (apiVersion, kind, duplicate gates/rules) - Errors:
YAML_PARSE_ERROR,YAML_EMPTY,YAML_CONVERSION_ERROR
Format Detection (FormatDetector):
- Content-based: detects
apiVersion:,---,kind:patterns - Extension-based:
.yaml,.yml→PolicyFormats.Yaml
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:
- Changes to metadata (name, version, description)
- Changes to settings (defaultAction, unknownsThreshold, stopOnFirstFailure, deterministicMode)
- Gate changes (by ID): added, removed, modified (action, type, config diffs)
- Rule changes (by Name): added, removed, modified (action, match diffs)
- Summary with counts of added/removed/modified and
HasChangesflag
Merge applies one of three strategies via PolicyMergeStrategy:
| Strategy | Behavior |
|---|---|
OverlayWins | Overlay values take precedence on conflict |
BaseWins | Base values take precedence on conflict |
FailOnConflict | Returns 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
| Component | Source File |
|---|---|
| Contracts | src/Policy/__Libraries/StellaOps.Policy.Interop/Contracts/PolicyPackDocument.cs |
| Remediation Models | src/Policy/__Libraries/StellaOps.Policy.Interop/Contracts/RemediationModels.cs |
| Interfaces | src/Policy/__Libraries/StellaOps.Policy.Interop/Abstractions/ |
| JSON Exporter | src/Policy/__Libraries/StellaOps.Policy.Interop/Export/JsonPolicyExporter.cs |
| YAML Exporter | src/Policy/__Libraries/StellaOps.Policy.Interop/Export/YamlPolicyExporter.cs |
| JSON Importer | src/Policy/__Libraries/StellaOps.Policy.Interop/Import/JsonPolicyImporter.cs |
| YAML Importer | src/Policy/__Libraries/StellaOps.Policy.Interop/Import/YamlPolicyImporter.cs |
| Rego Generator | src/Policy/__Libraries/StellaOps.Policy.Interop/Rego/RegoCodeGenerator.cs |
| Rego Importer | src/Policy/__Libraries/StellaOps.Policy.Interop/Import/RegoPolicyImporter.cs |
| Diff/Merge Engine | src/Policy/__Libraries/StellaOps.Policy.Interop/DiffMerge/PolicyDiffMergeEngine.cs |
| Embedded OPA | src/Policy/__Libraries/StellaOps.Policy.Interop/Evaluation/EmbeddedOpaEvaluator.cs |
| Remediation Resolver | src/Policy/__Libraries/StellaOps.Policy.Interop/Evaluation/RemediationResolver.cs |
| Format Detector | src/Policy/__Libraries/StellaOps.Policy.Interop/Import/FormatDetector.cs |
| Schema Validator | src/Policy/__Libraries/StellaOps.Policy.Interop/Validation/PolicySchemaValidator.cs |
| DI Registration | src/Policy/__Libraries/StellaOps.Policy.Interop/DependencyInjection/PolicyInteropServiceCollectionExtensions.cs |
| CLI Commands | src/Cli/StellaOps.Cli/Commands/Policy/PolicyInteropCommandGroup.cs |
| Policy Engine API | src/Policy/StellaOps.Policy.Engine/Interop/Endpoints/PolicyInteropEndpoints.cs |
| JSON Schema | docs/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
| Method | Path | Auth Policy | Description |
|---|---|---|---|
| POST | /export | policy:read | Export policy to format |
| POST | /import | policy:write | Import policy from format |
| POST | /validate | policy:read | Validate policy document |
| POST | /evaluate | policy:read | Evaluate policy against input |
| GET | /formats | policy:read | List 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 Field | Type | Description |
|---|---|---|
artifact.digest | string | Artifact digest (e.g., sha256:abc...) |
artifact.mediaType | string | OCI media type |
artifact.reference | string | Full artifact reference |
sbom.digest | string | SBOM content hash |
sbom.format | string | Format identifier (e.g., cyclonedx-1.7, spdx-3.0.1) |
sbom.componentCount | int | Number of components |
sbom.content | object | Optional inline SBOM JSON |
attestations[] | array | Attestation references |
attestations[].digest | string | DSSE envelope digest |
attestations[].predicateType | string | in-toto predicate type URI |
attestations[].signatureVerified | bool | Signature verification status |
attestations[].rekorLogIndex | long | Transparency log index |
transparency.rekor[] | array | Rekor receipts |
transparency.rekor[].logId | string | Log identifier |
transparency.rekor[].uuid | string | Entry UUID |
transparency.rekor[].logIndex | long | Log position |
transparency.rekor[].integratedTime | long | Unix timestamp |
transparency.rekor[].verified | bool | Receipt verification status |
vex.mergeDecision | object | VEX merge decision |
vex.mergeDecision.algorithm | string | Merge algorithm (e.g., trust-weighted-lattice-v1) |
vex.mergeDecision.inputs[] | array | Source documents with trust weights |
vex.mergeDecision.decisions[] | array | Per-vulnerability decisions with provenance |
Code locations:
- Evidence models:
src/Policy/__Libraries/StellaOps.Policy/Gates/Opa/OpaEvidenceModels.cs - Context extension:
src/Policy/__Libraries/StellaOps.Policy/Gates/PolicyGateAbstractions.cs - Input builder:
src/Policy/__Libraries/StellaOps.Policy/Gates/Opa/OpaGateAdapter.cs
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 Type | Depth | Purpose |
|---|---|---|
Verdict | 0 | Root: the final verdict + composite score |
PolicyRule | 1 | Policy clause that triggered the decision |
Guardrail | 1 | Score guardrail (cap/floor) that modified the score |
ScoreComputation | 2 | Per-factor score contribution |
ReachabilityAnalysis | 3 | Reachability evidence leaf |
VexStatement | 3 | VEX attestation leaf |
Provenance | 3 | Provenance attestation leaf |
SbomEvidence | 3 | SBOM evidence leaf |
RuntimeSignal | 3 | Runtime detection signal leaf |
AdvisoryData | 3 | Advisory data leaf |
Counterfactual | 0 | What-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:
- Creates a
Counterfactualnode at depth 0 with the scenario label. - Connects overridden factor score nodes to the counterfactual node via
Overridesedges. - Recomputes the content-addressed graph ID, making each scenario distinctly identifiable.
14.5 · Proof Studio Service (Integration)
The IProofStudioService is the primary integration surface:
| Method | Input | Output |
|---|---|---|
Compose(request) | ProofStudioRequest (rationale + optional score factors + guardrails) | ProofStudioView (proof graph + optional score breakdown) |
ApplyCounterfactual(view, scenario) | Existing view + CounterfactualScenario | Updated 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
| Metric | Type | Description |
|---|---|---|
stellaops.proofstudio.views_composed_total | Counter | Proof studio views composed |
stellaops.proofstudio.counterfactuals_applied_total | Counter | Counterfactual scenarios applied |
14.8 · Tests
ProofGraphBuilderTests.cs— 18 tests (graph construction, determinism, depth hierarchy, critical paths, counterfactual overlay, edge cases)ProofStudioServiceTests.cs— 10 tests (compose, score breakdown, guardrails, counterfactual, DI resolution)
15 · Advisory Gap Status (2026-03-04 Batch)
Status: implementation delivered in Sprint 306.
ScorePolicyruntime contract now includes requiredPolicyId;ScorePolicy.Defaultemits deterministic IDscore-policy.default.v1.- Loader and validator behavior is aligned:
ScorePolicyLoaderenforcespolicyVersion, requiredpolicyId, schema validation, and deterministic load failures.- Missing
policyIdnow fails predictably with explicit error text.
- Schema ownership is canonicalized:
- runtime validator loads one canonical schema resource (
Schemas/score-policy.v1.schema.json) embedded inStellaOps.Policy. - source schema and embedded resource parity are guarded by tests.
- runtime validator loads one canonical schema resource (
- Section naming drift was removed; schema keys align with runtime serialization (
reachability,evidence,provenance,scoringProfile). - Existing policy tests and fixtures that build
ScorePolicywere updated to include deterministicpolicyId.
Legacy fixture note:
- Older YAML fixtures without
policyIdare no longer valid and must be migrated by adding deterministicpolicyIdvalues.
Closure sprint:
docs/implplan/SPRINT_20260304_306_Policy_score_policy_contract_consistency.md
