Export Flow
Currency check 2026-10-07: owner paths/source placement verified against
3238bd511a37e199ae99d4f17be2f13eabec8dd7. The supported boundary is/api/offlinekit/v1/exports(src/OfflineKit/StellaOps.OfflineKit.WebService/Program.cs). Re-verify the host mount and owning endpoint extension together. This is source verification; deployment and broader audit coverage are separate evidence. Other dated findings retain their stated dates. Re-verify:rg -n "MapGroup|Map.*Endpoints|AddHostedService" src/OfflineKit/StellaOps.OfflineKit.WebService/Program.cs, then inspect the endpoint extension or library pointer cited at the operation.
Supported implementation
The export application is hosted by offlinekit-web, with background profile-run execution in offlinekit-worker. The supported base is /api/offlinekit/v1/exports; the application groups are mounted below /api/offlinekit by src/OfflineKit/StellaOps.OfflineKit.WebService/Program.cs. The retained StellaOps.ExportCenter.* library and DTO names identify application code, rather than a separate deployable.
| Operation | Current route |
|---|---|
| List/create profiles | GET/POST /api/offlinekit/v1/exports/profiles |
| Read/update/archive profile | GET/PUT/DELETE /api/offlinekit/v1/exports/profiles/{profileId} |
| Submit a profile run | POST /api/offlinekit/v1/exports/profiles/{profileId}/runs |
| List/read/cancel runs | GET /api/offlinekit/v1/exports/runs, GET /api/offlinekit/v1/exports/runs/{runId}, POST /api/offlinekit/v1/exports/runs/{runId}/cancel |
| Run events | GET /api/offlinekit/v1/exports/runs/{runId}/events |
| Artifact metadata/download | GET /api/offlinekit/v1/exports/runs/{runId}/artifacts, GET /api/offlinekit/v1/exports/runs/{runId}/artifacts/{artifactId}, GET /api/offlinekit/v1/exports/runs/{runId}/artifacts/{artifactId}/download |
| Carrier descriptor | GET /api/offlinekit/v1/exports/runs/{runId}/carrier |
| Verification | POST /api/offlinekit/v1/exports/runs/{runId}/verify, GET /api/offlinekit/v1/exports/runs/{runId}/verify/manifest, GET /api/offlinekit/v1/exports/runs/{runId}/verify/attestation, POST /api/offlinekit/v1/exports/runs/{runId}/verify/stream |
| Assurance discovery/readiness | GET /api/offlinekit/v1/exports/assurance/profiles, GET /api/offlinekit/v1/exports/assurance/profiles/{profileId}, GET /api/offlinekit/v1/exports/assurance/profiles/{profileId}/readiness |
| Assurance production/readback/download | POST /api/offlinekit/v1/exports/assurance/profiles/{profileId}/runs, GET /api/offlinekit/v1/exports/assurance/profiles/{profileId}/runs/{runId}, GET /api/offlinekit/v1/exports/assurance/runs/{runId}/bundle |
| NIS2 SoA | GET /api/offlinekit/v1/exports/nis2/soa/profile, POST /api/offlinekit/v1/exports/nis2/soa/runs, GET /api/offlinekit/v1/exports/nis2/soa/runs/{runId}, GET /api/offlinekit/v1/exports/nis2/soa/runs/{runId}/bundle |
Ordinary profiles and runs use GUID identifiers. Assurance profile identifiers are the named catalogue ids, such as cra.technical-file and dora.roi. Endpoint extensions and DTOs are in src/OfflineKit/__Libraries/StellaOps.ExportCenter.Application/Api/, AssuranceExports/ and Nis2Soa/. The ordinary export, Assurance and typed SoA groups apply export.viewer to every route. Writes/run submission add export.operator; ordinary profile archive adds export.admin. Inherited, route-specific and configured resource-server default scopes apply together; a mutation scope does not substitute for Viewer.
The web host authenticates the Router identity envelope before resource-server authentication. Export requests pass through tenant middleware backed by the local tenants catalogue replica. A disabled required replica is an error, not proof that a tenant does not exist. The custody/import plane is excluded from that middleware and resolves its authenticated tenant itself.
An active profile submission returns 202 Accepted with a Queued run and a current-prefix status URL. ExportRunWorker leases queued work only when Export:Runs:Worker:Enabled is enabled on the worker. The executor writes declared inputs or a mirror bundle, records artifacts through distributions, then records completion/failure. The web role does not execute ordinary queued profile runs. Declared local inputs are restricted by Export:Runs:DeclaredInputRoots; mirror sources are absolute local paths or file:// URIs. This is implemented execution.
Carrier production uses Export:Carrier:SigningKeyId and the configured crypto registry (CryptoRegistryCarrierSigner). Operator-mounted material is registered through the configured key; there is no carrier-signing Signer HTTP hop. Without a carrier key id, a run can complete without a carrier and records EXPORT_RUN_CARRIER_NOT_WRITTEN; carrier-writing failure fails the run. The descriptor lists the five carrier files and their artifact download URLs.
Both roles use their family-owned stellaops_offlinekit database. Startup registration and the embedded chain under src/OfflineKit/__Libraries/StellaOps.OfflineKit.Persistence/Migrations/ own schema convergence; the connection is STELLAOPS_POSTGRES_OFFLINEKIT_CONNECTION (configuration override Postgres:OfflineKit:ConnectionString).
Operator references
Historical ExportCenter flow reconciled 2026-05-31 (superseded 2026-09-14)
The remainder records the predecessor implementation or unlanded design at the dates stated in its text. All routes, host names, source paths, DTOs, metrics and examples in this section are historical comparison, including sentences that originally said “current” or “today”. They are not instructions for the consolidated estate. The supported section and owner source above supersede them. Do not rebuild or deploy a predecessor from these examples.
Reconciliation note (re-verified against
src/ExportCenter, 2026-05-31). Export Center is a deterministic evidence-export engine built around tenant-scoped profiles that are run to produce signed, verifiable artifacts (canonical JSON, mirror bundles, compliance dossiers) — not a Chromium-rendered “PDF/Excel report” service. The flow below is the profile → run → artifact model that the code defines (/v1/exports/*). Sections that describe behavior not present in the codebase are explicitly marked NOT IMPLEMENTED, ORPHANED, or Draft/roadmap.Key implementation-status caveat: the HTTP API (profiles, runs, artifacts, verify, SSE, manifest) and the typed export surfaces (attestation, NIS2 SoA, risk bundle, audit/exception/lineage) are implemented, but the deployed Worker does not execute the generic profile→run phase pipeline — see the “Worker reality” note under Actors. Treat the worker-driven phases (§4) and the diagram’s worker column as the intended design, not live behaviour.
trivy-db retired (owner ruling 2026-08-09). Stella Ops does not support trivy-db. The
TrivyDbAdapter/TrivyJavaDbAdapterand thetrivy:db/trivy:java-dbprofiles are gone (moved tosrc/__Obsoleted/); theExportFormat.TrivyDb/TrivyJavaDbenum members are retained only so historical persisted plans still deserialize, and a plan requesting them now fails closed at adapter resolution. The offline vulnerability-feed capability is served instead by our own digest-attested compact vuln DB (stella vuln-db export --from-hub <url>).
Overview
Export Center generates and delivers deterministic evidence exports: canonical JSON archives, full/delta mirror bundles, and compliance dossiers (CRA, NIS2, standards mappings). Exports are defined as reusable profiles and executed as runs; every run produces hashed artifacts plus a signed manifest that can be independently verified offline.
The unit of work is a profile (the what/how/where), and a run (a single deterministic execution of a profile). Runs are tenant-scoped, can be triggered ad-hoc, on a cron schedule, by an event, or via API, and converge to identical bundle bytes for identical inputs.
Audience: operators and auditors who configure export profiles and download evidence, and integrators building tools that consume or verify Export Center artifacts.
Business Value: Deterministic, signed, offline-verifiable exports give auditors and downstream tools reproducible evidence without depending on a live StellaOps instance.
Actors
| Actor | Type | Role |
|---|---|---|
| User | Human | Creates/activates profiles, starts runs, downloads artifacts |
| Console | System | UI for profile and run management |
| Gateway / Router | Service | Routes export requests (Stella Router microservice mesh) |
| ExportCenter (WebService) | Service | Profile/run lifecycle, artifact + manifest serving, verification, SSE |
| ExportCenter (Worker) | Service | Background batch jobs (DevPortal offline bundle, risk bundle). See Worker reality note below — the generic profile→run phase pipeline is not driven by a deployed worker. |
| Adapters | Library | JSON raw/policy, combined runtime NDJSON, Mirror standard/delta, OCI distribution, compliance dossiers |
| EvidenceLocker | Service | Stores export manifest snapshots + distribution transcripts (optional) |
| Distribution targets | Storage | FileSystem, OCI registry, Mirror, OfflineKit, S3/Azure/GCS (see note) |
NOTE —
RustFS,Scanner,Policyas direct flow actors. The original doc listed Scanner/Policy/RustFS as inline data sources queried during the flow. In code the run pipeline resolves a scope into items via theExportScopeResolver/ExportPlanner(Core/Planner/*) and the configured adapters; there is no hard-codedGET /internal/scans/{id}/GET /internal/verdicts/{scan_id}orchestration inExportCenter. RustFS is not referenced anywhere insrc/ExportCenter. Artifact persistence is to the configured distribution targets / local artifact store (IExportArtifactStore), not a fixed RustFS layout.
NOT IMPLEMENTED — worker-driven run execution (the generic phase pipeline). The deployed
StellaOps.ExportCenter.Workerhost registers exactly twoBackgroundServices (Worker/Program.cs):Worker(a one-shot DevPortal offline bundle job,DevPortalOfflineJob) andRiskBundleWorker(a one-shot risk bundle job). Each runs once if enabled, then idles onTask.Delay(Timeout.Infinite). There is no hosted service that polls theExportRuntable and drives a started run throughScopeResolution → … → Finalize. TheExportPlanner/ExportScopeResolver(Core/Planner/*) and theExportPhaseKindmodel exist as building blocks but are not invoked by any deployed run-executor loop. Concretely:POST …/profiles/{id}/runscreates anExportRunalready inRunningstate (ExportApiEndpoints.StartRunFromProfile) but nothing in the deployed services transitions it toCompleted, producesExportArtifacts, signs, or distributes for the generic profile pipeline. The worker-column steps in the flow diagram below and the §4 phase list describe the intended engine, not behaviour wired into the shipped Worker. The artifact/manifest/verify/download read paths (§5–§7) and the typed export surfaces (attestation, NIS2 SoA, risk bundle, audit/exception/lineage — see “Other typed export surfaces”) are separately implemented and do produce output.
Prerequisites
- Caller holds the required export scope (see Authorization below).
- An export profile exists and is in
Activestatus (runs can only start from anActiveprofile;Draft/Paused/Archivedprofiles are rejected). - Source data exists for the profile’s resolved scope.
Authorization (scopes)
Verified against StellaOpsScopes.cs and StellaOpsResourceServerPolicies.cs. Scope claim values are dot-form (no colon-form variant exists for Export Center):
| Scope (claim value) | Policy constant | Grants |
|---|---|---|
export.viewer | ExportViewer | Read-only: list/get profiles, runs, artifacts; download; verify; manifest; attestation status; SSE streams |
export.operator | ExportOperator | Create/update profiles; start runs; cancel runs |
export.admin | ExportAdmin | Archive (soft-delete) profiles; retention/encryption/scheduling administration |
The WebService applies a global fallback policy requiring authentication + export.viewer; write paths additionally require export.operator and archive requires export.admin. Health probes (/healthz, /readyz) are anonymous.
Supported Export Formats
Verified against ExportFormat (Core/Planner/ExportPlanModels.cs) and the profile catalogue (docs/modules/export-center/profiles.md).
| Format enum | Description |
|---|---|
JsonRaw | Canonical raw JSON / JSONL archives (advisories, VEX, SBOMs) |
JsonPolicy | JsonRaw plus policy snapshot + evaluated findings |
Ndjson | Newline-delimited JSON (streaming) |
Csv | CSV |
Mirror | Full mirror layout with indexes (full / delta variants) |
TrivyDb | RETIRED 2026-08-09 — enum value 6 retained only for historical persisted plans; no adapter declares it in SupportedFormats, so nothing can produce it (not “fails closed at adapter resolution”: GetAdapterForFormat has no production call site — verified 2026-08-14) |
TrivyJavaDb | RETIRED 2026-08-09 — enum value 7, same reason as TrivyDb |
Compression is independent (CompressionFormat: None, Gzip, Zstd, Brotli).
NOT IMPLEMENTED — PDF, Excel/
.xlsx, SARIF, CycloneDX/SPDX as Export Center output formats. The original doc’s format table (PDF reports rendered via Chromium, Excel spreadsheets, SARIF,.cdx.json/.spdx.jsondirect outputs) does not correspond to anyExportFormatvalue or adapter insrc/ExportCenter. Export Center emits evidence/data bundles, not human-readable rendered reports. SBOMs are carried as inputs/records (ExportInputKind.Sbom) inside JSON/mirror bundles, not as a standalone CycloneDX/SPDX export format selectable here. (SBOM document generation lives in the SBOM flow — see SBOM Generation Flow.)
Draft/roadmap — compliance dossier “formats”. CRA technical-file / conformity-dossier, NIS2 SoA / effectiveness reports, and standards mappings are real profile variants (see
Adapters.Cra,Adapters.Nis2,Adapters.StandardsMappingandprofiles.md), exposed via dedicated typed endpoints rather than a genericformatselector.
Flow Diagram
The flow is profile-first: a profile is created and activated, then a run is started from that profile. The intended design is that the run is planned (scope resolution), executed by a worker (data fetch → transform → write → manifest → sign → distribute), and progress is streamed over SSE.
Read with the “Worker reality” note above. The ExportCenter Wkr column below (data-fetch → transform → write → manifest → sign → distribute) is the intended engine. In the shipped code the API creates the run in
Runningstate, but no deployed background service executes these phases for the generic profile pipeline, so therun.completed/artifact steps are not driven end-to-end today. The diagram is retained as the target design.
┌──────────────────────────────────────────────────────────────────────────────────┐
│ Export Flow (profile → run → artifact) │
└──────────────────────────────────────────────────────────────────────────────────┘
┌────────┐ ┌─────────┐ ┌──────────────────┐ ┌───────────────────┐ ┌──────────────┐
│ User │ │ Console │ │ ExportCenter API │ │ ExportCenter Wkr │ │ Distribution │
└───┬────┘ └────┬────┘ └────────┬─────────┘ └─────────┬─────────┘ └──────┬───────┘
│ │ │ │ │
│ Create & │ POST /v1/exports/profiles │ │
│ activate │ PUT …/{id} (status=Active) │ │
│ profile │───────────────>│ │ │
│ │ 201 / 200 │ │ │
│<───────────│<───────────────│ │ │
│ │ │ │ │
│ Start run │ POST /v1/exports/profiles/{id}/runs │ │
│───────────>│───────────────>│ │ │
│ │ │ (concurrency check; │ │
│ │ │ Running or Queued) │ │
│ │ 202 Accepted │ Location: │ │
│ │ {runId,status}│ …/runs/{runId} │ │
│<───────────│<───────────────│ │ │
│ │ │ │ │
│ Subscribe │ GET …/runs/{runId}/events (SSE) │ │
│───────────>│───────────────>│ plan: scope-resolve│ │
│ │ event:connected │ │
│ │ event:run.started │ │
│ │ │ data-fetch ────────>│ │
│ │ event:run.progress │ transform/redact │
│ │ │ │ write output │
│ │ │ │ generate manifest │
│ │ │ │ sign (optional) │
│ │ │ │ distribute ──────>│
│ │ event:run.completed │ │
│<───────────│<───────────────│ │ │
│ │ │ │ │
│ Download │ GET …/runs/{runId}/artifacts/{aid}/download │
│───────────>│───────────────>│ (file stream) │ │
│<───────────│<───────────────│ │ │
│ Verify │ POST …/runs/{runId}/verify │ │
│───────────>│───────────────>│ (manifest+sig+hashes)│ │
│<───────────│<───────────────│ │ │
Step-by-Step
1. Create / activate an export profile
Profiles carry the scope, format, signing options, and (optional) cron schedule. A newly created profile starts in Draft; a run can only be started once it is moved to Active (via PUT).
POST /v1/exports/profiles HTTP/1.1
Authorization: Bearer {jwt} # requires export.operator
Content-Type: application/json
{
"name": "weekly-evidence-json",
"description": "Canonical JSON evidence archive",
"kind": "Scheduled",
"scope": {
"targetKinds": ["sbom", "vex", "advisory"],
"namespaces": ["acme/payments"],
"dateRange": { "from": "2026-05-01T00:00:00Z", "to": "2026-05-30T00:00:00Z", "field": "CreatedAt" },
"maxItems": 5000
},
"format": {
"format": "JsonPolicy",
"compression": "Zstd",
"sortKeys": true,
"normalizeTimestamps": true
},
"signing": { "enabled": true, "algorithm": "ES256", "includeProvenance": true },
"schedule": "0 8 * * MON"
}
Tenant identity: the WebService derives the tenant from the
tenant_id(ortid) claim on the JWT, not from anX-Tenant-Idheader. Requests without a resolvable tenant claim return400. (The original doc’sX-Tenant-Id: acme-corpheader is not how tenancy is resolved here.)
ExportProfileKind values: AdHoc, Scheduled, EventDriven, Continuous. ExportProfileStatus values: Draft, Active, Paused, Archived.
2. Start a run from the profile
POST /v1/exports/profiles/{profileId}/runs HTTP/1.1
Authorization: Bearer {jwt} # requires export.operator
Content-Type: application/json
{
"scopeOverride": null,
"formatOverride": null,
"correlationId": "trace-abc123",
"dryRun": false
}
The API enforces per-tenant and per-profile concurrency limits (ExportConcurrencyOptions: default MaxConcurrentRunsPerTenant=4, MaxConcurrentRunsPerProfile=2, QueueExcessRuns=true, MaxQueueSizePerTenant=10). If limits are exceeded and queuing is disabled, the API returns 429 Too Many Requests.
On success it returns 202 Acceptedwith a Location header pointing at the run resource, and a body whose status is Running (or Queued if it had to be queued):
{
"runId": "9f1c…",
"profileId": "3a2b…",
"tenantId": "11…",
"status": "Running",
"trigger": "Api",
"correlationId": "trace-abc123",
"initiatedBy": "user:alice",
"progress": { "totalItems": 0, "processedItems": 0, "failedItems": 0, "totalSizeBytes": 0, "percentComplete": 0 },
"createdAt": "2026-05-30T10:30:00Z",
"startedAt": "2026-05-30T10:30:00Z",
"expiresAt": "2026-06-06T10:30:00Z"
}
ExportRunStatus: Queued, Running, Completed, PartiallyCompleted, Failed, Cancelled. ExportRunTrigger: Manual, Scheduled, Event, Api. Runs default to a 7-day expiresAt (now.AddDays(7)). The API-created run is written directly in Running (or Queued) state with empty progress; the Trigger for runs started through POST …/runs is always Api.
Caveat (see “Worker reality”). The run is created
Running, but for the generic profile pipeline no deployed worker subsequently advances it toCompleted/Failedor produces artifacts. The status/progress fields and the §3–§4 planning model exist, but the end-to-end run engine is not wired in the shipped Worker host today.
3. Scope resolution & planning
The planner (Core/Planner/ExportPlanner.cs, ExportScopeResolver.cs) turns the profile’s ExportScope into a concrete set of ResolvedExportItems and an ExportPlan of ordered phases. ExportScope filters include: targetKinds, sourceRefs, tags, namespaces, dateRange, runIds, excludePatterns, maxItems, and deterministic sampling (None/Random/First/Last/Stratified/Systematic).
Each input is tracked as an ExportInput with ExportInputKind (Sbom, Vex, Attestation, ScanReport, PolicyResult, Evidence, RiskBundle, Advisory) and an ExportInputStatus (Pending/Processing/Processed/Failed/Skipped).
NOT IMPLEMENTED (as a fixed data-gather contract): the original “Data Gathering” table with
GET /internal/scans/{id},GET /internal/verdicts/{scan_id},GET /internal/vex/applied/{scan_id},GET /internal/sboms/{digest}. No such fixed internal-call sequence exists insrc/ExportCenter; data is resolved via the scope resolver and adapters.
4. Execution phases
ExportPhaseKind (Core/Planner/ExportPlanModels.cs) defines the intended worker pipeline order. The ExportPlanner builds an ExportPlan of ExportPlanPhases in this order, but — per the “Worker reality” note above — no deployed background service currently executes the plan for the generic profile pipeline, so treat this list as the enumerated design, not a live runtime sequence:
1. ScopeResolution – resolve scope and collect items
2. DataFetch – fetch and transform source data
3. Transform – normalization / redaction
4. WriteOutput – write output files (JSON/Mirror/…)
5. GenerateManifest – compute checksums + manifest
6. Sign – sign artifacts (when signing.enabled)
7. Distribute – push to distribution targets
8. Verify – verify distribution
9. ApplyRetention – apply retention policy
10. Finalize – cleanup / finalization
NOT IMPLEMENTED — Chromium PDF rendering. The original
chromium.pdf({...})snippet and “PDF generation uses Chromium for high-fidelity rendering” claim have no counterpart insrc/ExportCenter. Removed.
Draft/roadmap — named report templates.
compliance-executive,compliance-detailed,audit-evidence,developer-sarif,custom-{tenant}are not a configurable template registry in code. Profiles select a format and (for compliance) a typed adapter/variant; there is no free-form template name field beyond the profile configuration.
5. Artifacts & manifest
Each run produces one or more artifacts (ExportArtifact, Core/Persistence/IExportRunRepository.cs) with a Kind (free-form string), a content-addressed Checksum, a ChecksumAlgorithm string that defaults to "SHA-256" (it is a plain string field, not an enum), size, content type, and metadata. List/fetch/download:
GET /v1/exports/runs/{runId}/artifacts # list
GET /v1/exports/runs/{runId}/artifacts/{artifactId} # metadata
GET /v1/exports/runs/{runId}/artifacts/{artifactId}/download # file stream
Download streams the file directly (FileStreamHttpResult) with the artifact’s content type and filename, and emits an audit event + download metric. The run manifest is available separately:
GET /v1/exports/runs/{runId}/verify/manifest # manifest content + digest
NOT IMPLEMENTED — fixed RustFS path layout & signed download URLs. The original
blobs/exports/{tenant}/{yyyy}/{mm}/{exp-id}/…RustFS tree and theGET …/download → { "download_url": "https://storage…?sig=…" }signed-URL response are not what the code does. Download is a direct authenticated file stream from the artifact store; there is no signed-URL indirection and no hard-coded RustFS directory contract insrc/ExportCenter.
6. Evidence locker integration (optional)
When configured, Export Center pushes a manifest snapshot to the Evidence Locker (IExportEvidenceLockerClient.PushSnapshotAsync), carrying the distribution transcript on the snapshot request itself (ExportEvidenceSnapshotRequest.Distribution). The locker returns a bundle id, a Merkle root hash, and an optional DSSE signature. Export Center does not read bundles back: the client is push-only, plus a TLPT evidence-pack delegation (AssembleTlptEvidencePackAsync).
// shape returned by PushSnapshotAsync (ExportEvidenceSnapshotResult)
{
"bundleId": "bnd-…",
"rootHash": "sha256:…",
"signature": { /* ExportDsseSignatureInfo: keyId, algorithm, signature, … */ } // optional
}
The original “Evidence Sealing” JSON (
bnd-789ghi,merkle_root, base64signature, fixedcontentsarray) was illustrative; the real contract is theIExportEvidenceLockerClientinterface above. Detailed evidence packaging is covered in Evidence Bundle Export Flow.
7. Verification
Export Center can verify a completed run’s manifest, signatures, and content hashes — including optional Rekor transparency-log checks — without a live data source:
POST /v1/exports/runs/{runId}/verify HTTP/1.1
Authorization: Bearer {jwt} # requires export.viewer
Content-Type: application/json
{
"verifyHashes": true,
"verifySignatures": true,
"verifyManifest": true,
"verifyEncryption": true,
"checkRekor": false,
"trustedKeys": []
}
Returns an ExportVerificationResponse with isValid, per-file hash results, manifest and signature results (algorithm, keyId, signer, optional Rekor index), errors, and warnings. A streaming variant is available at POST /v1/exports/runs/{runId}/verify/stream (SSE), and attestation status at GET /v1/exports/runs/{runId}/verify/attestation.
8. Distribution & delivery
Distribution is driven by ExportDistributionKind:
| Kind | Notes |
|---|---|
FileSystem | Local filesystem |
Mirror | Mirror-server distribution (full/delta) |
OfflineKit | Air-gap offline-kit distribution |
OciRegistry | OCI artifact push (feature-flagged; disable for air-gap) |
Webhook | Notification (metadata only) |
AmazonS3 / AzureBlob / GoogleCloudStorage | Object-store targets |
On-prem / air-gap posture. Per the module charter, OCI distribution must be disable-able for air-gap and tests must not reach the network. The cloud object-store kinds (
AmazonS3,AzureBlob,GoogleCloudStorage) are opt-in distribution targets a tenant may configure; they are not defaults and Export Center stores only references/transcripts, never managing those services itself. Offline/file-system/mirror/OCI-to-a-local-registry are the first-class paths.
Draft/roadmap — email delivery & per-recipient delivery config. Delivery by email attachment/link is not implemented as an Export Center distribution kind (
Webhookcarries metadata only; there is no
Export “Types” → profile kinds + input kinds
The original “Export Types” table (scan_report, sbom, vulnerability_report, policy_compliance, ai_code_guard_report, evidence_bundle, audit_log, …) does not map to a single type field on the request. In code:
- A profile has a
kind(AdHoc/Scheduled/EventDriven/Continuous) and a format (JsonRaw/JsonPolicy/Ndjson/Csv/Mirror; theTrivyDb/TrivyJavaDbmembers are retired — see the format table). - The content is expressed through
ExportScope.targetKindsandExportInputKind(Sbom,Vex,Attestation,ScanReport,PolicyResult,Evidence,RiskBundle,Advisory). - Compliance dossiers (CRA, NIS2 SoA/effectiveness, standards mapping) and audit/exception/lineage/risk bundles are produced by dedicated typed endpoints (see “Other typed export surfaces” below), not by a generic
typeenum.
ORPHANED —
ai_code_guard_reportexport type. No such export type/profile exists insrc/ExportCenter. Treat as NOT IMPLEMENTED.
Data Contracts
The original ExportRequest/ExportResponse TypeScript interfaces do not match the code. The actual contracts are (Api/ExportApiModels.cs):
Create profile (CreateExportProfileRequest)
interface CreateExportProfileRequest {
name: string; // required, unique per tenant
description?: string;
kind?: "AdHoc" | "Scheduled" | "EventDriven" | "Continuous"; // default AdHoc
scope?: ExportScope;
format?: ExportFormatOptions;
signing?: ExportSigningOptions;
schedule?: string; // cron
}
interface ExportFormatOptions {
format?: "JsonRaw" | "JsonPolicy" | "Ndjson" | "Csv" | "Mirror";
// "TrivyDb" | "TrivyJavaDb" still parse (historical
// plans) but are RETIRED and fail at adapter resolution
compression?: "None" | "Gzip" | "Zstd" | "Brotli";
includeMetadata?: boolean; // default true
prettyPrint?: boolean;
redactFields?: string[];
normalizeTimestamps?: boolean; // default true
sortKeys?: boolean; // default true
}
interface ExportSigningOptions {
enabled?: boolean;
algorithm?: string; // default "ES256"
keyId?: string;
providerHint?: string;
includeProvenance?: boolean; // default true
}
Start run (StartExportRunRequest)
interface StartExportRunRequest {
scopeOverride?: ExportScope;
formatOverride?: ExportFormatOptions;
correlationId?: string;
dryRun?: boolean;
}
Run response (ExportRunResponse)
interface ExportRunResponse {
runId: string;
profileId: string;
tenantId: string;
status: "Queued" | "Running" | "Completed" | "PartiallyCompleted" | "Failed" | "Cancelled";
trigger: "Manual" | "Scheduled" | "Event" | "Api";
correlationId?: string;
initiatedBy?: string;
progress: { totalItems: number; processedItems: number; failedItems: number; totalSizeBytes: number; percentComplete: number };
error?: { code: string; message: string; details?: Record<string,string> };
createdAt: string;
startedAt?: string;
completedAt?: string;
expiresAt?: string;
artifacts?: Array<{ artifactId: string; name: string; kind: string; sizeBytes: number; contentType?: string; checksum?: string; downloadUrl?: string }>;
}
Endpoint reference (/v1/exports/*)
Verified against Api/ExportApiEndpoints.cs. The in-service route prefix is /v1/exports(a gateway may front it under /api). The legacy GET/POST/DELETE /exports endpoints are deprecated and return 410 Gone(Program.cs).
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /v1/exports/profiles | viewer | List profiles (filter by status/kind/search; paginated) |
| GET | /v1/exports/profiles/{profileId} | viewer | Get profile |
| POST | /v1/exports/profiles | operator | Create profile (201 Created) |
| PUT | /v1/exports/profiles/{profileId} | operator | Update profile |
| DELETE | /v1/exports/profiles/{profileId} | admin | Archive (soft-delete) profile (204) |
| POST | /v1/exports/profiles/{profileId}/runs | operator | Start run (202 Accepted; 429 if over limits) |
| GET | /v1/exports/runs | viewer | List runs (filter by profile/status/trigger/date/correlation) |
| GET | /v1/exports/runs/{runId} | viewer | Get run (+artifacts) |
| POST | /v1/exports/runs/{runId}/cancel | operator | Cancel queued/running run |
| GET | /v1/exports/runs/{runId}/artifacts | viewer | List artifacts |
| GET | /v1/exports/runs/{runId}/artifacts/{artifactId} | viewer | Artifact metadata |
| GET | /v1/exports/runs/{runId}/artifacts/{artifactId}/download | viewer | Download (file stream) |
| GET | /v1/exports/runs/{runId}/events | viewer | SSE run events |
| POST | /v1/exports/runs/{runId}/verify | viewer | Verify run |
| GET | /v1/exports/runs/{runId}/verify/manifest | viewer | Get manifest |
| GET | /v1/exports/runs/{runId}/verify/attestation | viewer | Attestation status |
| POST | /v1/exports/runs/{runId}/verify/stream | viewer | Stream verification (SSE) |
Other typed export surfaces (same service, separate endpoint groups)
Registered in Program.cs alongside the core API; covered in depth by the module docs (docs/modules/export-center/*):
- Attestation & promotion attestation (
MapAttestationEndpoints,MapPromotionAttestationEndpoints) - Risk bundles (
MapRiskBundleEndpoints) - Simulation export (
MapSimulationExportEndpoints) - Audit bundle (
MapAuditBundleEndpoints) - Exception report (
MapExceptionReportEndpoints) - Lineage export (
MapLineageExportEndpoints) - Assurance / compliance exports — CRA, NIS2 SoA (
MapAssuranceExportEndpoints,MapNis2SoaExportEndpoints; e.g.GET /v1/exports/nis2/soa/profile,POST /v1/exports/nis2/soa/runs) - Incident management (
MapIncidentEndpoints)
SSE run events
StreamRunEvents (GET /v1/exports/runs/{runId}/events) emits event: <type>\ndata: <json>\n\n. The ExportRunSseEventTypes constant class declares: run.started, run.progress, run.phase.started, run.phase.completed, run.artifact.created, run.completed, run.failed, run.cancelled.
Actually emitted by the handler (it polls the run row every 2s): the framing events connected and disconnected, plus run.progress (on ProcessedItems change) and a status-change event mapped to run.started (→Running), run.completed (→Completed/PartiallyCompleted), run.failed (→Failed), or run.cancelled (→Cancelled).
ORPHANED —
run.phase.started,run.phase.completed,run.artifact.created. These three constants are declared but never emitted anywhere insrc/ExportCenter(only defined inExportApiModels.cs; not referenced byStreamRunEventsor any worker). They are unwired, awaiting the worker engine described in the “Worker reality” note. NOT IMPLEMENTED as emitted events today.
The original doc’s single
WebSocket: export readymessage is replaced by the typed SSE event stream above. Export Center uses Server-Sent Events, not WebSockets.
Scheduled Exports
Scheduling is a profile-level concern: a profile carries a single cron schedule string and a kind of Scheduled. Scheduled runs are triggered with ExportRunTrigger.Scheduled.
// the schedule lives on the profile, not in a separate "schedules:" config block
{
"name": "weekly-evidence-json",
"kind": "Scheduled",
"schedule": "0 8 * * MON",
"format": { "format": "JsonPolicy" },
"scope": { "targetKinds": ["sbom", "vex", "advisory"] }
}
Draft/roadmap — declarative
schedules:/delivery:YAML block. The original multi-schedule YAML with relativedate_range(-7d/now),delivery.method: email, and templated subjects is not the implemented model. A profile has one cron string; relative date-range tokens and email delivery are not implemented (see formats/delivery notes above).
Error Handling
| Condition | Behavior (verified) |
|---|---|
| Tenant claim missing | 400 BadRequest (tenant_not_found_in_claims) |
| Profile/run/artifact not found (or cross-tenant) | 404 NotFound |
Start run on non-Active profile | 400 BadRequest (profile_not_active) |
| Duplicate profile name | 409 Conflict (profile_name_conflict) |
| Update an archived profile | 400 BadRequest (profile_archived) |
| Cancel run in non-cancellable state | 400 BadRequest (run_cancel_invalid_state) |
| Over concurrency / queue limits | 429 Too Many Requests (audited) |
| Run failure | The ExportRun model supports Failed/PartiallyCompleted with an ErrorJson (surfaced as error.code/message/details). For the generic profile pipeline these transitions are not driven today — no deployed worker advances or fails a started run (see the “Worker reality” note). The status enum + error contract are in place for when the engine is wired. |
The original “Recovery” column (fall back to default template, simplify report and retry, retry with exponential backoff, queue-for-retry email) describes behavior not visible at the API layer and not implemented: there is no template registry, no PDF path, and (per the “Worker reality” note) no deployed run-executor that could perform worker-side retry/idempotency or retention pruning for the generic profile pipeline. Those remain roadmap.
Observability
Metrics
The meter is StellaOps.ExportCenter (ExportTelemetry.Meter, version 1.0.0); every instrument name is prefixed export_ (WebService/Telemetry/ExportTelemetry.cs). The four instruments the core profile/run/artifact/SSE API actually increments at runtime — with the exact tags the handlers pass — are:
| Instrument name | C# field | Type | Tags emitted by the API |
|---|---|---|---|
export_runs_total | ExportRunsTotal | Counter | tenant_id, profile_id |
export_runs_in_progress | ExportRunsInProgress | UpDownCounter | tenant_id |
export_artifact_downloads_total | ArtifactDownloadsTotal | Counter | tenant_id, artifact_kind |
export_sse_connections_total | SseConnectionsTotal | Counter | tenant_id |
ExportTelemetry also declares many more instruments for the typed surfaces and the (not-yet-wired) worker engine — e.g. counters export_runs_success_total, export_runs_failed_total, export_artifacts_total, export_bytes_total, export_concurrency_limit_exceeded_total, export_audit_events_total, the export_incidents_*/export_risk_bundle_jobs_*/export_timeline_events_* families, and histograms export_run_duration_seconds, export_plan_duration_seconds, export_bundle_size_bytes — see docs-archive/modules/export-center/architecture.md (Observability) for the full list.
Corrected. The original metric names (
export_requests_total,export_duration_seconds,export_size_bytes,export_failures_totalwithtype/format/reasonlabels) are NOT the actual instrument names — treat them as NOT IMPLEMENTED. A prior reconciliation note also claimed “the canonical tag set istenant/profile/adapter/result”; that is not what the code emits — the core API handlers tag withtenant_id/profile_id/artifact_kind(singular dimensions above). Theadapter/result/phasedimensions appear in structured log fields, not as tags on these counters.
Trace / phase context
Phases map to ExportPhaseKind (see §4): ScopeResolution → DataFetch → Transform → WriteOutput → GenerateManifest → Sign → Distribute → Verify → ApplyRetention → Finalize.
Related Flows
- SBOM Generation Flow - Source of SBOM records
- Policy Evaluation Flow - Source of policy snapshots/findings
- Evidence Bundle Export Flow - Detailed evidence packaging
- Dashboard Data Flow - Data aggregation patterns
Module references (source of truth)
- API:
src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiEndpoints.cs,ExportApiModels.cs - Domain:
…/StellaOps.ExportCenter.Core/Domain/{ExportProfile,ExportRun,ExportInput,ExportDistribution}.cs - Planner / formats / scope:
…/StellaOps.ExportCenter.Core/Planner/{ExportPlanModels,ExportScopeModels}.cs - Scopes:
src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs(ExportViewer/ExportOperator/ExportAdmin) - Module dossiers:
docs/modules/export-center/{architecture,profiles,provenance-and-signing,mirror-bundles}.md(trivy-adapter.mdis retained as a ⛔ FROZEN historical record only)
