StellaOps Export Center
Export Center is the StellaOps service that packages reproducible evidence bundles — canonical JSON, Trivy DB, and OCI-style mirrors — with provenance metadata and optional signing, for offline, air-gapped, or mirrored deployments.
Audience: operators producing and verifying exports for Offline Kit and mirror parity, and automation authors driving export jobs through the API or CLI.
Recent changes
- 2026-06-12 - Risk-bundle worker materialization. API-submitted risk-bundle jobs now remain
PendinguntilStellaOps.ExportCenter.Workerleases them fromPostgresRiskBundleJobStoreand materializes the bundle through the realRiskBundleJobpath. The WebService handler no longer synthesizes bundle IDs, root hashes, or provider hashes. - 2026-06-12 - NIS2 WebService source-coupling closure. The
StellaOps.ExportCenter.WebServiceruntime no longer compiles theStellaOps.ExportCenter.Adapters.Nis2compatibility assembly. The typed NIS2 SoA/effectiveness implementation now lives inStellaOps.ExportCenter.Corebecause those routes are built-in ExportCenter capabilities; the old adapter project remains a type-forwarding shell for binary compatibility. - 2026-06-05 - CRA runtime source-coupling closure. The
StellaOps.ExportCenter.WebServiceruntime no longer compiles theStellaOps.ExportCenter.Adapters.Craimplementation project for Assurance profile descriptors. CRA adapter IDs, schema IDs, and media types used by profile discovery are host-local metadata until signed mounted CRA adapter bundles and loader admission exist. - 2026-06-05 - Pluginized config root alignment. Compose overlays now have a canonical ExportCenter config root at
devops/etc/plugins/exportcenter/. The legacydevops/etc/plugins/export/registry.yamlremains for unknown consumers; new runtime composition docs and registries useexportcenter. - 2026-04-24 — Fail-closed DI defaults. Default Export Center DI helpers now fail closed instead of silently registering in-memory repositories/stores. In-memory and local crypto helpers are exposed only through explicit
*InMemoryForTestingor*TestHarness*methods. - 2026-04-16 — Truthful-runtime cutover. Non-testing
StellaOps.ExportCenter.WebServicenow uses PostgreSQL-backed canonical export repositories plus a real Evidence Locker client. Simulation-export and audit-bundle host paths still return explicit501 problem+jsonuntil durable backends exist (see Remaining truthful unsupported runtime surfaces). Verification readback, exception reports, incident management, timeline publication, and risk-bundle job state/materialization now use durable PostgreSQL-backed state outsideTesting.
Responsibilities
- Coordinate export jobs based on profiles and scope selectors.
- Assemble manifests, provenance documents, and cosign signatures.
- Stream bundles via HTTP/OCI and stage them for Offline Kit uses.
- Expose CLI/API surfaces for automation.
Key components
StellaOps.ExportCenter.WebService— HTTP API surface (profiles, runs, artifacts, SSE, verification, attestation, incidents, risk bundles, simulations, audit bundles, exception reports, lineage, assurance/NIS2-SoA). Hosts the planner and the truthful-runtime DI wiring (Program.cs,AddExportCenterTruthfulRuntime).StellaOps.ExportCenter.Worker— background bundle builder, plus the risk-bundle worker (RiskBundleWorker) and DevPortal-offline worker.StellaOps.ExportCenter.Core— domain model (ExportProfile,ExportRun,ExportInput,ExportDistribution), the export adapters, planner/scope resolver, manifest writer, mirror/offline-kit packagers, encryption, and verification.StellaOps.ExportCenter.Infrastructure— PostgreSQL data source, repositories, EF Core context, and SQL migrations (Db/Migrations/001_initial_schema.sql).- Compliance/assurance libraries: CRA and StandardsMapping stay separate adapter libraries. NIS2 SoA/effectiveness code is host-owned in Core for the typed NIS2 routes;
StellaOps.ExportCenter.Adapters.Nis2is a type-forwarding compatibility assembly. Additional support libraries include*.AttestationBundles,*.RiskBundles, and*.DevPortalOffline.
Profiles at a glance
Profile kinds are stored on ExportProfile.Kind (ExportProfileKind enum: AdHoc=1, Scheduled=2, EventDriven=3, Continuous=4). The output adapter chosen for a profile is identified by an adapter id (IExportAdapter.AdapterId / .Id):
- json:raw / json:policy — Evidence bundles with raw ingestion facts (
JsonRawAdapter) or policy overlays (JsonPolicyAdapter). - trivy:db / trivy:java-db — Trivy-compatible vulnerability feeds with deterministic manifests (
TrivyDbAdapter,TrivyJavaDbAdapter). - mirror (full / delta) — OCI-style mirrors with provenance and optional encryption. The full-mirror adapter id is
mirror:standard(MirrorAdapter) and the delta adapter id ismirror:delta(MirrorDeltaAdapter); both emit manifests whoseprofilefield ismirror:fullormirror:delta(MirrorBundleBuilder). The doc previously implied an adapter literally namedmirror:full; the manifest profile string ismirror:full, but the adapter id ismirror:standard. - devportal:offline — Developer portal static assets, specs, SDKs, and changelogs packaged with
manifest.json,checksums.txt, helper scripts, and a DSSE-signed manifest (manifest.dsse.json) for offline verification. - Assurance / compliance exports — separate framework profiles served by the assurance + NIS2-SoA endpoints (
AssuranceExportProfileRegistry):nis2.statement-of-applicability,nis2.effectiveness-report,dora.roi,cra.technical-file,cra.conformity-dossier.
API surface
All routes require an OAuth scope via RequireAuthorization. The three export scopes are defined in StellaOpsScopes (export.viewer, export.operator, export.admin) and surfaced as policies StellaOpsResourceServerPolicies.ExportViewer/ExportOperator/ExportAdmin. The global fallback policy in Program.cs requires export.viewer on every authenticated request; /healthz, /readyz, OpenAPI, build-info, and discovery endpoints are AllowAnonymous.
Core export API (ExportApiEndpoints, base group /v1/exports):
| Method & route | Scope | Purpose |
|---|---|---|
GET /v1/exports/profiles | export.viewer | List profiles (filters: status, kind, search, offset, limit; limit clamped 1–100, default 50) |
GET /v1/exports/profiles/{profileId:guid} | export.viewer | Get a profile |
POST /v1/exports/profiles | export.operator | Create a profile (audited) |
PUT /v1/exports/profiles/{profileId:guid} | export.operator | Update a profile (audited) |
DELETE /v1/exports/profiles/{profileId:guid} | export.admin | Archive (soft-delete) a profile (audited) |
POST /v1/exports/profiles/{profileId:guid}/runs | export.operator | Start a run (profile must be Active; returns 202 Accepted, 429 on concurrency limits) |
GET /v1/exports/runs | export.viewer | List runs (filters: profileId, status, trigger, createdAfter, createdBefore, correlationId, offset, limit) |
GET /v1/exports/runs/{runId:guid} | export.viewer | Get a run with its artifacts |
POST /v1/exports/runs/{runId:guid}/cancel | export.operator | Cancel a queued/running run (audited) |
GET /v1/exports/runs/{runId:guid}/artifacts | export.viewer | List artifacts for a run |
GET /v1/exports/runs/{runId:guid}/artifacts/{artifactId:guid} | export.viewer | Get artifact metadata |
GET /v1/exports/runs/{runId:guid}/artifacts/{artifactId:guid}/download | export.viewer | Stream the artifact file (audited) |
GET /v1/exports/runs/{runId:guid}/events | export.viewer | SSE stream of run events (text/event-stream) |
POST /v1/exports/runs/{runId:guid}/verify | export.viewer | Verifies durable run metadata, manifest/signature, and artifact payloads from the Postgres verification artifact store |
GET /v1/exports/runs/{runId:guid}/verify/manifest | export.viewer | Reads the persisted export manifest from the Postgres verification artifact store; returns 404 when no manifest is recorded |
GET /v1/exports/runs/{runId:guid}/verify/attestation | export.viewer | Attestation status (HasAttestation, AttestationType = DSSE, ManifestDigest) |
POST /v1/exports/runs/{runId:guid}/verify/stream | export.viewer | Streams verification progress over persisted verification artifacts |
Attestation (AttestationEndpoints, group /v1/exports, scope export.viewer): GET /{id}/attestation, GET /attestations/{attestationId}, POST /{id}/attestation/verify.
Promotion attestation (PromotionAttestationEndpoints, group /v1/promotions): POST /attestations (export.operator), GET /attestations/{assemblyId}, GET /{promotionId}/attestations, POST /attestations/{assemblyId}/verify, GET /attestations/{assemblyId}/bundle.
Incidents (IncidentEndpoints, group /v1/incidents): GET /status, GET "", GET /recent, GET /{id} (export.viewer); POST "" activate, PATCH /{id} update, POST /{id}/resolve (export.operator). Non-testing runtime uses PostgresExportIncidentManager with durable incident/update rows and timeline publication.
Risk bundles (RiskBundleEndpoints, group /v1/risk-bundles): GET /providers, GET /jobs, GET /jobs/{jobId} (export.viewer); POST /jobs submit, POST /jobs/{jobId}/cancel (export.operator). Current non-testing runtime uses PostgreSQL-backed job state and lifecycle events (risk_bundle_jobs, risk_bundle_job_events) instead of the previous 501 default. API-submitted jobs create durable Pending records; StellaOps.ExportCenter.Worker leases pending or expired Running jobs and materializes bundles through RiskBundleJob.
Simulation exports (SimulationExportEndpoints, group /v1/exports/simulations): GET "", GET /{exportId}, GET /{simulationId}/stream, GET /{simulationId}/csv (export.viewer); POST "" (export.operator). Current non-testing runtime returns 501 with error_code=exportcenter.simulation_exports.not_implemented.
Audit bundles (AuditBundleEndpoints, group /v1/audit-bundles): POST "" (export.operator); GET "", GET /{bundleId}, GET /{bundleId}/download, GET /{bundleId}/index (export.viewer).
Exception reports (ExceptionReportEndpoints, group /v1/exports/exceptions): POST / (export.operator); GET /, GET /{jobId}, GET /{jobId}/download (export.viewer). Non-testing runtime uses durable Postgres job state and report-content persistence; testing-only in-memory generation remains behind Export:UseInMemoryExceptionReportGenerator.
Lineage evidence packs (LineageExportEndpoints, group /api/v1/lineage): POST /export and POST /export/{packId}/sign (export.operator); GET /export/{packId}, GET /export/{packId}/download, POST /export/{packId}/verify (export.viewer).
Assurance (AssuranceExportEndpoints, group /v1/exports/assurance, export.viewer): GET /profiles, GET /profiles/{profileId}, GET /profiles/{profileId}/readiness. NIS2 SoA (Nis2SoaExportEndpoints, group /v1/exports/nis2/soa): GET /profile (export.viewer), POST /runs (export.operator), GET /runs/{runId:guid}, GET /runs/{runId:guid}/bundle (export.viewer).
Legacy GET/POST /exports and DELETE /exports/{id} are deprecated and now return 410 Gone (LegacyExportEndpointGone); use the /v1/exports/* routes instead.
Persistence
PostgreSQL schema export_center (+ export_center_app for helper functions), auto-migrated on startup when ExportCenter:Database:ApplyMigrationsAtStartup is true (default). Migration 001_initial_schema.sql creates four row-level-security-isolated tables (tenant_id = export_center_app.require_current_tenant()):
export_profiles—profile_id,tenant_id,name,description,kind(1–4),status(1–4),scope_json,format_json,signing_json,schedule, timestamps,archived_at. Unique on(tenant_id, name)where not archived.export_runs—run_id,profile_id(FK),tenant_id,status(1–6),trigger(1–4),correlation_id,initiated_by, item/byte counters,error_json, timestamps,expires_at.export_inputs— items included in a run (kind1–8,status1–5,source_ref,content_hashsha256,size_bytes, …).export_distributions— artifact distribution to targets (kind1–5,status1–6,target,artifact_path,artifact_hash,attempt_count, …).
Migration 006_verification_artifacts_exception_reports.sql adds durable backend state for the first ExportCenter unsupported-subfeature slice:
export_verification_runsandexport_verification_artifactspersist run metadata, manifest/signature bytes, artifact payloads, expected hashes, content types, and encryption flags for verification readback.exception_report_jobspersists exception report request filters, job status/progress, generated report bytes, content hash, content type, file name, and error state for list/status/download readback. All three tables enable row-level security: verification runs and report jobs isolate directly ontenant_id; verification artifacts isolate through their parent run.
Migration 007_timeline_notifications.sql adds durable state for timeline publication:
export_timeline_notificationspersists the notification channel, event id, dedupe key, tenant reference, event type, serialized payload, delivery state, attempt count, last error, and publish timestamps. Non-testing runtime registersPostgresExportNotificationSinkas the production timeline sink; repeated event ids update the same row and incrementattempt_count.- The table enables row-level security through
tenant_id = export_center_app.require_current_tenant(). Non-UUID system events use the all-zero system tenant id for RLS while keeping their originaltenant_reffor replay/audit metadata.
Migration 008_export_incidents.sql adds durable state for incident management:
export_incidentspersists incident type, severity, status, summary/description, affected tenants/profiles, activation/resolution actors, correlation id, metadata, and lifecycle timestamps.export_incident_updatespersists the ordered incident update trail, including status/severity transitions, operator, message, and timestamp. Both tables enable row-level security and keep incident lifecycle state out of process memory in non-testing runtime.
Domain enums (Core Domain): ExportRunStatus (Queued=1, Running=2, Completed=3, PartiallyCompleted=4, Failed=5, Cancelled=6); ExportRunTrigger (Manual=1, Scheduled=2, Event=3, Api=4). API-started runs use Trigger = Api and expire 7 days after creation.
Configuration
Root section ExportCenter (ExportCenterOptions): Database.ConnectionString (required; or ConnectionStrings:ExportCenter/ConnectionStrings:Default), Database.ApplyMigrationsAtStartup (default true), ObjectStore (Kind = FileSystem/AmazonS3, RootPath, BucketName, Region), Timeline (Enabled, Endpoint, RequestTimeoutSeconds, Source default stellaops.export-center), Signing (Enabled default true, Algorithm default ES256, KeyId, Provider), and Quotas (MaxConcurrentExports default 10, MaxExportSizeBytes default 1 GiB, DefaultRetentionDays default 90).
Pluginized Runtime Composition
ExportCenter uses the shared plugin probe surface for adapter/data-pack classification:
GET /internal/plugins/statusPOST /internal/plugins/probe
Compose plugin overlays mount the canonical config, trust, and scratch roots:
| Purpose | Host path | Container path |
|---|---|---|
| Operator config/data-pack registry | devops/etc/plugins/exportcenter/ | /app/etc/plugins/exportcenter |
| Adapter trust roots | devops/etc/certificates/trust-roots/plugins/exportcenter/ | /app/trust-roots/plugins/exportcenter |
| Probe scratch | named volume | /var/lib/stellaops/plugin-scratch/exportcenter |
devops/etc/plugins/exportcenter/registry.yaml is the canonical compose-era registry. devops/etc/plugins/export/registry.yaml is retained as a legacy alias and must not be deleted until all consumers are audited. Host adapters (json:raw, json:policy, mirror:standard, runtime:combined) remain registered by the service. Declarative packs such as assurance.profile-pack and standards-mapping-v1 remain data/config and do not execute code. The first signed mounted ExportCenter adapter profile is the recommended Trivy DB / Trivy Java DB bundle set under /app/plugins/exportcenter/recommended; CRA/PDF/custom regulatory adapters remain future optional producer work and must ship signed bundle artifacts, runtime loader admission, image audit proof, and live probe evidence before they can be treated as loaded plugins.
Current source-coupling state: the WebService project no longer references the CRA or NIS2 adapter implementation assemblies. NIS2 is not an optional mounted adapter in this slice; it is a built-in typed ExportCenter route implemented in Core, with a compatibility type-forwarder assembly for existing consumers.
Telemetry
Meter and activity source StellaOps.ExportCenter. Representative metrics (ExportTelemetry): counters export_runs_total, export_runs_success_total, export_runs_failed_total, export_artifacts_total, export_bytes_total, export_artifact_downloads_total, export_sse_connections_total, export_concurrency_limit_exceeded_total, export_timeline_events_published_total/_failed_total/_deduplicated_total, export_incidents_activated_total/_resolved_total/_escalated_total/_deescalated_total, export_risk_bundle_jobs_submitted_total/_completed_total, export_simulation_exports_total, export_audit_events_total, export_registry_capabilities_probed_total, export_referrer_discovery_method_total, export_referrers_discovered_total, export_referrer_discovery_failures_total; histograms export_run_duration_seconds, export_plan_duration_seconds, export_bundle_size_bytes, export_incident_duration_seconds, export_risk_bundle_job_duration_seconds; up/down counter export_runs_in_progress.
Integrations & dependencies
- Concelier/Excititor/Policy data stores for evidence.
- Signer/Attestor for provenance signing.
- CLI for operator-managed exports.
Operational notes
- Runbooks in ./operations/ for deployment and monitoring.
- Observability assets:
operations/observability.mdandoperations/dashboards/export-center-observability.json(offline import). - Mirror bundle instructions and validation notes.
- Telemetry dashboards for export latency and retry rates.
- Testing-only in-memory runtime switches are explicit (
Export:AllowInMemoryRepositories,Export:UseInMemoryEvidenceLocker,Export:UseInMemoryVerificationArtifactStore,Export:UseInMemoryAttestationStore,Export:UseInMemoryPromotionAttestationStore,Export:UseInMemoryIncidentManager,Export:UseInMemoryRiskBundleJobHandler,Export:UseInMemorySimulationExporter,Export:UseInMemoryAuditBundleJobHandler,Export:UseInMemoryExceptionReportGenerator,Export:UseInMemoryTimelineNotificationSink). Non-testing runtime must use durable services or truthful501gaps. - Local/test harness registration methods are intentionally named:
AddExportVerificationInMemoryForTesting,AddExportTimelinePublisherInMemoryForTesting,AddExportDistributionInMemoryForTesting,AddExportSchedulingInMemoryForTesting,AddPackRunIntegrationInMemoryForTesting,AddTenantScopeEnforcementInMemoryForTesting,AddTestHarnessAgeKeyWrapper, andAddTestHarnessKmsClient.
Remaining truthful unsupported runtime surfaces
UnsupportedExportAttestationServiceandUnsupportedPromotionAttestationAssemblerremain truthful helper defaults for attestation readback paths that are not supplied durable collaborators.UnsupportedSimulationReportExporterandUnsupportedAuditBundleJobHandlerare the current shipped truth for the remaining admin/job surfaces: outsideTestingthey return501 problem+jsoninstead of keeping process-local state. The simulation-export durable-backend gap uses the stableerror_codedocumented in api.md.UnsupportedExportIncidentManager,UnsupportedExportNotificationSink,UnsupportedVerificationArtifactStore, andUnsupportedExportDistributionRepositoryare the default helper-level shipped truth when no durable implementation is supplied. Non-testingStellaOps.ExportCenter.WebServicenow registersPostgresExportIncidentManagerfor incident management,PostgresExportNotificationSinkfor timeline publication,PostgresExportVerificationArtifactStorefor verification readback,RiskBundleJobHandlerwithPostgresRiskBundleJobStorefor risk-bundle job state, andExceptionReportGeneratorwithPostgresExceptionReportJobStorefor exception reports.DisabledReferrerDiscoveryServiceis the explicit default when OCI referrer discovery is not configured; it reports discovery as not configured instead of returning a successful empty result.- The remaining simulation-export and audit-bundle gaps are no longer mock/stub runtime debt because the host does not fabricate success or persist canonical state in process. Risk-bundle endpoints now persist durable job state and use the separate
StellaOps.ExportCenter.Workerqueue/lease path for materialization by the worker-hostedRiskBundleJob.
Related resources
- Operations runbook:
operations/runbook.md - Observability:
operations/observability.md(dashboard for offline import:operations/dashboards/export-center-observability.json) - DevPortal offline bundles:
devportal-offline.md(bundle structure, verification workflow, DSSE signature details) - Provenance & signing:
provenance-and-signing.md(manifest/provenance schema, signing pipeline, verification)
Backlog references
EXPORT-ATTEST-75-002cross-team deliverable.
Epic alignment
- Epic 10 – Export Center: deliver canonical JSON, Trivy DB, and mirror bundle workflows with provenance, signatures, and offline parity.
Implementation Status
Delivery Phases
- Phase 1 – JSON & mirror foundations: Stand up service + worker, deliver canonical JSON and mirror profiles, seed schema migrations, publish manifest/provenance formats
- Phase 2 – Trivy adapters & distribution: Implement Trivy DB/Java DB adapters, wire OCI/object storage distribution, expose policy snapshot embedding + verification
- Phase 3 – Delta, encryption, scheduling: Release mirror deltas, bundle encryption, advanced scheduling/automation, resumable downloads, CLI/Console verification workflows
Acceptance Criteria
- Operators can create, monitor, and download exports; verification succeeds against manifest + provenance
- Trivy bundles import cleanly; mirror bundles run in Offline Kit reference environment (full + delta)
- Policy snapshot runs reproduce deterministic decisions with embedded policyVersion + inputsHash
- Tenant scoping and RBAC block unauthorized actions; encryption-enabled bundles lock data to recipient keys
- Metrics and dashboards reflect live runs; alerts trigger on sustained failure rates
- Retried runs remain idempotent with matching manifests, hashes, and distribution artefacts
Key Risks & Mitigations
- Schema drift: Versioned adapters with compatibility gates, CI integration tests, fail-fast with actionable errors
- Bundle bloat: zstd compression, sharding, delta exports, OCI dedupe
- Data leakage: Strict schema allowlists, tenancy filters, redaction enforcement, encryption options
- Non-determinism: Embed policy snapshots, enforce deterministic ordering, include content hashes in manifest
