Findings Ledger
The Findings Ledger is the immutable, append-only event store that records vulnerability findings, policy decisions, and workflow state changes across the Stella Ops platform. It gives operators and auditors a tamper-evident, replayable history of how every finding reached its current state.
Purpose
- Audit trail: Every finding state change (open, triage, suppress, resolve) is recorded with cryptographic hashes and actor metadata.
- Deterministic replay: Events can be replayed to reconstruct finding states at any point in time.
- Merkle anchoring: Event chains are Merkle-linked for tamper-evident verification.
- Tenant isolation: All events are partitioned by tenant with cross-tenant access forbidden.
Consolidated modules (Sprint 207)
The src/Findings/ directory is the unified home for all findings-related services:
- Findings Ledger (
StellaOps.Findings.Ledger,StellaOps.Findings.Ledger.WebService): Core append-only event ledger. - RiskEngine (
StellaOps.RiskEngine.Core,StellaOps.RiskEngine.WebService,StellaOps.RiskEngine.Worker): Computes risk scores using CVSS, EPSS, KEV, exploit maturity, fix-chain attestation, and VEX gates. Infrastructure lives under__Libraries/StellaOps.RiskEngine.Infrastructure. - VulnExplorer (merged into Findings Ledger WebService, SPRINT_20260408_002): VulnExplorer endpoints (
/v1/vulns,/v1/vex-decisions,/v1/evidence-subgraph,/v1/fix-verifications,/v1/audit-bundles) are now served byStellaOps.Findings.Ledger.WebService. Contracts live underContracts/VulnExplorer/, adapter services underServices/VulnExplorerAdapters.cs. The standaloneStellaOps.VulnExplorer.Apicontainer (stellaops-api) has been decommissioned. - Findings.Security (
StellaOps.Findings.Security.WebService): the Console exposure read model (/api/v2/security*); owns thefindings_securityschema. See security-read-model.md. - Findings.VulnCorrelation (
StellaOps.Findings.VulnCorrelation.WebService): the analytics-ingestion + vulnerability-correlation worker; owns theanalytics.*schema (relocated from Platform by PAC-5). See vuln-correlation.md.
Previously archived docs for RiskEngine and VulnExplorer are in docs-archive/modules/risk-engine/ and docs-archive/modules/vuln-explorer/.
Quick links
- Findings.VulnCorrelation — analytics ingestion,
analytics.*schema ownership - FL1–FL10 remediation tracker
- Implementation plan
- Schema catalog (events/projections/exports)
- Merkle & external-anchor policy
- Tenant isolation & redaction manifest
- Architecture — runtime instrumentation overview (architecture, endpoints, ADRs)
- Runtime-instrumentation algorithms (privacy filter, aggregation, score formula)
- Runtime-instrumentation API reference
- Enabling runtime instrumentation (1-page guide)
- Runtime-instrumentation ADRs:
Core API surface (selected)
The Findings Ledger WebService (StellaOps.Findings.Ledger.WebService) hosts the ledger write/read surface plus the merged VulnExplorer, scoring, and runtime endpoints. Routes and authorization are grounded in Program.cs and the Endpoints/ handlers.
| Route | Verb | Authorization policy | Scope grounding |
|---|---|---|---|
/vuln/ledger/events | POST | ledger.events.write | configured RequiredScopes (default vuln:operate) |
/api/v1/findings/ledger/advanced-assurance/append | POST | ledger.events.write | configured RequiredScopes (default vuln:operate) |
/api/v1/findings/ledger | GET | ledger.export.read | configured RequiredScopes (default vuln:operate) |
/api/v1/findings/ledger/{ledgerId:guid}/chain-verify | GET | ledger.export.read | configured RequiredScopes (default vuln:operate) |
/ledger/export/{findings,vex,advisories,sboms} | GET | ledger.export.read | configured RequiredScopes (default vuln:operate) |
/api/v1/findings/{findingId}/state | PATCH | (default policy) | configured RequiredScopes (default vuln:operate) |
/api/v1/findings/{findingId}/score, /scores | POST | scoring.write | configured RequiredScopes (default vuln:operate) |
/api/v1/findings/{findingId}/score | GET | scoring.read | configured RequiredScopes (default vuln:operate) |
/api/v2/security/vulnerabilities/{identifier} | GET | scoring.read | configured RequiredScopes (default vuln:operate) |
/api/v1/findings/{findingId:guid}/runtime/traces | POST | findings.runtime.write | findings:write (StellaOpsScopes.FindingsWrite) |
| runtime list/score/timeline reads | GET | findings.runtime.read | findings:read (StellaOpsScopes.FindingsRead) |
/v1/vulns, /v1/vulns/{id}, /v1/vex-decisions (read), /v1/evidence-subgraph/{vulnId} | GET | VulnExplorer.View | vuln:view (StellaOpsScopes.VulnView) |
/v1/vex-decisions (POST/PATCH), /v1/fix-verifications (POST/PATCH) | POST/PATCH | VulnExplorer.Operate | vuln:operate (StellaOpsScopes.VulnOperate) |
/v1/audit-bundles | POST | VulnExplorer.Audit | vuln:audit (StellaOpsScopes.VulnAudit) |
The
ledger.*andscoring.*policies are bound to whateverfindings:ledger:Authority:RequiredScopesresolves to (they do not pin a fixed scope constant); when that list is empty the service falls back tovuln:operate(StellaOpsScopes.VulnOperate). Only the runtime (findings.runtime.*) and VulnExplorer (VulnExplorer.*) policies pin specific scope constants. Tenant resolution usesX-StellaOps-TenantId/X-StellaOps-Tenantheaders or thestellaops:tenantclaim viaStellaOpsTenantResolver.vuln:readis deprecated in favour ofvuln:view.
Persistence (schema findings, owned tables in migrations/001_initial.sql and later): ledger_events (append-only, LIST-partitioned by tenant_id, hash-chained via event_hash/previous_hash/merkle_leaf_hash), ledger_merkle_roots, findings_projection, finding_history, and triage_actions. The ledger event-type enum (finding.created, finding.status_changed, …, finding.closed, plus airgap.*, orchestrator.export_recorded, attestation.pointer_linked, and the nis2.*/cra.* incident families) is defined in Domain/LedgerEventConstants.cs. The write path returns a LedgerWriteStatus of Success (201), Idempotent (200), ValidationFailed (400), or Conflict (409).
Compatibility read-model surfaces
GET /api/v2/security/vulnerabilities/{identifier}is the authoritative shipped vulnerability-detail route for the Web console.- The route is backed by Findings Ledger projections plus optional scoring state. Unknown fields remain null or absent instead of being fabricated in the API or the web client.
signedScoreis emitted only when cached or historical scoring state exists for the resolved finding.proofSubjectIdis surfaced only when the projection carries replay/proof identity, allowing the Web console to enable verification only when a real proof subject exists.
Runtime cutover status
RiskEngine.WebServiceis PostgreSQL-backed in every non-testing environment. The previous live in-memory score-result path is no longer part of the production host composition root.RiskEngine.WebServiceno longer registers null CVSS/KEV/EPSS data sources in non-testing hosts. Source-backed CVSS/KEV/EPSS and exploit-maturity calls fail closed with truthful unsupported/configuration errors unless a real EPSS bundle/snapshot is configured or callers provide explicit inline scoring signals.Findings.Ledger.WebServicekeeps compatibility-only scoring state, webhook registration state, runtime traces/timeline state, evidence-content lookup, reachability mini-map lookup, backport evidence lookup, and merged VulnExplorer write state isolated to explicitTestingharness wiring. In non-testing environments those retired or unimplemented surfaces return truthful501 problem+jsonresponses instead of fabricating success or returning silent empty data.- Rekor transparency-log verification is disabled by default. Evidence graph signature checks resolve an unavailable/unverified state until operators explicitly set
findings:ledger:rekor:enabled=trueandfindings:ledger:rekor:baseUrlto a local mirror or approved external Rekor endpoint. - The merged VulnExplorer VEX override attestor uses
TestVexOverrideAttestorAdapteronly under the explicit testing harness. Non-testing hosts resolveUnsupportedVexOverrideAttestorAdapter, preserving the fail-closed runtime boundary. - Projection-backed read surfaces remain live and truthful:
GET /v1/vulns,GET /v1/vulns/{id},GET /v1/evidence-subgraph/{vulnId}, andGET /api/v2/security/vulnerabilities/{identifier}still resolve from persisted Findings projections. LedgerDataSourcenow applies UTC session defaults andsearch_path=findings,publicon every PostgreSQL connection so raw-SQL repositories resolve canonical Findings tables consistently after restart.- Graph Asset Registry v1 lifecycle events are visible in Findings through the
findings.asset_registry_eventsprojection. The Findings worker readsgraph.asset_registry_eventsread-only, preserves Graph payload/event/Merkle hashes unchanged, and checkpoints Graph append order for deterministic catch-up.
Implementation Status
Historical roadmap (largely delivered). The four delivery phases below were the original build-out plan. The code now reflects substantial completion of all four; the phase list is retained for traceability, with the current grounding noted inline. For the live, source-accurate surface, see
architecture.mdand the Runtime cutover status section above.
Delivery Phases
- Phase 1 – Observability baselines: Instrument writer/projector with metrics, structured logs, OTLP exporters, Grafana dashboards + alert rules. Delivered:
StellaOps.Findings.Ledger.WebService/Program.csregistersAddStellaOpsTelemetrywith meterStellaOps.Findings.Ledger(plus theRuntimeInstrumentationMetricsmeter) and theRuntimeInstrumentationActivitytrace source; activity sourceStellaOps.Findings.Ledgeris declared inObservability/LedgerTelemetry.cs. Seeobservability.md. - Phase 2 – Determinism harness: Finalize NDJSON fixtures, implement replay harness CLI, add CI pipeline jobs. Delivered:
tools/LedgerReplayHarnessplus__Tests/StellaOps.Findings.Ledger.ReplayHarness.TestsandStellaOps.Findings.Tools.LedgerReplayHarness.Tests; the replay/diff HTTP surface (POST /v1/ledger/replay,POST /v1/ledger/diff) is live inProgram.cs. Seereplay-harness.md. - Phase 3 – Deployment & backup collateral: Integrate ledger service into Compose/Helm, automate PostgreSQL migrations, document backup cadence. Delivered: startup auto-migration is wired via
AddStartupMigrations<LedgerServiceOptions>(schemaName: "findings", moduleName: "FindingsLedger", …)inProgram.cs; 21 embedded SQL migrations (001_initial.sql…021_vex_override_envelopes.sql) live underStellaOps.Findings.Ledger/migrations/. Seedeployment.md. - Phase 4 – Provenance & air-gap extensions: Ingest orchestrator run export metadata, extend ledger events for bundle provenance, store attestation pointers. Delivered:
POST /internal/ledger/orchestrator-export,POST /internal/ledger/airgap-import, and the/v1/ledger/attestation-pointers*routes inProgram.cs; backing event types (orchestrator.export_recorded,airgap.bundle_imported,attestation.pointer_linked) inDomain/LedgerEventConstants.cs. Seeairgap-provenance.md.
Key Dependencies (historical)
- AdvisoryAI Sprint 110.A completion (raw findings parity)
- Observability schema approval to unblock Phase 1 instrumentation
- QA lab capacity for the multi-million-finding replay checkpoint
- DevOps review of Compose/Helm overlays
- Orchestrator export schema freeze for provenance linkage
Acceptance Criteria (historical)
- Metrics/logging/tracing implementation merged with dashboards exported
- Harness CLI + fixtures + signed reports committed
- Compose/Helm overlays + backup/restore runbooks validated
- Air-gap provenance fields documented + implemented
- Sprint tracker and release notes updated after each phase
