Findings Ledger — Runtime Instrumentation API Reference
Companion to
architecture.md(overview) andruntime-instrumentation.md(algorithms). Source of truth for the wire shape is the OpenAPI document atopenapi/findings-ledger.v1.yaml; this page is the human-readable per-endpoint reference.
All runtime endpoints share these conventions:
- Tenant scoping is implicit — the request tenant is resolved from the authenticated bearer token and the routing layer rejects calls with no tenant context.
- Bearer authentication is mandatory. Authority scopes:
- Read:
findings.runtime.read - Write:
findings.runtime.write
- Read:
- Empty-result reads return
404 NotFound(not200 OK + empty list) — see ADR-013 Findings Runtime Disabled Mode. - Timestamps are RFC 3339 / ISO 8601 with offset.
- All paths are rooted at
/api/v1/findings/{findingId}.
1. POST /{findingId}/runtime/traces — ingest a runtime trace
Required scope: findings.runtime.write
Accepts one runtime trace observation produced by an eBPF / syscall / APM probe. The service applies the privacy filter (strips @0x[hex]+ suffixes), persists the redacted trace, upserts the per-finding aggregate, and recomputes the runtime score in-line.
Request body
{
"capturedAt": "2026-04-28T12:34:56Z",
"artifactDigest": "sha256:abc123...",
"componentPurl": "pkg:npm/lodash@4.17.21",
"containerId": "container://prod/api-7f9c-2",
"containerName": "api",
"metadata": { "probe": "ebpf-uprobe", "host": "node-12" },
"frames": [
{
"symbol": "main",
"file": "/app/server.js",
"line": 42,
"isEntryPoint": true,
"isVulnerableFunction": false,
"confidence": 0.95
},
{
"symbol": "lodash.merge@0x7FFEDEADBEEF",
"isEntryPoint": false,
"isVulnerableFunction": true,
"confidence": 0.90
}
]
}
containerName and metadata are first-class durable fields on the trace row since sprint 026 (FL-RUNTIME-V2-001…003). Both round-trip on the aggregate read response, sourced from the most recent persisted trace for the (tenant, finding). containerId, artifact/component identity, and redacted frames remain the primary correlation inputs.
Required fields
| Field | Type | Notes |
|---|---|---|
capturedAt | RFC 3339 timestamp | When the probe captured the observation |
artifactDigest | string | OCI / build-artifact digest, non-empty |
componentPurl | string | SBOM component purl, non-empty |
frames | array of frame object | At least one frame; first frame with isVulnerableFunction=true is the bucket key |
Response — 202 Accepted
{
"findingId": "00000000-0000-0000-0000-000000000001",
"observationId": "0192f5c7-4b18-7c04-8fe7-5b5a3b5b2c00",
"recordedAt": "2026-04-28T12:34:56.789Z",
"artifactDigest": "sha256:abc123...",
"componentPurl": "pkg:npm/lodash@4.17.21",
"frameCount": 2,
"privacyFilterApplied": true
}
The Location response header points to the ingested trace (/api/v1/findings/{findingId}/runtime/traces/{observationId}). privacyFilterApplied is always true — the filter is unconditional.
Response — 400 BadRequest
problem+json shape per RFC 7807. Returned when the request body fails validation (missing/empty fields, no frames). Per-field errors are flattened into the errors map of an ASP.NET Core validation problem document:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"frames": ["frames are required"],
"artifactDigest": ["artifact digest is required"]
}
}
Response — 401 Unauthorized / 403 Forbidden
Standard ASP.NET problem document. 403 is returned when the caller authenticates but lacks the findings.runtime.write scope.
Sample curl
curl -sS -X POST \
"https://stella-ops.local/api/v1/findings/00000000-0000-0000-0000-000000000001/runtime/traces" \
-H "Authorization: Bearer $STELLA_TOKEN" \
-H "Content-Type: application/json" \
-d @sample-trace.json
Common error patterns
| Status | Client behavior |
|---|---|
| 400 | Repair the payload; do not retry the same request as-is |
| 401 | Refresh the bearer token and retry once |
| 403 | Surface to operator; missing scope is not auto-recoverable |
| 5xx | Retry with exponential backoff (the ingest path is idempotent on (tenant, finding, capturedAt, hash)) |
2. GET /{findingId}/runtime/traces — list aggregates
Required scope: findings.runtime.read
Returns the redacted per-function aggregates for a finding.
Query parameters
| Name | Default | Range | Description |
|---|---|---|---|
limit | 50 | 1…200 | Max aggregates returned |
sortBy | hits | hits, recent | Sort by hit count or recency |
Response — 200 OK
{
"findingId": "00000000-0000-0000-0000-000000000001",
"collectionActive": true,
"collectionStarted": "2026-04-25T08:00:00Z",
"summary": {
"totalHits": 1843,
"uniquePaths": 3,
"posture": "EbpfDeep",
"lastHit": "2026-04-28T12:34:56Z",
"directPathObserved": true,
"productionTraffic": false,
"containerCount": 4
},
"traces": [
{
"id": "sha256:abc123...|lodash.merge",
"vulnerableFunction": "lodash.merge",
"isDirectPath": true,
"hitCount": 1500,
"firstSeen": "2026-04-25T08:00:00Z",
"lastSeen": "2026-04-28T12:34:56Z",
"containerId": null,
"containerName": "api",
"metadata": { "probe": "ebpf-uprobe", "host": "node-12" },
"callPath": []
}
]
}
containerId is intentionally null on aggregate rows because the aggregate table stores containerCount rather than projecting individual container identities; containerId remains persisted on raw trace rows for retention-window evidence. containerName and metadata are sourced from the most recent persisted raw trace for the (tenant, finding) — sprint 026 (FL-RUNTIME-V2-003) promoted these fields from the v1 accepted-but-not-persisted caveat to first-class durable storage and added them to the aggregate response.
Response — 404 NotFound
Returned when there are no aggregates for (tenant, finding). Body intentionally empty — clients MUST treat 404 as “no data” rather than “server error”.
Sample curl
curl -sS \
"https://stella-ops.local/api/v1/findings/00000000-0000-0000-0000-000000000001/runtime/traces?limit=10&sortBy=recent" \
-H "Authorization: Bearer $STELLA_TOKEN"
Common error patterns
| Status | Client behavior |
|---|---|
| 401 | Refresh bearer token, retry once |
| 403 | Surface; missing findings.runtime.read scope |
| 404 | Render “no runtime evidence yet” UI; do not retry |
| 5xx | Retry with exponential backoff |
3. GET /{findingId}/runtime/score — current runtime score
Required scope: findings.runtime.read
Returns the runtime trustworthiness score (0…1, displayed as 0…100) plus the per-component breakdown.
Response — 200 OK
{
"findingId": "00000000-0000-0000-0000-000000000001",
"score": {
"score": 0.78,
"breakdown": {
"observationScore": 0.92,
"recencyFactor": 0.85,
"qualityFactor": 0.40
},
"computedAt": "2026-04-28T12:34:56Z"
}
}
score.score is a normalized [0, 1] wire value adapted from the persisted domain score (0..100). The wire-side breakdown collapses the four pre-weight components into three buckets to match the existing UI contract:
observationScore— log-scaled hits component (hitsweight)recencyFactor— linear-decayed recencyqualityFactor— coverage if available, else binary entry-point flag
For the full four-component math see ADR-016 Findings Runtime Score Formula.
Display vs. wire
The score.score field is normalized to [0, 1] on the wire so it composes with other 0…1 metrics (Evidence-Weighted Score, reachability confidence, advisory match score) without per-metric rescaling on the consumer side. Human-facing surfaces — CLI default human output, the Web UI gauge, and dashboards — multiply by 100 to display as a percentage. Concretely:
| Surface | Value rendered for score.score = 0.875 |
|---|---|
Wire / --output json | 0.875 |
stellaops findings runtime score get (human) | 87.50/100 (wire: 0.875) |
| Web UI score gauge | 87.5% |
| Sample SDK human print | 87.50/100 (wire: 0.875) |
JSON callers MUST treat the wire value as [0, 1]. CLI / SDK callers that parse human output should ignore the post-multiplication form; parse JSON instead. The wire convention is stable across the v1 contract; new metric fields added under breakdown follow the same [0, 1] shape (see ADR-016 Findings Runtime Score Formula §wire shape).
Response — 404 NotFound
No score has been derived yet for (tenant, finding). Empty body.
Sample curl
curl -sS \
"https://stella-ops.local/api/v1/findings/00000000-0000-0000-0000-000000000001/runtime/score" \
-H "Authorization: Bearer $STELLA_TOKEN"
Common error patterns
| Status | Client behavior |
|---|---|
| 401 | Refresh bearer, retry once |
| 403 | Surface; missing findings.runtime.read scope |
| 404 | Render “no score yet” UI; pair with a “no traces yet” hint |
| 5xx | Retry with backoff |
4. GET /{findingId}/runtime-timeline — chronological events
Required scope: findings.runtime.read
Note the path uses a dash (
runtime-timeline), not a slash. This differs from the/runtime/...endpoints above and is contract-tested.
Returns chronologically-ordered events for a finding within a [from, to] window.
Query parameters
| Name | Default | Range | Description |
|---|---|---|---|
from | to - 24h | RFC 3339 | Inclusive start |
to | now | RFC 3339 | Inclusive end (must be >= from) |
bucketHours | 1 | 1…168 | Time-bucket granularity (clamped to 7 days max) |
Response — 200 OK
{
"events": [
{
"timestamp": "2026-04-27T12:34:56Z",
"kind": "trace_ingested",
"correlationId": "0192f5c7-4b18-7c04-8fe7-5b5a3b5b2c00",
"payload": { "frames": 7, "containerId": "..." }
},
{
"timestamp": "2026-04-28T08:00:00Z",
"kind": "score_computed",
"correlationId": null,
"payload": { "score": 0.78 }
}
],
"from": "2026-04-27T12:34:56Z",
"to": "2026-04-28T12:34:56Z",
"bucketHours": 1
}
payload is an opaque JSON object whose shape varies by kind. Known event kinds: trace_ingested, score_computed, aggregate_updated.
Response — 400 BadRequest
Returned when from > to. Problem document with title=invalid_range.
Response — 404 NotFound
No events match the (tenant, finding, [from, to]) tuple.
Sample curl
curl -sS \
"https://stella-ops.local/api/v1/findings/00000000-0000-0000-0000-000000000001/runtime-timeline?from=2026-04-27T00:00:00Z&to=2026-04-28T23:59:59Z&bucketHours=4" \
-H "Authorization: Bearer $STELLA_TOKEN"
Common error patterns
| Status | Client behavior |
|---|---|
| 400 | Validate from <= to client-side and retry |
| 401 | Refresh bearer, retry once |
| 404 | Render “no timeline events” empty state |
| 5xx | Retry with backoff |
4.1 v1 → v2 history — containerName + metadata persistence
Sprint reference:
SPRINT_20260429_026tasksFL-RUNTIME-V2-001..004(lifted the v1 caveat originally codified inSPRINT_20260429_024taskFL-RUNTIME-HYG-003).
containerName and metadata were originally accepted on the wire but not persisted (v1) so that agents could emit a v2-ready payload before the server-side schema landed. Sprint 026 promoted both to first-class durable fields on findings.runtime_traces:
| Field | v2 behavior (current) |
|---|---|
containerName | Persisted to the trace row; returned on aggregate reads from the latest trace. |
metadata | Persisted to the trace row as JSONB; returned on aggregate reads from the latest trace. |
Concrete consequences for callers:
POST /runtime/traceswrites both fields to the trace row when the payload includes them; nothing changed on the wire shape.- The aggregate response (
GET /runtime/traces, §2) surfacescontainerNameandmetadataon every row, sourced from the most recent persisted raw trace for the (tenant, finding). - The persistence-level round-trip is verified by
RuntimeTraceRepository_RoundTrips_ContainerNameandRuntimeTraceRepository_RoundTrips_Metadata(and four sibling tests) insrc/Findings/__Tests/StellaOps.Findings.Ledger.Runtime.Persistence.Tests/. The HTTP-level contract is verified byRuntimeTracesEndpoints_Ingest_PersistsContainerNameAndMetadatainsrc/Findings/StellaOps.Findings.Ledger.Tests/.
Backwards compatibility: pre-existing v1 rows (no values written) are correctly NULL for both columns and round-trip cleanly on read. v1 agents that omit either field continue to work without changes.
4.2 Live (v1.0) — GET /api/v1/capabilities/runtime
Sprint reference:
SPRINT_20260429_025_FindingsLedger_runtime_capabilities_endpoint(docs/implplan/). Status: shipped — endpoint is reachable in production builds.
A read-only capabilities endpoint that lets UIs and CLIs distinguish three product states that otherwise look identical on the runtime read endpoints:
| State | Wire response on /runtime/* | Capabilities endpoint returns |
|---|---|---|
| Operator has not enabled runtime instrumentation. | 404 NotFound | { enabled: false, lastIngestAt: null, ingestedTraceCount: 0 } |
| Enabled, no traces ingested yet. | 404 NotFound | { enabled: true, lastIngestAt: null, ingestedTraceCount: 0 } |
| Enabled, traces present. | 200 OK + body | { enabled: true, lastIngestAt: "2026-..", ingestedTraceCount: N>0 } |
Request
GET /api/v1/capabilities/runtime
X-Tenant-Id: <tenant-uuid>
Authorization: Bearer <token>
Response — 200 OK
{
"enabled": true,
"lastIngestAt": "2026-04-28T08:30:42.317Z",
"ingestedTraceCount": 42
}
enabled— reflects the platform-widefindings:ledger:runtime:enabledconfig flag. There is no per-tenant toggle in v1.lastIngestAt— tenant-scopedMAX(captured_at)across the runtime trace table, ornullwhen the tenant has zero rows (or the feature is disabled).ingestedTraceCount— tenant-scopedCOUNT(*)over the runtime trace table. Zero when disabled or empty.
Response — 401 Unauthorized / 403 Forbidden
401— no bearer token presented (or invalid token).403— bearer is valid but lacks thefindings:readscope. Note: ingest-only callers (findings:writeonly) hit this branch.
Auth
Requires findings:read (same scope used by the runtime list / score read endpoints). The endpoint is tenant-scoped through the standard tenant accessor; data NEVER crosses tenants.
Sample curl
curl -sS -H "Authorization: Bearer ${TOKEN}" \
-H "X-Tenant-Id: ${TENANT}" \
https://stella-ops.local/api/v1/capabilities/runtime
5. Disabled-mode behavior (cross-cutting)
When findings:ledger:runtime:enabled = false (the default for new deployments):
| Endpoint | Behavior in disabled mode |
|---|---|
| Ingest POST | Returns 202 Accepted with frameCount: 0 — no-op acknowledge |
| List GET | Returns 404 NotFound |
| Score GET | Returns 404 NotFound |
| Timeline GET | Returns 404 NotFound |
Operators flip the toggle as documented in enabling-runtime-instrumentation.md. Background and rationale: ADR-013 Findings Runtime Disabled Mode.
Cross-references
- OpenAPI:
openapi/findings-ledger.v1.yaml - Architecture:
architecture.md - Algorithms:
runtime-instrumentation.md - CLI:
../../cli/findings-runtime.md - Sample SDK consumer:
docs/samples/findings-runtime-sdk/
