Files
git.stella-ops.org/docs/modules/evidence-locker/architecture.md
master 563079fc69 feat(evidence-locker): Decision Capsule sealing pipeline
Builds the previously-aspirational Capsule create/seal/verify/export/replay
pipeline. Unblocks the former CAPSULE-001 task that lived (BLOCKED) in
SPRINT_20260408_005; carried over as CAPSULE-AUDIT-001 inside the new
SPRINT_20260422_002 (created + archived in same pass).

Pipeline:
- CapsuleManifest record: deterministic SBOM+feeds+reachability+policy+VEX
  content-address bundle.
- CapsuleManifestCanonicalizer: mirrors AUDIT-007 algorithm byte-for-byte
  (ordinal-sorted UTF-8 JSON via JsonDocument round-trip).
- ICapsuleSigner + EcdsaCapsuleSigner + NullCapsuleSigner: DSSE PAE
  contract, DSSE payload type application/vnd.stellaops.decision-capsule+json.
  Pattern-identical to IAuditBundleManifestSigner; defined locally rather
  than cross-referencing IExportAttestationSigner (which lives inside
  ExportCenter.WebService, not a shared library — future cleanup noted).
- CapsuleService: create / seal / verify / export (zip) / replay.
- PostgresCapsuleRepository (Dapper) with tenant RLS hookup.

Endpoints (all tenant-scoped, POST):
- POST /api/v1/evidence/capsules
- POST /api/v1/evidence/capsules/{id}/seal
- POST /api/v1/evidence/capsules/{id}/verify
- POST /api/v1/evidence/capsules/{id}/export (application/zip)
- POST /api/v1/evidence/capsules/{id}/replay

Storage: embedded migration 005_decision_capsules.sql creates
evidence_locker.decision_capsules (RLS-enforced) + indexes + CHECK
constraints. Auto-applied by existing EvidenceLockerMigrationRunner.

Audit (CAPSULE-AUDIT-001):
- 5 new AuditActions.Evidence constants (CreateCapsule/Seal/Verify/Export/Replay)
- Each endpoint chained with .Audited(AuditModules.Evidence, ...)
- contentHash surfaced on responses so AuditActionFilter propagates it
  into details_jsonb.

Tests: 9 focused tests (determinism x3, sign+verify+tamper x3, null-signer
graceful degradation, pipeline round-trip, 404 on missing). Full
EvidenceLocker namespace sweep: 141/141, 0 failures.

Docs: docs/modules/evidence-locker/architecture.md §9bis (manifest schema,
DSSE payload type, storage, API surface, relationship to
release.run_capsule_replay_linkage).

Runtime curl+Timeline assertion deferred — running container image
predates these changes; rebuild pending. Structural wiring identical to
runtime-verified VerdictEndpoints (AUDIT-002 precedent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:04:38 +03:00

14 KiB

component_architecture_evidence_locker.md - Stella Ops EvidenceLocker (2025Q4)

Sealed, immutable storage for vulnerability scan evidence and audit logs.

Scope. Implementation-ready architecture for EvidenceLocker: tamper-proof evidence chains for compliance and forensic analysis with content-addressable storage and cryptographic sealing.


0) Mission & boundaries

Mission. Provide immutable, sealed storage for vulnerability scan evidence, audit logs, and compliance artifacts. Ensure tamper-proof evidence chains with cryptographic verification for forensic analysis and regulatory compliance.

Boundaries.

  • EvidenceLocker stores evidence; it does not generate verdicts.
  • EvidenceLocker seals bundles; signing is delegated to Signer.
  • Evidence is immutable once sealed; no modifications or deletions.
  • Supports offline export for air-gapped compliance audits.

1) Solution & project layout

src/EvidenceLocker/StellaOps.EvidenceLocker/
 ├─ StellaOps.EvidenceLocker.Core/           # Sealing, verification, chain validation
 │   ├─ Services/
 │   │   ├─ ISealingService.cs               # Sealing interface
 │   │   ├─ SealingService.cs                # Cryptographic sealing
 │   │   ├─ IVerificationService.cs          # Verification interface
 │   │   └─ ChainValidator.cs                # Evidence chain validation
 │   └─ Models/
 │       ├─ EvidenceBundle.cs                # Bundle model
 │       ├─ EvidenceItem.cs                  # Individual evidence item
 │       └─ SealManifest.cs                  # Seal metadata
 │
 ├─ StellaOps.EvidenceLocker.Infrastructure/ # Storage adapters, bundle management
 │   ├─ Storage/
 │   │   ├─ IEvidenceStore.cs                # Storage interface
 │   │   ├─ FileSystemStore.cs               # Local filesystem
 │   │   └─ ObjectStore.cs                   # S3/RustFS storage
 │   └─ Persistence/
 │       └─ PostgresRepository.cs            # Metadata persistence
 │
 ├─ StellaOps.EvidenceLocker.WebService/     # HTTP API for submission/retrieval
 │   └─ Program.cs
 │
 ├─ StellaOps.EvidenceLocker.Worker/         # Background sealing and archival
 │   └─ SealingWorker.cs
 │
 └─ StellaOps.EvidenceLocker.Tests/          # Unit and integration tests

2) External dependencies

  • PostgreSQL - Metadata storage (schema: evidence_locker)
  • RustFS/S3 - Object storage for evidence bundles
  • Signer - Cryptographic sealing operations
  • Authority - Authentication and authorization
  • ExportCenter - Evidence bundle export

3) Contracts & data model

3.1 EvidenceBundle

{
  "bundleId": "eb-2025-01-15-abc123",
  "tenantId": "tenant-xyz",
  "createdAt": "2025-01-15T10:30:00.000000Z",
  "sealedAt": "2025-01-15T10:30:05.000000Z",
  "status": "sealed",
  "items": [
    {
      "itemId": "item-001",
      "type": "sbom",
      "format": "cyclonedx-json",
      "digest": "sha256:abc123...",
      "size": 45678,
      "casUri": "cas://evidence/items/abc123"
    },
    {
      "itemId": "item-002",
      "type": "scan-result",
      "format": "stellaops-findings-v1",
      "digest": "sha256:def456...",
      "size": 12345,
      "casUri": "cas://evidence/items/def456"
    }
  ],
  "seal": {
    "algorithm": "sha256",
    "rootHash": "sha256:fedcba...",
    "signature": "base64...",
    "keyId": "sha256:keyabc..."
  },
  "chain": {
    "previousBundleId": "eb-2025-01-14-xyz789",
    "previousRootHash": "sha256:prevhash...",
    "sequenceNumber": 42
  }
}

3.2 Evidence Item Types

Type Format Description
sbom cyclonedx-json, spdx-json Software Bill of Materials
scan-result stellaops-findings-v1 Vulnerability scan findings
policy-verdict stellaops-verdict-v1 Policy evaluation result
vex-statement openvex-v1 VEX statement
audit-log ndjson Audit trail events
attestation dsse-v1 DSSE attestation envelope

3.3 Seal Manifest

public sealed record SealManifest
{
    public required string BundleId { get; init; }
    public required string RootHash { get; init; }
    public required string Algorithm { get; init; }
    public required DateTimeOffset SealedAt { get; init; }
    public required string KeyId { get; init; }
    public required byte[] Signature { get; init; }
    public required IReadOnlyList<string> ItemDigests { get; init; }
}

4) REST API (EvidenceLocker.WebService)

All under /api/v1/evidence. Auth: OpTok.

POST /bundles                   { items: EvidenceItem[] } → { bundleId, status: "pending" }
POST /bundles/{id}/items        { item: EvidenceItem } → { itemId }
POST /bundles/{id}/seal         → { status: "sealed", seal: SealManifest }

GET  /bundles/{id}              → { bundle: EvidenceBundle }
GET  /bundles/{id}/items/{itemId}  → binary content
GET  /bundles/{id}/verify       → { valid: bool, details }

GET  /bundles?tenant={id}&from={date}&to={date}  → { bundles: BundleSummary[] }

POST /export                    { bundleIds: string[], format: "zip"|"tar" } → { exportId }
GET  /export/{id}               → binary archive
GET  /export/{id}/status        → { status, progress }
POST /evidence                  { producer_bundle, raw_bom_path?, vex_refs[]? } → { evidence_id, evidence_score, stored }
GET  /evidence/score?artifact_id={id} → { evidence_score, status }
GET  /healthz | /readyz | /metrics

5) Configuration (YAML)

EvidenceLocker:
  Postgres:
    ConnectionString: "Host=postgres;Database=evidence_locker;..."

  Storage:
    Provider: "rustfs"  # or "filesystem", "s3"
    RustFs:
      Endpoint: "http://rustfs:8080"
      Bucket: "stellaops-evidence"
    Filesystem:
      BasePath: "/data/evidence"

  Sealing:
    Policy: "immediate"  # or "batch"
    BatchSize: 100
    BatchIntervalSeconds: 60
    Algorithm: "sha256"

  Retention:
    DefaultDays: 2555  # 7 years
    ComplianceDays: 3650  # 10 years for regulated

  Export:
    MaxBundlesPerExport: 1000
    CompressionLevel: 6

  Authority:
    Issuer: "https://authority.stellaops.local"
    RequiredScopes: ["evidence:read", "evidence:write"]

6) Sealing Process

6.1 Sealing Flow

1. Bundle created (status: "pending")
   └─ Items added with content digests

2. Sealing triggered (immediate or batch)
   ├─ Compute Merkle root from item digests
   ├─ Include chain pointer (previous bundle hash)
   └─ Request signature from Signer

3. Bundle sealed (status: "sealed")
   └─ Immutable; no further modifications

6.2 Chain Integrity

Evidence chains are linked via Merkle roots:

Bundle N-1                    Bundle N                      Bundle N+1
┌─────────────┐              ┌─────────────┐               ┌─────────────┐
│ rootHash: H1│◄────────────│ prevHash: H1│◄─────────────│ prevHash: H2│
│ seq: 41     │              │ rootHash: H2│               │ rootHash: H3│
└─────────────┘              │ seq: 42     │               │ seq: 43     │
                             └─────────────┘               └─────────────┘

7) Security & compliance

  • Immutability: Sealed bundles cannot be modified
  • Tamper detection: Merkle tree verification for all items
  • Chain validation: Linked bundle verification
  • Encryption at rest: Optional bundle encryption
  • Access control: Tenant-scoped with Authority tokens
  • Audit trail: All access logged

8) Performance targets

  • Item ingestion: < 100ms P95 per item
  • Sealing: < 500ms P95 per bundle (up to 100 items)
  • Verification: < 200ms P95 per bundle
  • Export: < 5s P95 for 100 bundles

9) Observability

Metrics:

  • evidence.bundles.created_total{tenant}
  • evidence.bundles.sealed_total{tenant}
  • evidence.items.ingested_total{type}
  • evidence.verification.duration_seconds
  • evidence.storage.bytes_total

Tracing: Spans for ingestion, sealing, verification, export.


9bis) Decision Capsule pipeline

Sprint SPRINT_20260422_002 CAPSULE-PIPELINE-001 introduces a content-addressed Decision Capsule that binds a release decision's input set (SBOM, vuln feeds, reachability evidence, policy version, derived VEX) into a deterministic, DSSE-signed manifest. The capsule becomes the verifiable replay anchor for a release decision.

9bis.1) Manifest & canonicalisation

  • Type: StellaOps.EvidenceLocker.Capsules.CapsuleManifest (record).
  • apiVersion / kind: stella.ops/v1 / DecisionCapsuleManifest.
  • DSSE payload type: application/vnd.stellaops.decision-capsule+json (distinct from the audit-bundle payload type introduced by AUDIT-007).
  • Canonicalisation: CapsuleManifestCanonicalizer.Canonicalize(manifest) — UTF-8, non-indented, keys ordinal-sorted. Algorithm mirrors the audit-bundle canonicaliser (AUDIT-007) so verifiers can share a single canonical-JSON path.
  • Content address: SHA-256 of the canonicalised bytes, surfaced as contentHash on every API response and persisted alongside the manifest in evidence_locker.decision_capsules.

9bis.2) Storage

Table evidence_locker.decision_capsules (migration 005_decision_capsules.sql):

column type notes
capsule_id uuid (PK)
tenant_id uuid RLS via evidence_locker_app.require_current_tenant()
manifest_json text Canonical manifest bytes (stored as text for byte-exact replay)
content_hash text lowercase-hex SHA-256 of manifest_json
dsse_envelope text / NULL DSSE envelope JSON (omitted for unsealed or unsigned capsules)
signing_status text unsealed | signed | unsigned | invalid
assembly_inputs jsonb Opaque input references the capsule was assembled from
sealed_at timestamptz Non-null once sealed

Auto-migration applies on service startup via EvidenceLockerMigrationRunner (embedded resource; no manual SQL required).

9bis.3) Signing

The pipeline reuses the AUDIT-007 pattern: canonicalise the manifest, then delegate DSSE PAE signing to an abstraction (ICapsuleSigner). Default implementation is EcdsaCapsuleSigner (ECDSA P-256 / SHA-256) which produces an envelope shaped identically to the export / audit-bundle DSSE envelopes. When no signer is registered, NullCapsuleSigner returns an unsigned result with signing_status = "signer_not_registered" — same graceful degradation contract as AUDIT-007.

9bis.4) API surface

All endpoints live under /api/v1/evidence/capsules, are tenant-scoped, and emit audit events (module=evidence) via StellaOps.Audit.Emission:

Method Route Audit action Scope
POST /api/v1/evidence/capsules create_capsule EvidenceCreate
POST /api/v1/evidence/capsules/{id}/seal seal_capsule EvidenceCreate
POST /api/v1/evidence/capsules/{id}/verify verify_capsule EvidenceRead
POST /api/v1/evidence/capsules/{id}/export export_capsule EvidenceRead
POST /api/v1/evidence/capsules/{id}/replay replay_capsule EvidenceRead

Export returns a application/zip bundle with manifest.json, manifest.dsse.json (if sealed + signed), assembly-inputs.json, and metadata.json. Replay deserialises the sealed manifest, recomputes its content hash, and returns the reconstructed verdict.

9bis.5) Relationship to release.run_capsule_replay_linkage

The existing release.run_capsule_replay_linkage table (SPRINT_20260220_023 B23-RUN-06) continues to link a release run to a capsule by decision_capsule_id / capsule_hash. The Decision Capsule pipeline is the source of those fields: the content_hash this pipeline computes is what the linkage row's capsule_hash column references, and signature_status mirrors the capsule's signing_status.


10) Testing matrix

  • Sealing tests: Correct Merkle root computation
  • Chain tests: Linked bundle verification
  • Tamper tests: Detection of modified items
  • Export tests: Archive integrity verification
  • Retention tests: Policy enforcement

  • Bundle packaging: ./bundle-packaging.md
  • Attestation contract: ./attestation-contract.md
  • Evidence bundle spec: ./evidence-bundle-v1.md
  • Evidence pack schema: ./guides/evidence-pack-schema.md
  • Promotion gate evidence contract: ./promotion-evidence-contract.md
  • Audit bundle index schema: ./schemas/audit-bundle-index.schema.json
  • ExportCenter: ../export-center/architecture.md
  • Attestor: ../attestor/architecture.md