Export Center Architecture
Derived from Epic 10 – Export Center and the subsequent export adapter deep dives.
The Export Center is the dedicated service layer that packages StellaOps evidence and policy overlays into reproducible bundles. It runs as a multi-surface API backed by asynchronous workers and format adapters, enforcing Aggregation-Only Contract (AOC) guardrails while providing deterministic manifests, signing, and distribution paths.
Product-facing assurance pack summaries live under NIS2 SoA export, NIS2 KPI dashboard, CRA technical file, and CRA conformity dossier. This document remains the ExportCenter implementation architecture and adapter owner map.
Runtime topology
- Export Center API (
StellaOps.ExportCenter.WebService). Receives profile CRUD, export run requests, status queries, artifact listing/download streams, and verification through the unified Web API gateway / Stella Router (serviceName: exportcenter). Enforces tenant scopes, RBAC (export.viewer/export.operator/export.admin), per-tenant/per-profile concurrency guards, and a default authorization fallback policy requiring theexport.viewerscope on every request. Profile/run state is persisted in PostgreSQL (export_centerschema) via canonical repositories; the synchronousPOST /v1/exports/profiles/{id}/runshandler creates the run record directly. - Export Center Worker (
StellaOps.ExportCenter.Worker). AWebApplication/BackgroundServicehost that runs the DevPortal offline bundle job (Worker->DevPortalOfflineJob) and the risk-bundle queue worker (RiskBundleWorker->RiskBundleJob). The risk-bundle worker pollsPostgresRiskBundleJobStore, leases pending or expired running jobs, and materializes API-submitted bundles throughRiskBundleJob. It does not dequeue generic export runs from an Orchestrator queue and does not drive theexport_runslifecycle. (Roadmap: a generic adapter-driven worker pipeline that leasesexport_runsfrom the Orchestrator is described below under Worker pipeline but is not yet the shipped worker host — treat the generic dequeue/adapter-invoke flow as the target design, not current behaviour.) - Backing stores.
- PostgreSQL schema
export_center(and helper schemaexport_center_app) with row-level security keyed offapp.current_tenant. Tables created by001_initial_schema.sql:export_profiles,export_runs,export_inputs,export_distributions. The migration bookkeeping tableexport_schema_versionis created by the migration runner itself (ExportCenterMigrationRunner.VersionTableSql), not by001_initial_schema.sql. There is noexport_eventstable, and no dedicatedexport_artifactstable — the artifact API (IExportArtifactRepository) is a projection overexport_distributionsrows (see Data model). - Migration
002_audit_bundle_jobs.sqladds durable audit-bundle job state inexport_center.audit_bundle_jobs, including tenant-scoped status/progress and EvidenceLocker push results. - Migration
003_distribution_idempotency.sqladds durable export distribution idempotency, retention/deletion markers, OCI manifest metadata, and the widened distribution-kind constraint needed for OCI/Azure/GCS targets. - Migration
004_attestation_persistence.sqladds durableexport_attestationsandpromotion_attestation_assembliestables for DSSE export attestation readback/verification and promotion attestation assembly readback/export. - Migration
005_audit_bundle_worker_payload_staging.sqladds audit-bundle lease metadata plus content-addressed payload storage keys/lengths so completed ZIP and index payloads can live outside inline PostgreSQL row content. - Migration
006_verification_artifacts_exception_reports.sqladds durable verification run/artifact readback and exception report job/content state. - Migration
007_timeline_notifications.sqladds durable timeline notification sink state inexport_center.export_timeline_notifications. - Migration
008_export_incidents.sqladds durable incident lifecycle state inexport_center.export_incidentsandexport_center.export_incident_updates. - Migration
010_audit_bundle_producer_gaps.sqlpersists typed, attributable requested-section gaps on audit-bundle jobs; a completed bundle therefore cannot silently imply that an omitted producer section was present. - Object storage (filesystem or Amazon S3 per
ExportCenter:ObjectStore:Kind) or filesystem for staging bundle payloads. - Optional registry/object storage credentials injected via Authority-scoped secrets.
- PostgreSQL schema
- Current truthful runtime boundary.
- Non-testing
StellaOps.ExportCenter.WebServicenow requires PostgreSQL-backed canonical export repositories and a realEvidenceLocker:BaseUrl; it no longer falls back toDevelopmentin-memory repositories or an in-memory evidence locker client. - Non-testing export verification artifact readback now uses durable Postgres verification run/artifact tables rather than a
501placeholder or in-process state. - Non-testing export attestation and promotion attestation assembly runtime is backed by
PostgresExportAttestationStoreandPostgresPromotionAttestationStore; the signing/assembly services keep the existing deterministic DSSE behavior while persisting tenant-scoped readback state in PostgreSQL. - Non-testing exception report generation and incident management now use durable Postgres job/content and incident/update state; risk bundle job orchestration and simulation export still fail with explicit
501 problem+jsonresponses instead of keeping canonical state in-process. - Non-testing audit-bundle generation is backed by
PostgresAuditBundleJobStoreplusAuditBundleBackgroundWorker:POST /v1/audit-bundlescreates aPendingdurable row, the worker leases pending/expired jobs withFOR UPDATE SKIP LOCKED, and completed bundles push an EvidenceLocker snapshot manifest with artifact hashes and distribution metadata. Requested sections that cannot be materialized are persisted and emitted as typedproducerGaps; they never cause placeholder artifacts to be synthesized. WhenExportCenter:ObjectStore:Kind=FileSystemandExportCenter:ObjectStore:RootPathare configured, completed ZIP and index JSON payloads are staged under content-addressedaudit-bundles/<tenant>/.../sha256/<prefix>/<hash>keys and the Postgres row keeps only metadata/storage keys. Without a configured object store, non-testing processing fails closed throughUnsupportedAuditBundlePayloadStoreinstead of completing with inline/in-memory payload state. - Non-testing export distribution lifecycle is backed by
PostgresExportDistributionRepository; distribution idempotency keys, retention expiry, deletion markers, OCI manifest digest/reference metadata, and status updates are durable Postgres state. - Non-testing mirror exports use
OciReferrerDiscoveryServicebacked by the OCI distribution/referrer HTTP stack instead ofDisabledReferrerDiscoveryService. The service performs registry capability probing, native OCI referrers API discovery, fallback tag discovery, and referrer layer content fetches through configured registry auth. - Non-testing timeline publication is backed by
PostgresExportNotificationSink, which persists channel, event id, dedupe key, tenant reference, event type, serialized payload, delivery state, attempt count, and publish timestamps inexport_center.export_timeline_notifications.Testingcan still opt into the in-memory sink explicitly. - Legacy
/exportsendpoints now return410 Gonewith migration guidance to/v1/exports/*; they no longer claim scheduled, listed, or deleted export work without durable state. - Default helper registrations no longer silently add in-memory stores.
AddExportVerification,AddExportTimelinePublisher, andAddExportDistributionregister unsupported fail-closed services; scheduling, pack-run integration, and tenant-scope enforcement require typed durable stores or explicit*InMemoryForTestinghelpers. - Test harness crypto and disabled-discovery helpers are explicitly named. Age/KMS local wrappers use
AddTestHarnessAgeKeyWrapperandAddTestHarnessKmsClient; missing OCI referrer discovery usesDisabledReferrerDiscoveryServiceand reports “not configured”. Testingcan still opt into the in-memory host implementations, but only through explicitExport:UseInMemory*switches.- Runtime gate (Sprint 20260513_018 URCV-005). Enforcement of the Testing-only flag set now lives in
ExportCenterRuntimeConfigurationValidator, which implements the sharedIRuntimeConfigurationValidatorcontract. The validator runs at hostStartAsyncvia the sharedRuntimeConfigurationValidationHostedService; non-Testinghosts that enable anyExport:UseInMemory*flag (orExport:AllowInMemoryRepositories) fail to start. This replaces the prior in-lineEnforceTestingOnlyFlagstatic.
- Non-testing
Pluginized adapter-pack boundary
ExportCenter has canonical config/trust/scratch roots for pluginized runtime composition, and as of commit e35af41bef (2026-06-07, WS1) the signed mounted exporter-bundle loader is implemented: MountedExportAdapterRuntimePluginLoader admits the Trivy-DB / Trivy-Java-DB exporter bundles through a fail-closed admission chain and feeds the survivors into ExportAdapterRegistry. The docker-compose.plugins.exportcenter.yml overlay now mounts the signed recommended exporter bundle root in addition to config/trust/scratch:
| Purpose | Host path | Container path |
|---|---|---|
| Signed exporter bundle | devops/plugins/exportcenter/recommended/<bundle>/ (manifest.json + <assembly>.dll + <assembly>.dll.sig) | /app/plugins/exportcenter/recommended/<bundle> |
| Operator config/data-pack registry | devops/etc/plugins/exportcenter/ | /app/etc/plugins/exportcenter |
| Adapter trust root | devops/etc/certificates/trust-roots/plugins/exportcenter/cosign.pub | /app/etc/certificates/trust-roots/plugins/exportcenter/cosign.pub |
| Probe scratch | named volume exportcenter-plugin-scratch | /var/lib/stellaops/plugin-scratch/exportcenter |
devops/etc/plugins/exportcenter/registry.yaml is the canonical config path for new composition work. The older devops/etc/plugins/export/registry.yaml file is retained as a legacy alias for unknown consumers and must not be removed until those consumers are audited.
Runtime plugin status is exposed by GET /internal/plugins/status and POST /internal/plugins/probe. Both are mapped AllowAnonymous (Program.cs:128-147) — they bypass the global export.viewer fallback policy, so the /internal/* prefix must be gateway-blocked from external traffic (repeated anonymous probe calls trigger a filesystem plugin-admission scan and are a minor DoS/enumeration vector). The service registers host-owned deterministic adapters by default (json:raw, json:policy, mirror:standard, and runtime:combined) and, when the overlay is applied, the mounted signed Trivy exporters via the loader. Declarative entries such as assurance.profile-pack and standards-mapping-v1 are data packs and cannot execute code. The decided first slice of signed mounted exporters is Trivy DB + Trivy Java DB only (stellaops.exportcenter.trivy-db, stellaops.exportcenter.trivy-java-db); NIS2 is a built-in typed ExportCenter route implemented in Core, and CRA/PDF/custom regulatory adapters are deferred optional bundle-producer work. Until those future optional adapters are staged, they report not-mounted and non-required.
See Mounted exporter-bundle loader below for the full admission chain, config keys, producer, and overlay.
The 2026-06-12 boundary slice landed for NIS2 only (corrected 2026-07-12 against src/):
- NIS2 — extracted, as designed.
StellaOps.ExportCenter.Adapters.Nis2is aTypeForwarders.cscompatibility shell; the executing types (Nis2EffectivenessExporteret al.) live inCore/Adapters/Nis2/, and no non-test service project references the Nis2 adapter project. NIS2 SoA/effectiveness execution is host-owned Core code because the typed NIS2 API is a built-in ExportCenter capability. - CRA — still compiled into and executed by the host.
StellaOps.ExportCenter.WebService.csproj:24carries aProjectReferencetoStellaOps.ExportCenter.Adapters.Cra, andAssuranceExports/AssuranceBundleProduceService.csconstructsnew CraTechFileAdapter()(:486,:524) andnew ConformityDossierAdapter()(:549) in-process. Full CRA tech-file and conformity-dossier bundles are therefore produced in the host today — the host does not keep “only descriptor IDs and media types”. Moving CRA behind a signed mounted bundle remains the target design, not the shipped state.
Mounted exporter-bundle loader (signed bundle admission)
MountedExportAdapterRuntimePluginLoader (src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/MountedExportAdapterRuntimePluginLoader.cs, commit e35af41bef) is wired into StellaOps.ExportCenter.WebService/Program.cs via AddMountedExportAdapters. It is fail-closed: a bundle is only handed to ExportAdapterRegistry after its assembly clears the full admission chain, and every reject path registers zero adapters (asserted by the 22 loader tests + a live forcing-function; the pre-existing ExportAdapterRegistryTests stayed 20/20).
Admission chain (per bundle directory):
- Manifest binding —
manifest.jsonmust declaremodule=exportcenter,contractVersion=runtime-bundle.v1, the matching profile, capabilityexportcenter:adapter, and a well-formed assembly descriptor (relativepath+sha256);idmust equal the bundle directory name. - Payload + per-DLL SHA-256 + path-traversal guard — the manifest payload digest and each DLL are hashed; rooted/escaping paths are rejected.
- Detached RSA-PKCS1-SHA256 verification —
<assembly>.sigis verified viaOfflineDevRsaSha256PluginVerifier(AllowUnsigned=false) against the mounted trust root, then theIExportAdapterentry type(s) are activated viaActivatorUtilities.
The loader installs an AssemblyLoadContext.Default.Resolving hook so the transitive StellaOps.* dependencies a mounted adapter carries (which are not on the host’s TPA list) resolve at activation — this is the “default ALC only probes the TPA list” trap, and the commit proves the fix with an integration test using a real absent-from-host transitive dep.
Configuration (ExportCenter:RuntimePlugins):
| Key | Default | Purpose |
|---|---|---|
ExportCenter:RuntimePlugins:RootPath | /app/plugins/exportcenter | Root containing profile directories. |
ExportCenter:RuntimePlugins:Profile | recommended | Profile directory (<RootPath>/<Profile>). |
ExportCenter:RuntimePlugins:TrustRootPath | /app/etc/certificates/trust-roots/plugins/exportcenter/cosign.pub | Trust-root public key. |
ExportCenter:RuntimePlugins:AllowUnsigned | false | Development-only escape hatch; MUST stay false in production. |
Module/contract constants live in MountedExportAdapterTargets (module exportcenter, probe contract exportcenter.adapter.v1, manifest contract runtime-bundle.v1, capability exportcenter:adapter).
Bundle producer: devops/build/package-runtime-plugins.ps1 -Module exportcenter -Profile recommended -UseOfflineDevSigner publishes the StellaOps.ExportCenter.Core assembly (which carries the Trivy adapters) as the signed stellaops.exportcenter.trivy-db and stellaops.exportcenter.trivy-java-db bundles. The generated cosign.pub is git-ignored under the exportcenter trust-root directory.
Opt-in compose overlay: devops/compose/docker-compose.plugins.exportcenter.yml layers read-only mounts of the recommended bundle root + trust root onto export-web and export-worker, and restates the loader defaults (ExportCenter__RuntimePlugins__RootPath/Profile/TrustRootPath, AllowUnsigned=false). Apply with COMPOSE_EXTRA_FILES=docker-compose.plugins.exportcenter.yml ./scripts/compose-cli.ps1 up.
Probe / status surface: ExportAdapterPluginProbeReporter reflects the live registry, so GET /internal/plugins/status and POST /internal/plugins/probe show admitted bundles and surface rejected bundles with their reason (no silent-green).
Live runtime probe is pending. The loader, the 22 tests + live forcing-function, and the overlay are committed (
e35af41bef), but a live overlay-up acceptance probe against a runningexport-web(mount the signed Trivy bundles, confirm/internal/plugins/probereports them admitted) has not yet been recorded.
Truthful unsupported surface classification
The following Unsupported* services are intentional non-testing runtime behavior, not hidden fallback state. They are acceptable shipped truth because they fail closed instead of fabricating persisted success, but each still maps to a future durable-backend workstream.
| Runtime surface | Current service | Current behavior outside Testing | Classification |
|---|---|---|---|
| Verification artifact readback | PostgresExportVerificationArtifactStore | Reads run metadata, manifest/signature bytes, artifact payloads, expected hashes, content types, and encryption flags from durable Postgres verification tables. | Durable for subfeature B as of SPRINT_20260612_011; missing artifacts return 404 or verification errors rather than fabricated success. |
| Incident management endpoints | PostgresExportIncidentManager | Persists incident activation/update/resolution lifecycle state and update history in Postgres, and publishes durable timeline events through PostgresExportNotificationSink. | Durable for subfeature C as of SPRINT_20260612_011; testing-only in-memory management stays behind Export:UseInMemoryIncidentManager. |
| Risk bundle job orchestration | RiskBundleJobHandler + PostgresRiskBundleJobStore + StellaOps.ExportCenter.Worker | Non-testing runtime persists risk-bundle job state and lifecycle events in PostgreSQL (risk_bundle_jobs, risk_bundle_job_events) and no longer returns the previous exportcenter.risk_bundles.not_implemented 501 default. | API-submitted jobs are queued as durable pending records; RiskBundleWorker leases them and materializes bundles through the worker-hosted RiskBundleJob. |
| Simulation export | UnsupportedSimulationReportExporter | Returns 501 problem+json for simulation export endpoints with error_code=exportcenter.simulation_exports.not_implemented. | ROADMAP; acceptable truthful fail-closed behavior until durable simulation export runtime exists. |
| Exception report generation | ExceptionReportGenerator + PostgresExceptionReportJobStore | Creates durable report jobs, stores status/progress and generated report bytes, and supports list/status/download readback across process restarts. | Durable for subfeature F as of SPRINT_20260612_011; testing-only in-memory generation stays behind Export:UseInMemoryExceptionReportGenerator. |
| Timeline publication sink | PostgresExportNotificationSink | Persists timeline notification envelopes, dedupe keys, delivery state, attempt counts, and serialized payloads in export_center.export_timeline_notifications. | Durable for subfeature A as of SPRINT_20260612_011; testing-only in-memory publication stays behind Export:UseInMemoryTimelineNotificationSink. |
| Verification helper artifact store | UnsupportedVerificationArtifactStore | Throws NotSupportedException from generic default verification helper calls when no durable IExportArtifactStore is supplied. Non-testing StellaOps.ExportCenter.WebService registers PostgresExportVerificationArtifactStore instead. | Acceptable fail-closed helper default; production runtime now uses durable Postgres verification readback. |
| Distribution helper repository default | UnsupportedExportDistributionRepository | Throws NotSupportedException from generic default helper calls when no durable IExportDistributionRepository is supplied. Non-testing StellaOps.ExportCenter.WebService registers PostgresExportDistributionRepository instead. | Acceptable fail-closed helper default; production runtime now uses durable Postgres distribution state. |
| OCI referrer discovery helper default | DisabledReferrerDiscoveryService | Reports discovery/capability probing as not configured for generic adapter-only helper registrations. Non-testing StellaOps.ExportCenter.WebService registers OciReferrerDiscoveryService instead. | Acceptable helper default; production runtime now uses real OCI referrer discovery. |
Integration peers
- Findings Ledger for advisory, VEX, SBOM payload streaming.
- Policy Engine for deterministic policy snapshots and evaluated findings.
- Orchestrator for job scheduling, quotas, and telemetry fan-out.
- Authority for tenant-aware access tokens and KMS key references.
- Console & CLI as presentation surfaces consuming the API.
Gap remediation (EC1–EC10)
- Schemas: publish signed
ExportProfile+ manifest schemas with selector validation; keep in repo alongside OpenAPI docs. - Determinism: per-adapter ordering/compression rules with rerun-hash CI; pin Trivy DB schema versions.
- Provenance: DSSE/SLSA attestations with log metadata for every export run; include tenant IDs in predicates.
- Integrity: require checksum/signature headers and OCI annotations; mirror delta/tombstone rules documented for adapters.
- Security: cross-tenant exports denied by default; enforce approval tokens and encryption recipient validation.
- Offline parity: provide export-kit packaging + verify script for air-gap consumers; include fixtures under
src/ExportCenter/__fixtures. - Advisory link: see
docs-archive/product/advisories/27-Nov-2025-superseded/28-Nov-2025 - Export Center and Reporting Strategy.md(EC1–EC10) for the original requirements; keep it alongside sprint tasks for implementers.
Job lifecycle
Profile model note. A canonical
export_profilesrow has akind(ExportProfileKind:AdHoc,Scheduled,EventDriven,Continuous) plus a separate JSONformatblock (ExportFormatOptions.formatis anExportFormat:JsonRaw,JsonPolicy,Ndjson,Csv,Mirror,TrivyDb,TrivyJavaDb) and ascopeblock. The colon-delimited identifiers below (json:raw,adapter:trivy:db, etc.) are adapter ids (IExportAdapter.AdapterId), not profilekindvalues — see Adapter responsibilities for the authoritative adapter-id list. Beware the near-collision: the two Trivy adapter ids carry anadapter:prefix (adapter:trivy:db,adapter:trivy:java-db), while the unprefixedtrivy:db/trivy:java-dbare the profile names used inprofiles.mdandoverview.md. Assurance profile ids (nis2.*,cra.*,dora.*) belong to the separate Assurance/NIS2 typed routes and registry, not to the genericexport_profilesstore.
- Profile selection. Operator or automation picks a profile mapped (via its
format/adapter) to one of the registered adapters (adapter idsjson:raw,json:policy,runtime:combined,adapter:trivy:db,adapter:trivy:java-db,mirror:standard, opt-inmirror:delta), or invokes an Assurance profile such asnis2.statement-of-applicability,nis2.effectiveness-report,dora.roi,dora.major-incident-report,dora.info-sharing,cra.technical-file, orcra.conformity-dossierthrough the Assurance/NIS2 routes, and submits scope selectors. The genericExportScopesupportstargetKinds,sourceRefs,tags,namespaces,dateRange,maxItems,sampling,runIds, andexcludePatterns(seeCore/Planner/ExportScopeModels.cs). DORA RoI can materialize from source-supplieddora-roi.v1register rows plus an owner approval reference; DORA major-incident and information-sharing can materialize from request-supplied Notify connector inputs; TLPT delegates to EvidenceLocker. Seeprofiles.mdfor profile definitions and configuration fields. - Run creation.
POST /v1/exports/profiles/{profileId}/runsvalidates the profile isActive, enforces concurrency, and writes anexport_run. The run is admitted asRunningimmediately, orQueuedwhen the tenant/profile concurrency limits are exceeded andQueueExcessRunsis enabled (defaults:MaxConcurrentRunsPerTenant=4,MaxConcurrentRunsPerProfile=2,MaxQueueSizePerTenant=10). Over-limit requests without queueing return429. The run trigger is recorded asApifor this path. - (Roadmap) Adapter execution. In the target design the worker streams data from Findings Ledger and Policy Engine using pagination cursors, and adapters write canonical payloads to staging storage, compute checksums, and emit progress. In the current build run progress (
totalItems/processedItems/failedItems/totalSizeBytes) is surfaced through the run record and the SSE stream, but no generic adapter-execution worker host drives it yet (see runtime topology note). - Manifest and provenance emission. Manifest/provenance assembly writes
export.jsonandprovenance.jsonand signs them. Verification readback of these artifacts is served byIExportArtifactStore; outsideTestingthe WebService registersPostgresExportVerificationArtifactStore, which readsexport_center.export_verification_runsandexport_center.export_verification_artifactsrather than buffering artifacts in process. - Distribution registration. Distribution artefacts (download URL, OCI reference, object storage path) are tracked as
export_distributionsrows. - Download & verification. Clients list artifacts (
GET /v1/exports/runs/{runId}/artifacts), download them (.../artifacts/{artifactId}/download), and verify runs (POST /v1/exports/runs/{runId}/verify,GET .../verify/manifest,GET .../verify/attestation).
Cancellation (POST /v1/exports/runs/{runId}/cancel) transitions an in-flight run to the Cancelled status (ExportRunStatus.Cancelled) and records an audit entry; cancellation only succeeds from a cancellable state (otherwise 400).
Core components
API surface
Detailed request and response payloads are catalogued in
api.md. The canonical export API is rooted at/v1/exports(registered byMapExportApiEndpoints). These are the service-internal paths; through the Stella Router gateway the same routes are reached under the/api/export-centerprefix (e.g.POST /api/export-center/v1/exports/profiles/{id}/runs) — CLI/Console integrators must include that prefix. Profile/run/artifact identifiers are GUIDs and route constraints enforce:guid. Each endpoint carries an explicit authorization policy (ExportViewer/ExportOperator/ExportAdmin), shown per row.Profiles API (
/v1/exports/profiles).GET /v1/exports/profiles— list tenant-scoped profiles (status,kind,search,offset,limitquery params). Scope:export.viewer.GET /v1/exports/profiles/{profileId:guid}— get one profile. Scope:export.viewer.POST /v1/exports/profiles— create a profile (CreateExportProfileRequest:name,description,kind,scope,format,signing,schedule); new profiles start inDraft. Scope:export.operator. Audited.PUT /v1/exports/profiles/{profileId:guid}— update a profile (fullUpdateExportProfileRequest). There is noPATCHverb. Scope:export.operator. Audited.DELETE /v1/exports/profiles/{profileId:guid}— archive (soft-delete) a profile. Scope:export.admin. Audited.- The profile CRUD/catalog store remains keyed by canonical tenant UUIDs, but the HTTP surface accepts Authority’s canonical
stellaops:tenantslug claim (withtenant_id/tidaliases for compatibility) and resolves it throughIStellaOpsTenantResolveragainstshared.tenantsbefore repository access. Unknown or inactive tenants fail closed with400; no caller-supplied tenant header is used as a storage key.
Runs API (
/v1/exports).POST /v1/exports/profiles/{profileId:guid}/runs— start a run from a profile (StartExportRunRequest: optionalscopeOverride,formatOverride,correlationId,dryRun); returns202 Acceptedwith the run resource, or429on concurrency limits. Scope:export.operator. Audited.GET /v1/exports/runs— list runs (profileId,status,trigger,createdAfter,createdBefore,correlationId,offset,limit). Scope:export.viewer.GET /v1/exports/runs/{runId:guid}— run status, progress counters, and artifact summaries. Scope:export.viewer.GET /v1/exports/runs/{runId:guid}/events— Server-Sent Events with connect/progress/status-change/disconnect frames (current implementation polls the run record every 2s). Scope:export.viewer.POST /v1/exports/runs/{runId:guid}/cancel— cooperative cancellation with audit logging. Scope:export.operator. Audited.
Artifacts API (
/v1/exports/runs/{runId:guid}/artifacts). Scope:export.viewerfor all.GET .../artifacts— list artifacts produced by a run.GET .../artifacts/{artifactId:guid}— artifact metadata.GET .../artifacts/{artifactId:guid}/download— stream the artifact file (FileStreamHttpResult; audited download). There is no separate top-levelmanifest/provenancedownload verb —export.json/provenance.jsonare retrieved through the verification endpoints below.
Verification API (
/v1/exports/runs/{runId:guid}/verify). Scope:export.viewerfor all.POST .../verify— verify manifest, signatures, and content hashes (VerifyRunRequest:verifyHashes,verifySignatures,verifyManifest,verifyEncryption,checkRekor,trustedKeys). Audited.GET .../verify/manifest— return the run manifest content + digest.GET .../verify/attestation— return attestation status (hasAttestation,attestationType,manifestDigest).POST .../verify/stream— stream verification progress via SSE.
Legacy endpoints (
/exports).GET /exports(export.viewer),POST /exports(export.operator), andDELETE /exports/{id}(export.admin) are registered only to return410 Gone(legacy_export_endpoint_removedproblem+json) with migration guidance to the/v1/exports/*routes.NIS2 SoA typed API.
GET /v1/exports/nis2/soa/profile: discover the active tenant-scoped NIS2 Statement of Applicability profile.POST /v1/exports/nis2/soa/runs: submit a signed NIS2 SoA run from a Policy control-register snapshot and Authority tenant compliance profile.GET /v1/exports/nis2/soa/runs/{runId}: read deterministic run status, progress, hashes, verification summary, and signed bundle URL.GET /v1/exports/nis2/soa/runs/{runId}/bundle: download the production DSSE-signed NIS2 SoA bundle.
Assurance export profile registry.
GET /v1/exports/assurance/profiles: list framework-aware Assurance export profiles. OptionalframeworkId=nis2|dora|crafilters the deterministic built-in profile list; DORA built-ins are advertised for discovery/readiness, anddora.roi,dora.major-incident-report, anddora.info-sharinghave source-supplied HTTP produce paths.GET /v1/exports/assurance/profiles/{profileId}: read a single Assurance profile descriptor.GET /v1/exports/assurance/profiles/{profileId}/readiness: read fail-closed readiness with stable reason codes andsecretValuesExposed=false.POST /v1/exports/assurance/profiles/{profileId}/runs(scopeexport.operator): produce a signed, deterministic Assurance bundle. Supported over HTTP today forcra.technical-file,cra.conformity-dossier,nis2.effectiveness-report, source-supplieddora.roi, source-supplieddora.major-incident-report, source-supplieddora.info-sharing, and EvidenceLocker-delegateddora.tlpt-evidence-pack; fail-closed (409) on blocked readiness.dora.roiaccepts{ doraRoi: { register, ownerApprovalRef, ...refs } }, requires the register tenant to match the authenticated tenant, validates against the vendored EBA RoI taxonomy package, and signs with operator-owned config (Export:Assurance:DoraRoi:SigningKeyFile/:SigningKey), never request-body key material. Its ZIP wrapper is assembled by the sharedDoraRoiBundleAssemblerand byte-matches the offline HMAC CLI wrapper; HTTP-onlyownerApprovalRef/sourceSnapshotRefvalues stay request/readiness metadata and are not serialized intomanifest.json. Deployed images can setExport:Assurance:DoraRoi:TaxonomyPackagePathto a mounted read-only EBA outer ZIP; readiness uses that configured path before clearingdora-roi-schema-mapping-missing.dora.major-incident-reportaccepts{ doraMajorIncidentReport: { report, channel, producedAt } }, maps the JSON-friendly channel DTO into NotifyNotifyChannel, invokes the Notify DORA major-incident report encoder/outbound handoff, and signs with operator-ownedExport:Assurance:DoraMajorIncidentReportHMAC config.dora.info-sharingaccepts{ doraInfoSharing: { request, subscriberApprovalRef } }, maps the request DTO into NotifyDoraInfoSharingExportRequest, emits Notify STIX/TAXII/DSSE artifacts, and signs with operator-ownedExport:Assurance:DoraInfoSharingHMAC config.dora.tlpt-evidence-packaccepts{ tlptEvidencePack: <CreateTlptEvidencePackRequest> }, delegates assembly/signing to EvidenceLockerPOST /api/v1/evidence/capsules/tlpt-packs, and persists the sealed EvidenceLocker response JSON as the Assurance run artifact. Thenis2.effectiveness-reportbody carries{ nis2Effectiveness: { reportMonth, sourceReport, ...refs } }; its HMAC signing key is operator-owned config (Export:Assurance:Nis2Effectiveness:SigningKeyFile/:SigningKey), never the request body, and the output is byte-identical tostella export nis2-effectiveness-report.GET /v1/exports/assurance/profiles/{profileId}/runs/{runId}: read an Assurance produce run.GET /v1/exports/assurance/runs/{runId}/bundle: download a produced Assurance bundle.- The registry is discovery + readiness + (for the wired profiles above) produce/download. It does not replace adapter-owned export execution, and it preserves the legacy typed NIS2 SoA routes for existing Web/CLI callers. Console consumers must use the advertised profile list as the readiness allowlist; route-owned target ids that are not advertised are rendered as unavailable instead of probing non-existent readiness endpoints.
All endpoints require Authority-issued JWT (+ DPoP) tokens with the appropriate export scope and tenant claim alignment. The real export scopes (StellaOps.Auth.Abstractions.StellaOpsScopes) are export.viewer (read-only access to runs and bundles), export.operator (create/update profiles, start/cancel runs), and export.admin (archive profiles; administrative control over retention, encryption keys, and scheduling policies) — these map to the StellaOpsResourceServerPolicies.ExportViewer/ExportOperator/ExportAdmin authorization policies. The colon-form scopes (export:run, export:read, export:download, export:profile:manage) used in earlier drafts do not exist. A global authorization fallback policy requires export.viewer on every request (health probes opt out via AllowAnonymous). The default WebService resource-server allowlist is the Authority standard bootstrap tenant default; local compose also sets Authority__ResourceServer__RequiredTenants__0=default so browser console tokens and ExportCenter tenant policy stay aligned.
Assurance export profile registry
ExportCenter exposes a small built-in registry for optional Assurance-pack exports. The registry uses the generic assurance-evidence-export-v1 descriptor so Web and CLI setup flows can discover available profiles without hard-coding NIS2-only or CRA-only concepts.
| Profile id | Framework | Pack | Adapter id | Payload schema | Compatibility routes |
|---|---|---|---|---|---|
cra.conformity-dossier | cra | cra.technical-documentation | cra:conformity-dossier | conformity-dossier-v1 | stella export conformity-dossier |
cra.technical-file | cra | cra.technical-documentation | cra:tech-file | cra-tech-file-v1 | stella export cra-tech-file |
dora.info-sharing | dora | dora | exportcenter.dora.info-sharing | dora-info-sharing-event-v1 | stella export dora-info-sharing, stella verify dora-info-sharing |
dora.major-incident-report | dora | dora | exportcenter.dora.major-incident-report | dora-major-incident-report.v1 | stella export dora-incident-report, stella verify dora-incident-report |
dora.roi | dora | dora | exportcenter.dora.roi | dora-roi-v1 | stella export dora-roi, stella verify dora-roi |
dora.tlpt-evidence-pack | dora | dora | exportcenter.dora.tlpt-evidence-pack | tlpt-evidence-pack.v1 | stella tlpt pack, stella verify tlpt-pack |
nis2.effectiveness-report | nis2 | nis2 | exportcenter.nis2.effectiveness-report | nis2-effectiveness-report-v1 | stella export nis2-effectiveness-report, POST /v1/exports/assurance/profiles/nis2.effectiveness-report/runs |
nis2.statement-of-applicability | nis2 | nis2 | exportcenter.nis2.soa | nis2-soa-v1 | /v1/exports/nis2/soa/* |
The DORA profiles are advertised with fail-closed readiness. dora.roi is HTTP-expressible when the request supplies the complete Findings dora-roi.v1 register and owner approval reference; missing source rows or approval keep readiness blocked for that request. The produced ZIP uses the same CLI wrapper manifest/signature shape for deterministic offline HMAC byte parity; HTTP-only approval/source refs are not artifact fields. dora.major-incident-report and dora.info-sharing are HTTP-expressible only when the request supplies the existing Notify connector inputs and channel state; ExportCenter maps JSON-friendly DTOs into Notify contracts and must not invent source workflow data. dora.tlpt-evidence-pack is HTTP-expressible only through the EvidenceLocker TLPT request shape and sealed response; ExportCenter does not assemble or sign the pack locally.
Request-aware readiness transformation is owned by the internal AssuranceProduceReadinessResolver. It starts from the registry’s fail-closed result, replaces only the DORA missing-input reasons actually satisfied by the request, preserves unrelated blockers, and ordinal-sorts the final reason set. AssuranceBundleProduceService retains profile dispatch, bundle production, signing gates, and durable run/artifact persistence; the extraction does not change the public readiness or produce contracts.
Readiness uses sealed ExportCenter configuration only as reason codes. NIS2 SoA readiness checks Authority profile access, signed-bundle storage, signing key/provider, and tenant export trust roots. DORA readiness fails closed on the owning workflow prerequisites listed above; it is a readiness descriptor, not a legal-compliance or regulator-submission workflow. CRA technical-documentation readiness checks signed-export storage, signing key/provider, and tenant export trust roots, and separately reports live-publication preflight (security-mailbox-unverified, intake-key-expired-or-missing, rotation-roles-missing, support-lifecycle-metadata-missing). A CRA profile can therefore be export-ready while livePublicationReady=false.
Worker pipeline
Status. This subsection describes the target generic export pipeline. The shipped
StellaOps.ExportCenter.Workerhost (see Runtime topology) currently runs the DevPortal-offline job and the durable risk-bundle queue worker; the generic input-resolver -> adapter-host -> content-writer flow below is implemented at the library level (Core/Adapters,Core/Planner) but is not yet driven by an Orchestrator-leased worker loop.
- Adapter registry. Adapters are registered in DI via
AddExportAdapters(restart-time registration; not a runtime plugin loader). The shipped registry (ExportAdapterRegistry) contains:JsonRawAdapter(json:raw),JsonPolicyAdapter(json:policy),CombinedRuntimeAdapter(runtime:combined, NDJSON),MirrorAdapter(mirror:standard),TrivyDbAdapter(adapter:trivy:db), andTrivyJavaDbAdapter(adapter:trivy:java-db);MirrorDeltaAdapter(mirror:delta) is opt-in viaAddMirrorDeltaAdaptersWithInMemoryStores. Adapters declareSupportedFormatsand resolve byExportFormatorAdapterId. Mind the two Trivy adapter ids carry anadapter:prefix that the other five do not (TrivyDbAdapter.cs:44,TrivyJavaDbAdapter.cs:54). - Input resolvers. The planner/scope resolver (
Core/Planner) queries source systems using stable pagination and compilesExportScopeselectors. Encryption usesBundleEncryptionServicewith optionalIAgeKeyWrapper/IKmsKeyWrapper(test wrappers viaAddTestHarnessAgeKeyWrapper/AddTestHarnessKmsClient). - Content writers.
- JSON adapters emit canonically ordered JSON/NDJSON.
- Trivy adapters materialise databases/archives matching Trivy DB expectations; schema version gates prevent unsupported outputs.
- Mirror adapters assemble deterministic filesystem trees (manifests, indexes, payload subtrees) and, when configured, OCI artefact layers; OCI referrer discovery is via
IReferrerDiscoveryService, defaulting toDisabledReferrerDiscoveryServiceuntil configured.
- Manifest generator. Aggregates counts, bytes, hash digests (SHA-256), profile metadata, and input references. Writes
export.jsonandprovenance.jsonusing canonical JSON (sorted keys, RFC3339 UTC timestamps). - Signing service. Integrates with platform KMS via Authority (default cosign signer). Produces in-toto SLSA attestations when configured. Supports detached signatures and optional in-bundle signatures.
- Distribution drivers.
dist-httpexposes staged files via download endpoint;dist-ocipushes artefacts to registries using ORAS with digest pinning;dist-objstoreuploads to tenant-specific prefixes with immutability flags. - Truthful unsupported paths. The current web host no longer pretends risk/simulation job surfaces are durable. Until durable backends land for those surfaces, those endpoints fail with
501 problem+jsonoutsideTesting. Verification artifact readback, exception report generation, incident management, and timeline publication now use durable Postgres backends. - Helper-level runtime boundary. Default service collection helpers must not be used as hidden local state. Use typed durable repository/store overloads for runtime, or the explicit
*InMemoryForTestingand*TestHarness*helpers in local tests only.
Data model
The schema is PostgreSQL, in the export_center schema, created by the embedded migration 001_initial_schema.sql (StellaOps.ExportCenter.Infrastructure/Db/Migrations/). Migrations are embedded resources (<EmbeddedResource Include="Db\Migrations\**\*.sql" />) and applied on startup by ExportCenterMigrationHostedService → ExportCenterMigrationRunner, which records each applied script (with SHA-256 checksum) in export_center.export_schema_version and refuses to start on a checksum mismatch. Startup application is gated by ExportCenter:Database:ApplyMigrationsAtStartup (default true). This is a service-specific runner, not the shared AddStartupMigrations helper. Every table enables row-level security with an *_isolation policy keyed off export_center_app.require_current_tenant() (app.current_tenant session setting).
Risk-bundle job state is stored by embedded migration 009_risk_bundle_jobs.sql in export_center.risk_bundle_jobs and export_center.risk_bundle_job_events. The WebService opens these rows under the operator/system RLS tenant (00000000-0000-0000-0000-000000000000) and preserves the requested tenant string in tenant_ref, matching the incident-manager pattern for operator-scoped state.
| Table | Purpose | Key columns | Notes |
|---|---|---|---|
export_profiles | Profile definitions. | profile_id (PK), tenant_id, name, description, kind (smallint 1–4), status (smallint 1–4), scope_json, format_json, signing_json, schedule, created_at, updated_at, archived_at. | kind = AdHoc/Scheduled/EventDriven/Continuous; status = Draft/Active/Paused/Archived. Format/variant lives in format_json (ExportFormatOptions), not kind. Unique (tenant_id, name) where archived_at IS NULL; updated_at maintained by trigger. |
export_runs | Run state machine and audit info. | run_id (PK), profile_id (FK), tenant_id, status (smallint 1–6), trigger (smallint 1–4), correlation_id, initiated_by, total_items, processed_items, failed_items, total_size_bytes, error_json, created_at, started_at, completed_at, expires_at. | status = Queued/Running/Completed/PartiallyCompleted/Failed/Cancelled; trigger = Manual/Scheduled/Event/Api. No selectors, policy_snapshot_id, duration_ms, or error_code columns (errors are serialised into error_json). |
export_inputs | Resolved input items per run. | input_id (PK), run_id (FK, cascade), tenant_id, kind (smallint 1–8), status (smallint 1–5), source_ref, name, content_hash (sha256), size_bytes, metadata_json, error_json, created_at, processed_at. | Enables resumable retries and audit. |
export_distributions | Distribution artefacts (and the backing store for the artifact API). | distribution_id (PK), run_id (FK, cascade), tenant_id, kind (smallint 1–5 in SQL), status (smallint 1–6), target, artifact_path, artifact_hash (sha256), size_bytes, content_type, metadata_json, error_json, attempt_count, created_at, distributed_at, verified_at. | ExportDistributionKind enum spans FileSystem/AmazonS3/Mirror/OfflineKit/Webhook/OciRegistry/AzureBlob/GoogleCloudStorage; the domain model also carries OCI/retention/idempotency fields. The IExportArtifactRepository artifact endpoints project over these rows — there is no separate export_artifacts table. |
export_timeline_notifications | Durable timeline notification sink. | event_id (PK), tenant_id, tenant_ref, channel, event_type, payload_json, delivery_state, attempt_count, published_at, delivered_at, last_error. | Used by PostgresExportNotificationSink; duplicate event ids update the same row and increment attempts. |
export_incidents | Durable incident lifecycle state. | incident_id (PK), tenant_id, tenant_ref, type, severity, status, summary, description, affected_tenants_json, affected_profiles_json, activated_at, last_updated_at, resolved_at, activated_by, resolved_by, correlation_id, metadata_json. | Used by PostgresExportIncidentManager; rows are operator-scope incident records while affected tenants/profiles remain payload fields. |
export_incident_updates | Durable incident update trail. | (incident_id, update_id) (PK), tenant_id, timestamp, previous_status, new_status, previous_severity, new_severity, message, updated_by. | Ordered update history for activate/update/resolve operations; cascades with its incident row. |
export_schema_version | Migration bookkeeping. | version (PK), script_name, script_checksum, applied_at_utc. | Created and written by the migration runner (ExportCenterMigrationRunner), not by 001_initial_schema.sql; used for checksum validation. |
There is no export_events table — run timeline frames are produced live by the SSE handler (ExportRunSseEventTypes), not persisted as events. Audit actions are emitted through the unified audit emission pipeline to the Timeline service.
Audit bundles (immutable triage exports)
Audit bundles are a specialized Export Center output: a deterministic, immutable evidence pack for a single subject (and optional time window) suitable for audits and incident response.
- Schema:
docs/modules/evidence-locker/schemas/audit-bundle-index.schema.json(bundle index/manifest with integrity hashes and referenced artefacts). - The index must list Rekor entry ids and RFC3161 timestamp tokens when present; offline bundles record skip reasons in predicates.
- Core APIs (registered by
MapAuditBundleEndpoints; mutations requireexport.operator, while immutable proof reads requireexport.viewer):POST /v1/audit-bundles- Create a new bundle (async generation); returns202 Accepted. Scope:export.operator.GET /v1/audit-bundles- List previously created bundles (subjectName,status,limit,continuationTokenquery params). Scope:export.viewer.GET /v1/audit-bundles/{bundleId}- Returns the bundle job status/details (AuditBundleStatusJSON), includingevidenceBundleId,evidenceRootHash,evidenceError, and typedproducerGaps. It does not stream bundle bytes via anAcceptheader — bytes are served by the dedicated/downloadroute below. Scope:export.viewer.GET /v1/audit-bundles/{bundleId}/download- Streams the completed bundle ZIP (application/zip); returns409 Conflictwhen the bundle is not yetCompleted, and typed503 problem+json(code=payload_unavailable,producer=ExportCenter.ObjectStore) when durable job state says completed but the real payload cannot be read. It never substitutes a generated placeholder ZIP. Scope:export.viewer.GET /v1/audit-bundles/{bundleId}/index- Returns the bundle index manifest (AuditBundleIndexDto). Scope:export.viewer.
- Runtime persistence: non-testing hosts use
PostgresAuditBundleJobStorethroughIAuditBundleJobStore; the explicitInMemoryAuditBundleJobStoreis reserved forTesting. Job reads and lists are tenant-scoped through the resolved StellaOps tenant context. - Worker + payload staging: creation is durable and asynchronous. The API writes a
Pendingjob;AuditBundleBackgroundWorkerleases jobs, advances progress, generates deterministic ZIP/index payloads, and records completion. The explicitTestingin-memory handler registration runs the same background worker, so an accepted in-memory job cannot remain in a pending-only queue. Payloads are stored inline only for explicit testing/no-store harnesses; non-testing runtime uses configured object storage and stores only storage keys/lengths on the job row, or fails closed when the object-store path is absent. - EvidenceLocker handoff: after bundle generation computes the ZIP hash, ExportCenter pushes an
audit-bundleEvidenceLocker snapshot with canonical artifact paths, SHA-256 digests, byte sizes, and a download transcript pointing at/v1/audit-bundles/{bundleId}/download. The background request carries a short-lived, tenant-bound gateway identity envelope with onlyevidence:hold; it does not depend on an ambient browser bearer token. Push failures are recorded on the durable job state and do not replace the generated bundle with fabricated evidence. - Typical contents: vuln reports, SBOM(s), VEX decisions, policy evaluations, and DSSE attestations, plus an integrity root hash and optional OCI reference.
- SBOM and vulnerability sections:
ExportCenter:SbomAudit:BaseUrlandExportCenter:VulnAudit:BaseUrlboth point to SbomService, not Scanner directly. SBOMs are fetched fromGET /api/v1/sbom/subject/{subject}/cyclonedx; vulnerability reports are fetched fromGET /api/v1/scan/results?subject={subject}after SbomService proxies Scanner’s canonical artifact vulnerability report. Both callers acquire a tenant-scoped client-credentials token withsbom:read, cache it by tenant and scope, send it asAuthorization: Bearer, and never send raw tenant headers. Thestellaops-exportcenter-nis2bootstrap client is first-party so normal tenant onboarding grants it to new tenants; Authority startup migration014_authority_exportcenter_first_party_convergence.sqlrepairs active tenants on existing installations. Token failure and 404/204/empty/non-success/unreachable upstream results returnnull, sosbom/cyclonedx.jsonorreports/vuln-report.jsonis omitted instead of fabricated. The shipped compose baseline configures both base URLs to the internal SBOM service alias. - Unified audit events + chain proof (Sprint
SPRINT_20260408_004AUDIT-007; authentication repairSPRINT_20260720_001). WhenincludeContent.auditEventsistrue, the handler acquires a client-credentials token containing onlytimeline:readplus the selectedstellaops:tenantclaim, then pulls the tenant’s events from Timeline’s/api/v1/audit/eventsusing the canonicallimit,startDate,endDate, andcursorquery contract. The token client sends scope and tenant only; Authority derivesaudfrom the registered client and emits both the sharedstellaopsaudience and Timeline’s requiredapi://timelineresource audience. The direct HTTP call sendsAuthorization: Bearer; it never sends a raw tenant header or an unsigned identity envelope. After both upstream reads succeed, the handler writesaudit/events.ndjson(AUDIT_EVENTS) when events exist and always attachesaudit/chain-proof.json(AUDIT_CHAIN_PROOF) sourced from/api/v1/audit/chain/verify. Missing credentials, 401/403/404/5xx, network/timeout failures, malformed response shapes, a missing continuation cursor, or a page-cap truncation abort the pair before either artifact is written and produce the stableproducer_request_failedgap instead of an empty or synthetic proof. The shipped compose baseline enables the internal Timeline URL and reuses the operator-provisioned ExportCenter service-account secret; Authority’s mounted Standard-plugin bootstrap allow-list grants that first-party client onlyauthority:tenants.read sbom:read timeline:readandstellaops api://timeline, without Timeline write/admin scopes or a wildcard audience. Authority startup reconciliation applies mounted config changes to the persisted client row; the config-only correction needs an Authority recreate/restart, not a new Authority image. - Audit bundle manifest + DSSE signature (AUDIT-007 criterion 3).
audit/manifest.jsonis a deterministic, canonicalised manifest (apiVersion: stella.ops/v1,kind: AuditBundleManifest) that binds the bundle id, tenant, subject, time window, audit event count, SHA-256 digests ofevents.ndjson/chain-proof.json, and a stable-sortedproducerGapsarray for every requested section that was unavailable or not implemented. Property names are serialised in ordinal-ascending order so identical inputs produce byte-identical payloads across runs and platforms.audit/manifest.dsse.jsonis a DSSE envelope over the canonical manifest bytes, payloadTypeapplication/vnd.stellaops.audit-bundle-manifest+json. Signing reusesIExportAttestationSigner(local ECDSA by default, or KMS viaAddExportAttestationWithKms) so audit bundles share the same verification path as promotion and export attestations.- Signing is controlled by
includeContent.signManifest(defaulttrue). Whenfalse, only the manifest artifact is emitted. Whentruebut the signer is unavailable, the process-local testing handler records the manifest and omits the envelope; themanifest.jsonartifact remains present so consumers can reverify against trusted public keys later.
- Reference:
docs/product/advisories/archived/27-Nov-2025-superseded/28-Nov-2025 - Vulnerability Triage UX & VEX-First Decisioning.md.
The Web Export Center quick action for Export StellaBundle is expected to use this audit-bundle surface directly. On successful completion the UI must carry the canonical bundleId through the /evidence/exports/bundles handoff, not a synthetic export-run placeholder, so the operator lands on the real generated bundle inventory and can immediately download, verify, or inspect provenance.
Adapter responsibilities
- JSON (
json:raw,json:policy).- Ensures canonical casing, timezone normalization, and linkset preservation.
- Policy variant embeds policy snapshot metadata (
policy_version,inputs_hash,decision_tracefingerprint) and emits evaluated findings as separate files. - Enforces AOC guardrails: no derived modifications to raw evidence fields.
- Trivy (adapter ids
adapter:trivy:db,adapter:trivy:java-db). Note theadapter:prefix —TrivyDbAdapter.AdapterId => "adapter:trivy:db"(Core/Adapters/Trivy/TrivyDbAdapter.cs:44) andTrivyJavaDbAdapter.AdapterId => "adapter:trivy:java-db"(TrivyJavaDbAdapter.cs:54). The unprefixedtrivy:db/trivy:java-dbstrings are the profile names (the WebService adapters also exposeName => "trivy:db",WebService/Adapters/Trivy/TrivyDbAdapter.cs:15); they are not validAdapterIdresolution keys.- Maps StellaOps advisory schema to Trivy DB format, handling namespace collisions and ecosystem-specific ranges.
- Validates compatibility against supported Trivy schema versions; run fails fast if mismatch.
- Emits optional manifest summarising package counts and severity distribution.
- Mirror (
mirror:standard, opt-inmirror:delta). The full-mirror adapter id ismirror:standard(MirrorAdapter); there is nomirror:fulladapter id.- Builds self-contained filesystem layout (
/manifests,/data/raw,/data/policy,/indexes). - The delta adapter (
MirrorDeltaAdapter, registered only viaAddMirrorDeltaAdaptersWithInMemoryStores) compares against a base manifest to write only changed artefacts; recordsremovedentries for cleanup. - Supports optional encryption of
/datasubtree (age/AES-GCM viaBundleEncryptionService) with key wrapping stored inprovenance.json.
- Builds self-contained filesystem layout (
- Combined runtime (
runtime:combined).CombinedRuntimeAdapteremits a deterministic NDJSON (ExportFormat.Ndjson) combined-runtime export ordered by item id; not previously documented.
- DevPortal (
devportal:offline). Note:devportal:offlineis a descriptive profile label, not anIExportAdapterid. It is produced by the worker’sDevPortalOfflineJob(Workerhosted service), not theExportAdapterRegistry.- Packages developer portal static assets, OpenAPI specs, SDK releases, and changelog content into a reproducible archive with manifest/checksum pairs.
- Emits
manifest.json,checksums.txt, helper scripts, and a DSSE signature document (manifest.dsse.json) as described in DevPortal Offline Bundle Specification. - Stores artefacts under
<storagePrefix>/<bundleId>/and signs manifests via the Export Center signing adapter (HMAC-SHA256 v1, tenant scoped).
- CRA (
cra:tech-file).- Assembles the CRA Annex VII technical-file layout defined in CRA Technical File v1:
index.json,checksums.txt, and fixed section documents for product description, design/development/manufacture, risk assessment, EU declaration stub, harmonised standards, test reports, SBOM, attestations, audit log slice, control register, and support policy. - Keeps the bundle evidence-pointer-first; sections cite immutable refs and hashes, including Evidence Locker portable bundle refs when available, instead of copying mutable runtime state.
- Emits deterministic tar+gzip bytes with fixed archive metadata. The file-backed CLI path can add a local HMAC DSSE signature at
signatures/index.dsse.jsonfor offline tamper verification. - Production signing uses the Signer/KMS crypto-provider contract and EU signing payload id
cra-tech-file-v1; default output is DSSE only atsignatures/index.dsse.json, while cadenced mode also emitssignatures/index.cadenced.dsse.jsonand records CAdES companion status. The adapter records durable EvidenceLocker regulatoryArtifactSignedand artifact-ledger entries after the archive hash is known. Missing control-register input or production CAdES provider packs remain explicit blockers rather than fabricated evidence.
- Assembles the CRA Annex VII technical-file layout defined in CRA Technical File v1:
- CRA conformity dossier (
cra:conformity-dossier).- Composes the CRA technical-file bundle and adds the CRA Conformity Dossier v1 overlays for Module A self-assessment, Module B+C EU type-examination plus production conformity, and Module H full QMS.
- Defaults to Module A for Stella itself per CRA-Q5 while keeping B+C and H overlays available for customer products or future SKUs that require stricter conformity routes.
- Preserves deterministic tar+gzip output and records explicit blockers for missing test certificates, audit signatures, QMS evidence, ISO/IEC 27001 control-effectiveness evidence, or Attestor signing support.
- Standards mapping (
standards:mapping).- Consumes only local
standards-mapping-v1YAML files for ISO/IEC 27001, IEC 62443-4-1, IEC 62443-4-2, ETSI EN 303 645, and the future JRC CRA mapping as defined in Standards Mapping v1. - Emits deterministic tar+gzip bundles containing
index.json,checksums.txt, the normalized source mapping, a canonical summary, andsignatures/index.dsse.json. - Signs
index.jsonwith the ExportCenter HMAC DSSE pattern and preserves draft, review, and requirement-level coverage gaps as explicit blockers. Remote source refs are rejected.
- Consumes only local
- NIS2 Statement of Applicability (
nis2:statement-of-applicability).- Consumes the tenant-scoped Policy
nis2-control-register-v1snapshot plus the Authority tenant compliance profile and emits the NIS2 Statement of Applicability v1 bundle. - The Web-callable route is
/v1/exports/nis2/soa/runs; it returns a deterministic run id, completed status/progress for synchronous production generation, bundle hash, SoA export hash, snapshot hash, verification summary, and signed bundle URL. - Production mode signs canonical
nis2-soa-v1statement bytes through the platform crypto-provider registry and verifies against configured tenant export trust roots before the run is recorded. Missing Authority profile, signing key, storage root, or trust roots fail closed instead of falling back to local HMAC signing.
- Consumes the tenant-scoped Policy
- NIS2 effectiveness report (adapter id
exportcenter.nis2.effectiveness-report; Assurance profile idnis2.effectiveness-report).- Consumes the local
nis2-effectiveness-dashboard-v113-area report emitted by the Platform/Telemetry effectiveness endpoint shape and emits the NIS2 Effectiveness Report v1 JSON bundle. - Normalizes source ordering, binds the requested calendar month, hashes the source report and monthly report, and writes fixed manifest entries for the source dashboard, report, and DSSE envelope.
- Uses the same deterministic local HMAC DSSE pattern as other file-backed EU export slices for offline tamper checks. This is not production CAdES/KMS signing; missing target, control-register, or Policy snapshot evidence is recorded as blockers.
- Consumes the local
Adapters expose structured telemetry events (adapter.start, adapter.chunk, adapter.complete) with record counts and byte totals per chunk. Failures emit adapter.error with reason codes.
Signing and provenance
- Manifest schema.
export.jsoncontains run metadata, profile descriptor, selector summary, counts, SHA-256 digests, compression hints, and distribution list. Deterministic field ordering and normalized timestamps. - Provenance schema.
provenance.jsoncaptures in-toto subject listing (bundle digest, manifest digest), referenced inputs (findings ledger queries, policy snapshot ids, SBOM identifiers), tool version (exporter_version, adapter versions), and KMS key identifiers. - Attestation. Cosign SLSA Level 2 template by default; optional SLSA Level 3 when supply chain attestations are enabled. Detached signatures stored alongside manifests; CLI/Console encourage
cosign verify --key <tenant-key>workflow. - Audit trail. Each run stores success/failure status, signature identifiers, and verification hints for downstream automation (CI pipelines, offline verification scripts).
OCI Referrer Discovery
Mirror bundles automatically discover and include OCI referrer artifacts (SBOMs, attestations, signatures, VEX statements) linked to container images via the OCI 1.1 referrers API.
Discovery Flow
┌─────────────────┐ ┌───────────────────────┐ ┌─────────────────┐
│ MirrorAdapter │────▶│ IReferrerDiscovery │────▶│ OCI Registry │
│ │ │ Service │ │ │
│ 1. Detect │ │ 2. Probe registry │ │ 3. Query │
│ images │ │ capabilities │ │ referrers │
│ │ │ │ │ API │
└─────────────────┘ └───────────────────────┘ └─────────────────┘
│
▼
┌───────────────────────┐
│ Fallback: Tag-based │
│ discovery for older │
│ registries (GHCR) │
└───────────────────────┘
Capability Probing
Before starting referrer discovery, the export flow probes each unique registry to determine capabilities:
- OCI 1.1+ registries: Native referrers API (
/v2/{repo}/referrers/{digest}) - OCI 1.0 registries: Fallback to tag-based discovery (
sha256-{digest}.*tags)
Capabilities are cached per registry host with a 1-hour TTL.
Logging at export start:
[INFO] Probing 3 registries for OCI referrer capabilities before export
[INFO] Registry registry.example.com: OCI 1.1 (referrers API supported, version=OCI-Distribution/2.1, probe_ms=42)
[WARN] Registry ghcr.io: OCI 1.0 (using fallback tag discovery, version=registry/2.0, probe_ms=85)
Telemetry Metrics
| Metric | Description | Tags |
|---|---|---|
export_registry_capabilities_probed_total | Registry capability probe operations | registry, api_supported |
export_referrer_discovery_method_total | Discovery operations by method | registry, method (native/fallback) |
export_referrers_discovered_total | Referrers discovered | registry, artifact_type |
export_referrer_discovery_failures_total | Discovery failures | registry, error_type |
Artifact Type Mapping
| OCI Artifact Type | Bundle Category | Example |
|---|---|---|
application/vnd.cyclonedx+json | sbom | CycloneDX SBOM |
application/vnd.spdx+json | sbom | SPDX SBOM |
application/vnd.openvex+json | vex | OpenVEX statement |
application/vnd.csaf+json | vex | CSAF document |
application/vnd.in-toto+json | attestation | in-toto attestation |
application/vnd.dsse.envelope+json | attestation | DSSE envelope |
application/vnd.slsa.provenance+json | attestation | SLSA provenance |
Error Handling
- If referrer discovery fails for a single image, the export logs a warning and continues with other images
- Network failures do not block the entire export
- Missing referrer artifacts are validated during bundle import (see ImportValidator)
Related Documentation
Distribution flows
- HTTP download. Console and CLI stream artifacts via
GET /v1/exports/runs/{runId}/artifacts/{artifactId}/download. The current handler returns the file as aFileStreamHttpResultwith the artifact’s content type (defaultapplication/octet-stream) and filename, and emits an audited download event. (The previously documentedX-Export-Digest/X-Export-Lengthresponse headers and explicit range/resumable support are not implemented by this handler; checksums are exposed via the artifact metadatachecksumfield instead.) - OCI push. Worker uses ORAS to publish bundles as OCI artefacts with annotations describing profile, tenant, manifest digest, and provenance reference. Supports multi-tenant registries with
repository-per-tenantnaming. - Object storage. Writes to tenant-prefixed paths (
s3://stella-exports/{tenant}/{run-id}/...) with immutable retention policies. Retention scheduler purges expired runs based on profile configuration. - Offline Kit seeding. Mirror bundles optionally staged into Offline Kit assembly pipelines, inheriting the same manifests and signatures.
Observability
- Metrics. The meter is
StellaOps.ExportCenter(ExportTelemetry.Meter, version1.0.0). All counter/histogram/gauge names are prefixedexport_(defined inTelemetry/ExportTelemetry.cs).- Core counters:
export_runs_total,export_runs_success_total,export_runs_failed_total,export_artifacts_total,export_bytes_total,export_concurrency_limit_exceeded_total,export_artifact_downloads_total,export_sse_connections_total,export_audit_events_total. - OCI referrer-discovery counters:
export_registry_capabilities_probed_total,export_referrer_discovery_method_total,export_referrers_discovered_total,export_referrer_discovery_failures_total. - Timeline counters:
export_timeline_events_published_total,export_timeline_events_failed_total,export_timeline_events_deduplicated_total. - Incident counters:
export_incidents_activated_total,export_incidents_resolved_total,export_incidents_escalated_total,export_incidents_deescalated_total,export_notifications_emitted_total. - Risk-bundle / simulation counters:
export_risk_bundle_jobs_submitted_total,export_risk_bundle_jobs_completed_total,export_simulation_exports_total. - Histograms:
export_run_duration_seconds,export_plan_duration_seconds,export_bundle_size_bytes,export_incident_duration_seconds,export_risk_bundle_job_duration_seconds. - Gauge (UpDownCounter):
export_runs_in_progress. - (The earlier
exporter_run_duration_seconds/exporter_run_bytes_total{profile}/exporter_active_runs{tenant}names —exporter_*prefix — are not emitted; the histogram isexport_run_duration_seconds. The doc previously listedconcurrency_limit_exceeded_total/artifact_downloads_total/sse_connections_totalwithout theexport_prefix — the real names carry it.)
- Core counters:
- Logs. Structured logs with run/tenant/profile/adapter/phase fields and correlation ids (
ExportLoggerExtensions). - Traces. Optional OpenTelemetry spans from the
StellaOps.ExportCenteractivity source (ExportActivityExtensions):export.run,export.plan,export.write,export.distribute. (There are noexport.fetchorexport.signspans in the current build; theexport.runroot span was not previously documented.) - Dashboards & alerts. DevOps pipeline seeds Grafana dashboards summarising throughput, size, failure ratios, and distribution latency. Alert thresholds: failure rate >5% per profile, median run duration >p95 baseline, signature verification failures >0. Runbook + dashboard stub for offline import:
operations/observability.md,operations/dashboards/export-center-observability.json.
Security posture
- Tenant claim enforced at every query and distribution path; cross-tenant selectors rejected unless explicit cross-tenant mirror feature toggled with signed approval.
- RBAC scopes:
export.viewer(read runs/bundles/artifacts/verify),export.operator(profile create/update, run start/cancel),export.admin(profile archive; retention/encryption/scheduling policy administration). Console hides actions without scope; CLI returns401/403. (The colon-formexport:profile:manage/export:run/export:read/export:downloadscopes are not used.) - Encryption options configurable per profile; keys derived from Authority-managed KMS. Mirror encryption uses tenant-specific recipients; JSON/Trivy rely on transport security plus optional encryption at rest.
- Restart-only plugin loading ensures adapters and distribution drivers are vetted at deployment time, reducing runtime injection risks.
- Deterministic output ensures tamper detection via content hashes; provenance links to source runs and policy snapshots to maintain auditability.
Deployment considerations
- Packaged as separate API and worker containers. Helm chart and compose overlays define horizontal scaling, worker concurrency, queue leases, and object storage credentials.
- Requires Authority client credentials for KMS and optional registry credentials stored via sealed secrets.
- Offline-first deployments disable OCI distribution by default and provide local object storage endpoints; HTTP downloads served via internal gateway.
- Health endpoints:
/healthz(liveness, no checks) and/readyz(readiness) are mapped anonymously so orchestrator probes are not blocked by the globalexport.viewerfallback policy. A/buildinfoendpoint exposes image provenance to the operator verify aggregator. (There is no/health/readyroute.)
Compliance checklist
- [ ] Profiles and runs enforce tenant scoping; cross-tenant exports disabled unless approved.
- [ ] Manifests and provenance files are generated with deterministic hashes and signed via configured KMS.
- [ ] Adapters run with restart-time registration only; no runtime plugin loading.
- [ ] Distribution drivers respect allowlist; OCI push disabled when offline mode is active.
- [ ] Metrics, logs, and traces follow observability guidelines; dashboards and alerts configured.
- [ ] Retention policies and pruning jobs configured for staged bundles.
Client capability surfacing
The ExportSurfacingClient extends the existing ExportCenterClient to expose backend capabilities that were previously not reachable from CLI/UI consumers.
Surfaced Capabilities
| Capability | Interface Method | Route |
|---|---|---|
| Profile CRUD | CreateProfileAsync, UpdateProfileAsync, ArchiveProfileAsync | POST/PUT/DELETE /v1/exports/profiles |
| Run Lifecycle | StartRunAsync, CancelRunAsync | POST /profiles/{id}/runs, POST /runs/{id}/cancel |
| Artifact Browsing | ListArtifactsAsync, GetArtifactAsync, DownloadArtifactAsync | GET /runs/{id}/artifacts |
| Verification | VerifyRunAsync, GetManifestAsync, GetAttestationStatusAsync | POST /runs/{id}/verify, GET .../manifest, GET .../attestation |
| Assurance Exports | ListAssuranceExportProfilesAsync, GetAssuranceExportProfileAsync, GetAssuranceExportReadinessAsync | GET /v1/exports/assurance/profiles, GET /v1/exports/assurance/profiles/{id}, GET /v1/exports/assurance/profiles/{id}/readiness |
| Discovery | DiscoverCapabilitiesAsync | Local capability catalogue |
Key Types
| Type | Location | Purpose |
|---|---|---|
IExportSurfacingClient | Client/IExportSurfacingClient.cs | Interface for extended operations |
ExportSurfacingClient | Client/ExportSurfacingClient.cs | HTTP implementation |
ExportSurfacingModels.cs | Client/Models/ | DTOs for profile CRUD, artifacts, verification, attestation status, capability discovery |
AssuranceExportModels.cs | Client/Models/ | DTOs for Assurance profile discovery and readiness |
DI Registration
AddExportSurfacingClient(Action<ExportCenterClientOptions>) in ServiceCollectionExtensions.cs — reuses the same ExportCenterClientOptions.
Test Coverage
- Models: CreateExportProfileRequest defaults, UpdateExportProfileRequest nulls, StartExportRunRequest defaults, ExportArtifact roundtrip, empty list, VerifyExportRunRequest defaults, ExportVerificationResult, HashVerificationEntry match/mismatch, SignatureVerificationEntry, ExportManifest, ExportAttestationStatus, ExportCapability, ExportCapabilitySummary, StartExportRunResponse, Assurance export profile/readiness DTOs.
- Client: Constructor null guards, DiscoverCapabilities, profile CRUD/run/artifact/verification argument validation, and Assurance export profile discovery/get/readiness calls.
