openapi: 3.1.0 info: title: StellaOps Findings Ledger Time-Travel API version: 1.0.0 description: | API for querying the Findings Ledger at specific points in time, creating snapshots, and performing historical analysis. Unblocks Export Center chains (73+ tasks). This API enables: - Point-in-time queries for findings, VEX statements, advisories, and SBOMs - Snapshot creation and management for reproducible exports - Historical comparison (diff) between two points in time - Event replay for audit and debugging purposes - Cross-enclave evidence verification ## Blocker References - SPRINT_0160_export_evidence (15 tasks) - SPRINT_0161_evidence_locker (7 tasks) - SPRINT_0162_exportcenter_i (15 tasks) - SPRINT_0163_exportcenter_ii (22 tasks) - SPRINT_0164_exportcenter_iii (14 tasks) contact: name: StellaOps Platform Team url: https://stella-ops.org license: name: AGPL-3.0-or-later url: https://www.gnu.org/licenses/agpl-3.0.html servers: - url: https://api.stella-ops.org/v1 description: Production API - url: https://api.staging.stella-ops.org/v1 description: Staging API tags: - name: snapshots description: Ledger snapshot management - name: time-travel description: Point-in-time queries - name: replay description: Event replay operations - name: diff description: Historical comparison - name: evidence description: Evidence snapshot linking paths: /ledger/snapshots: get: operationId: listSnapshots summary: List available snapshots description: Returns a paginated list of ledger snapshots for the tenant tags: - snapshots parameters: - $ref: '#/components/parameters/TenantId' - $ref: '#/components/parameters/PageSize' - $ref: '#/components/parameters/PageToken' - name: status in: query schema: $ref: '#/components/schemas/SnapshotStatus' - name: created_after in: query schema: type: string format: date-time - name: created_before in: query schema: type: string format: date-time responses: '200': description: List of snapshots content: application/json: schema: $ref: '#/components/schemas/SnapshotListResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' post: operationId: createSnapshot summary: Create a new snapshot description: | Creates a point-in-time snapshot of the ledger state. Snapshots are immutable and can be used for reproducible exports and historical analysis. tags: - snapshots requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateSnapshotRequest' responses: '201': description: Snapshot created content: application/json: schema: $ref: '#/components/schemas/Snapshot' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '409': description: Snapshot with this label already exists content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /ledger/snapshots/{snapshot_id}: get: operationId: getSnapshot summary: Get snapshot details description: Returns details of a specific snapshot including its metadata and statistics tags: - snapshots parameters: - $ref: '#/components/parameters/TenantId' - $ref: '#/components/parameters/SnapshotId' responses: '200': description: Snapshot details content: application/json: schema: $ref: '#/components/schemas/Snapshot' '404': $ref: '#/components/responses/NotFound' delete: operationId: deleteSnapshot summary: Delete a snapshot description: | Deletes a snapshot. Only snapshots in 'available' or 'expired' status can be deleted. Active snapshots referenced by exports cannot be deleted. tags: - snapshots parameters: - $ref: '#/components/parameters/TenantId' - $ref: '#/components/parameters/SnapshotId' responses: '204': description: Snapshot deleted '404': $ref: '#/components/responses/NotFound' '409': description: Snapshot is in use and cannot be deleted content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /ledger/at/{timestamp}: get: operationId: queryAtTimestamp summary: Query ledger state at timestamp description: | Returns the ledger state as it existed at the specified timestamp. This enables historical queries without creating a persistent snapshot. tags: - time-travel parameters: - $ref: '#/components/parameters/TenantId' - name: timestamp in: path required: true description: ISO 8601 timestamp to query schema: type: string format: date-time - name: entity_type in: query required: true schema: $ref: '#/components/schemas/EntityType' - name: filters in: query style: deepObject explode: true schema: $ref: '#/components/schemas/TimeQueryFilters' - $ref: '#/components/parameters/PageSize' - $ref: '#/components/parameters/PageToken' responses: '200': description: Historical state content: application/json: schema: $ref: '#/components/schemas/HistoricalQueryResponse' '400': $ref: '#/components/responses/BadRequest' '404': description: No data available for the specified timestamp content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /ledger/at-sequence/{sequence}: get: operationId: queryAtSequence summary: Query ledger state at sequence number description: | Returns the ledger state as it existed at the specified sequence number. Provides deterministic point-in-time queries based on event sequence. tags: - time-travel parameters: - $ref: '#/components/parameters/TenantId' - name: sequence in: path required: true description: Ledger sequence number schema: type: integer format: int64 minimum: 0 - name: entity_type in: query required: true schema: $ref: '#/components/schemas/EntityType' - name: filters in: query style: deepObject explode: true schema: $ref: '#/components/schemas/TimeQueryFilters' - $ref: '#/components/parameters/PageSize' - $ref: '#/components/parameters/PageToken' responses: '200': description: Historical state at sequence content: application/json: schema: $ref: '#/components/schemas/HistoricalQueryResponse' '400': $ref: '#/components/responses/BadRequest' '404': description: Sequence number not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /ledger/replay: post: operationId: replayEvents summary: Replay ledger events description: | Replays ledger events from a starting point. Useful for rebuilding projections, debugging, and audit purposes. Returns events in deterministic order. tags: - replay requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ReplayRequest' responses: '200': description: Replay results content: application/json: schema: $ref: '#/components/schemas/ReplayResponse' application/x-ndjson: schema: $ref: '#/components/schemas/ReplayEvent' '400': $ref: '#/components/responses/BadRequest' /ledger/diff: post: operationId: computeDiff summary: Compare ledger states description: | Computes the difference between two points in time. Returns added, modified, and removed entities between the specified timestamps or sequence numbers. tags: - diff requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DiffRequest' responses: '200': description: Diff results content: application/json: schema: $ref: '#/components/schemas/DiffResponse' '400': $ref: '#/components/responses/BadRequest' /ledger/changes: get: operationId: getChanges summary: Get change log description: | Returns a stream of changes (events) between two points in time. Optimized for incremental synchronization and export. tags: - diff parameters: - $ref: '#/components/parameters/TenantId' - name: since_timestamp in: query schema: type: string format: date-time - name: until_timestamp in: query schema: type: string format: date-time - name: since_sequence in: query schema: type: integer format: int64 - name: until_sequence in: query schema: type: integer format: int64 - name: entity_types in: query style: form explode: false schema: type: array items: $ref: '#/components/schemas/EntityType' - $ref: '#/components/parameters/PageSize' - $ref: '#/components/parameters/PageToken' responses: '200': description: Change log content: application/json: schema: $ref: '#/components/schemas/ChangeLogResponse' application/x-ndjson: schema: $ref: '#/components/schemas/ChangeLogEntry' /ledger/evidence/link: post: operationId: linkEvidence summary: Link evidence to finding description: | Links a finding to an evidence snapshot in a portable bundle. Creates an immutable ledger entry for audit purposes. tags: - evidence requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/LinkEvidenceRequest' responses: '201': description: Evidence linked content: application/json: schema: $ref: '#/components/schemas/LinkEvidenceResponse' '400': $ref: '#/components/responses/BadRequest' '409': description: Evidence already linked (idempotent) content: application/json: schema: $ref: '#/components/schemas/LinkEvidenceResponse' /ledger/evidence/{finding_id}: get: operationId: getEvidenceSnapshots summary: Get evidence snapshots for finding description: Returns all evidence snapshots linked to a specific finding tags: - evidence parameters: - $ref: '#/components/parameters/TenantId' - name: finding_id in: path required: true schema: type: string responses: '200': description: Evidence snapshots content: application/json: schema: $ref: '#/components/schemas/EvidenceSnapshotsResponse' '404': $ref: '#/components/responses/NotFound' /ledger/evidence/verify: post: operationId: verifyEvidence summary: Verify cross-enclave evidence description: | Verifies that an evidence snapshot exists, is valid, and matches the expected DSSE digest for cross-enclave verification. tags: - evidence requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/VerifyEvidenceRequest' responses: '200': description: Verification result content: application/json: schema: $ref: '#/components/schemas/VerifyEvidenceResponse' '400': $ref: '#/components/responses/BadRequest' /ledger/export/historical: post: operationId: exportHistorical summary: Export historical findings description: | Exports findings as they existed at a specific point in time. Supports all standard export shapes (findings, vex, advisory, sbom). tags: - time-travel requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/HistoricalExportRequest' responses: '200': description: Historical export content: application/json: schema: $ref: '#/components/schemas/HistoricalExportResponse' application/x-ndjson: schema: oneOf: - $ref: '#/components/schemas/FindingExportItem' - $ref: '#/components/schemas/VexExportItem' - $ref: '#/components/schemas/AdvisoryExportItem' - $ref: '#/components/schemas/SbomExportItem' '400': $ref: '#/components/responses/BadRequest' /ledger/staleness: get: operationId: checkStaleness summary: Check ledger staleness description: | Checks if the ledger data is stale compared to the configured thresholds. Used for air-gap scenarios to determine if bundle refresh is needed. tags: - evidence parameters: - $ref: '#/components/parameters/TenantId' - name: entity_types in: query style: form explode: false schema: type: array items: $ref: '#/components/schemas/EntityType' responses: '200': description: Staleness check result content: application/json: schema: $ref: '#/components/schemas/StalenessResponse' components: parameters: TenantId: name: X-Tenant-Id in: header required: true description: Tenant identifier for multi-tenant isolation schema: type: string minLength: 1 maxLength: 64 PageSize: name: page_size in: query description: Number of items per page (default 500, max 5000) schema: type: integer minimum: 1 maximum: 5000 default: 500 PageToken: name: page_token in: query description: Pagination token from previous response schema: type: string SnapshotId: name: snapshot_id in: path required: true description: Snapshot unique identifier schema: type: string format: uuid schemas: EntityType: type: string enum: - finding - vex - advisory - sbom - evidence description: Type of ledger entity SnapshotStatus: type: string enum: - creating - available - exporting - expired - deleted description: Snapshot lifecycle status Snapshot: type: object required: - snapshot_id - tenant_id - status - created_at - sequence_number properties: snapshot_id: type: string format: uuid tenant_id: type: string label: type: string description: Human-readable label for the snapshot description: type: string status: $ref: '#/components/schemas/SnapshotStatus' created_at: type: string format: date-time expires_at: type: string format: date-time sequence_number: type: integer format: int64 description: Ledger sequence number at snapshot time timestamp: type: string format: date-time description: Point-in-time timestamp statistics: $ref: '#/components/schemas/SnapshotStatistics' merkle_root: type: string pattern: '^sha256:[a-f0-9]{64}$' description: Merkle tree root hash for integrity verification dsse_digest: type: string pattern: '^sha256:[a-f0-9]{64}$' description: DSSE envelope digest if snapshot is signed metadata: type: object additionalProperties: true SnapshotStatistics: type: object properties: findings_count: type: integer format: int64 vex_statements_count: type: integer format: int64 advisories_count: type: integer format: int64 sboms_count: type: integer format: int64 events_count: type: integer format: int64 size_bytes: type: integer format: int64 CreateSnapshotRequest: type: object required: - tenant_id properties: tenant_id: type: string label: type: string minLength: 1 maxLength: 128 description: Unique label for this snapshot within the tenant description: type: string maxLength: 1024 at_timestamp: type: string format: date-time description: Create snapshot at specific timestamp (default is now) at_sequence: type: integer format: int64 description: Create snapshot at specific sequence number expires_in: type: string format: duration example: P30D description: ISO 8601 duration after which snapshot expires include_entity_types: type: array items: $ref: '#/components/schemas/EntityType' description: Entity types to include (default is all) sign: type: boolean default: false description: Sign the snapshot with DSSE envelope metadata: type: object additionalProperties: true SnapshotListResponse: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/Snapshot' next_page_token: type: string total_count: type: integer format: int64 TimeQueryFilters: type: object properties: status: type: string severity_min: type: number severity_max: type: number policy_version: type: string artifact_id: type: string vuln_id: type: string labels: type: object additionalProperties: type: string HistoricalQueryResponse: type: object required: - query_point - entity_type - items properties: query_point: $ref: '#/components/schemas/QueryPoint' entity_type: $ref: '#/components/schemas/EntityType' items: type: array items: oneOf: - $ref: '#/components/schemas/FindingExportItem' - $ref: '#/components/schemas/VexExportItem' - $ref: '#/components/schemas/AdvisoryExportItem' - $ref: '#/components/schemas/SbomExportItem' next_page_token: type: string total_count: type: integer format: int64 QueryPoint: type: object required: - timestamp - sequence_number properties: timestamp: type: string format: date-time sequence_number: type: integer format: int64 snapshot_id: type: string format: uuid description: If query was against a snapshot ReplayRequest: type: object required: - tenant_id properties: tenant_id: type: string from_sequence: type: integer format: int64 description: Starting sequence number (default 0) to_sequence: type: integer format: int64 description: Ending sequence number (default latest) from_timestamp: type: string format: date-time to_timestamp: type: string format: date-time chain_ids: type: array items: type: string description: Filter by specific chain IDs event_types: type: array items: type: string description: Filter by event types include_payload: type: boolean default: true output_format: type: string enum: - json - ndjson default: json page_size: type: integer minimum: 1 maximum: 10000 default: 1000 ReplayResponse: type: object required: - events - replay_metadata properties: events: type: array items: $ref: '#/components/schemas/ReplayEvent' next_page_token: type: string replay_metadata: $ref: '#/components/schemas/ReplayMetadata' ReplayEvent: type: object required: - event_id - sequence_number - chain_id - event_type - recorded_at properties: event_id: type: string format: uuid sequence_number: type: integer format: int64 chain_id: type: string chain_sequence: type: integer event_type: type: string occurred_at: type: string format: date-time recorded_at: type: string format: date-time actor_id: type: string actor_type: type: string artifact_id: type: string finding_id: type: string policy_version: type: string event_hash: type: string pattern: '^sha256:[a-f0-9]{64}$' previous_hash: type: string pattern: '^sha256:[a-f0-9]{64}$' payload: type: object additionalProperties: true ReplayMetadata: type: object properties: from_sequence: type: integer format: int64 to_sequence: type: integer format: int64 events_count: type: integer format: int64 has_more: type: boolean replay_duration_ms: type: integer format: int64 DiffRequest: type: object required: - tenant_id - from - to properties: tenant_id: type: string from: $ref: '#/components/schemas/DiffPoint' to: $ref: '#/components/schemas/DiffPoint' entity_types: type: array items: $ref: '#/components/schemas/EntityType' include_unchanged: type: boolean default: false output_format: type: string enum: - summary - detailed - full default: summary DiffPoint: type: object properties: timestamp: type: string format: date-time sequence_number: type: integer format: int64 snapshot_id: type: string format: uuid DiffResponse: type: object required: - from_point - to_point - summary properties: from_point: $ref: '#/components/schemas/QueryPoint' to_point: $ref: '#/components/schemas/QueryPoint' summary: $ref: '#/components/schemas/DiffSummary' changes: type: array items: $ref: '#/components/schemas/DiffEntry' next_page_token: type: string DiffSummary: type: object properties: added: type: integer modified: type: integer removed: type: integer unchanged: type: integer by_entity_type: type: object additionalProperties: type: object properties: added: type: integer modified: type: integer removed: type: integer DiffEntry: type: object required: - entity_type - entity_id - change_type properties: entity_type: $ref: '#/components/schemas/EntityType' entity_id: type: string change_type: type: string enum: - added - modified - removed from_state: type: object additionalProperties: true to_state: type: object additionalProperties: true changed_fields: type: array items: type: string ChangeLogResponse: type: object required: - entries properties: entries: type: array items: $ref: '#/components/schemas/ChangeLogEntry' next_page_token: type: string from_sequence: type: integer format: int64 to_sequence: type: integer format: int64 ChangeLogEntry: type: object required: - sequence_number - timestamp - entity_type - entity_id - event_type properties: sequence_number: type: integer format: int64 timestamp: type: string format: date-time entity_type: $ref: '#/components/schemas/EntityType' entity_id: type: string event_type: type: string event_hash: type: string actor_id: type: string summary: type: string LinkEvidenceRequest: type: object required: - tenant_id - finding_id - bundle_uri - dsse_digest properties: tenant_id: type: string finding_id: type: string bundle_uri: type: string format: uri description: URI to the evidence bundle dsse_digest: type: string pattern: '^sha256:[a-f0-9]{64}$' description: SHA-256 digest of the DSSE envelope valid_for: type: string format: duration example: P90D description: ISO 8601 duration for validity period LinkEvidenceResponse: type: object required: - success properties: success: type: boolean event_id: type: string format: uuid description: Ledger event ID for the linkage error: type: string EvidenceSnapshotsResponse: type: object required: - finding_id - snapshots properties: finding_id: type: string snapshots: type: array items: $ref: '#/components/schemas/EvidenceSnapshot' EvidenceSnapshot: type: object required: - finding_id - bundle_uri - dsse_digest - created_at properties: finding_id: type: string bundle_uri: type: string format: uri dsse_digest: type: string pattern: '^sha256:[a-f0-9]{64}$' created_at: type: string format: date-time expires_at: type: string format: date-time ledger_event_id: type: string format: uuid VerifyEvidenceRequest: type: object required: - tenant_id - finding_id - expected_dsse_digest properties: tenant_id: type: string finding_id: type: string expected_dsse_digest: type: string pattern: '^sha256:[a-f0-9]{64}$' VerifyEvidenceResponse: type: object required: - verified properties: verified: type: boolean snapshot: $ref: '#/components/schemas/EvidenceSnapshot' error_code: type: string enum: - not_found - expired - digest_mismatch error_message: type: string HistoricalExportRequest: type: object required: - tenant_id - entity_type - shape properties: tenant_id: type: string entity_type: $ref: '#/components/schemas/EntityType' shape: type: string enum: - compact - standard - full description: Export shape controlling field inclusion at_timestamp: type: string format: date-time at_sequence: type: integer format: int64 snapshot_id: type: string format: uuid description: Export from a specific snapshot filters: $ref: '#/components/schemas/TimeQueryFilters' output_format: type: string enum: - json - ndjson default: json page_size: type: integer minimum: 1 maximum: 5000 default: 500 page_token: type: string filters_hash: type: string description: Hash of filters for pagination consistency HistoricalExportResponse: type: object required: - query_point - entity_type - items properties: query_point: $ref: '#/components/schemas/QueryPoint' entity_type: $ref: '#/components/schemas/EntityType' shape: type: string items: type: array items: oneOf: - $ref: '#/components/schemas/FindingExportItem' - $ref: '#/components/schemas/VexExportItem' - $ref: '#/components/schemas/AdvisoryExportItem' - $ref: '#/components/schemas/SbomExportItem' next_page_token: type: string filters_hash: type: string export_metadata: $ref: '#/components/schemas/ExportMetadata' ExportMetadata: type: object properties: total_count: type: integer format: int64 exported_count: type: integer export_duration_ms: type: integer format: int64 merkle_root: type: string pattern: '^sha256:[a-f0-9]{64}$' FindingExportItem: type: object required: - event_sequence - observed_at - finding_id - policy_version - status - cycle_hash properties: event_sequence: type: integer format: int64 observed_at: type: string format: date-time finding_id: type: string policy_version: type: string status: type: string severity: type: number cycle_hash: type: string evidence_bundle_ref: type: string provenance: $ref: '#/components/schemas/ExportProvenance' labels: type: object additionalProperties: true VexExportItem: type: object required: - event_sequence - observed_at - vex_statement_id - product_id - status - cycle_hash properties: event_sequence: type: integer format: int64 observed_at: type: string format: date-time vex_statement_id: type: string product_id: type: string status: type: string statement_type: type: string known_exploited: type: boolean cycle_hash: type: string provenance: $ref: '#/components/schemas/ExportProvenance' AdvisoryExportItem: type: object required: - event_sequence - published - advisory_id - source - title - cycle_hash properties: event_sequence: type: integer format: int64 published: type: string format: date-time advisory_id: type: string source: type: string title: type: string severity: type: string cvss_score: type: number cvss_vector: type: string kev: type: boolean cycle_hash: type: string provenance: $ref: '#/components/schemas/ExportProvenance' SbomExportItem: type: object required: - event_sequence - created_at - sbom_id - subject_digest - sbom_format - components_count - cycle_hash properties: event_sequence: type: integer format: int64 created_at: type: string format: date-time sbom_id: type: string subject_digest: type: string sbom_format: type: string components_count: type: integer has_vulnerabilities: type: boolean cycle_hash: type: string provenance: $ref: '#/components/schemas/ExportProvenance' ExportProvenance: type: object properties: policy_version: type: string cycle_hash: type: string ledger_event_hash: type: string StalenessResponse: type: object required: - is_stale - checked_at properties: is_stale: type: boolean checked_at: type: string format: date-time last_event_at: type: string format: date-time staleness_threshold: type: string format: duration staleness_duration: type: string format: duration by_entity_type: type: object additionalProperties: type: object properties: is_stale: type: boolean last_event_at: type: string format: date-time events_behind: type: integer format: int64 ErrorResponse: type: object required: - error_code - message properties: error_code: type: string message: type: string details: type: object additionalProperties: true request_id: type: string format: uuid responses: BadRequest: description: Invalid request parameters content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' Unauthorized: description: Authentication required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' Forbidden: description: Insufficient permissions content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' NotFound: description: Resource not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT apiKey: type: apiKey in: header name: X-API-Key security: - bearerAuth: [] - apiKey: []