Runtime instrumentation — privacy filter, aggregation, score

Sprint reference: SPRINT_20260429_012_FindingsLedger_runtime_privacy_filter Library: src/Findings/__Libraries/StellaOps.Findings.Ledger.Runtime/ ADR: ADR-016 Findings Runtime Score FormulaOperations: runtime-instrumentation-operations.md— runbook, metrics, alerts, retention (sprint 014).

This page documents the three pure algorithms that sit between runtime trace ingestion and score persistence:

  1. Privacy filterRuntimeSymbolRedactor
  2. Trace aggregatorRuntimeTraceAggregator
  3. Score calculatorRuntimeScoreCalculator

All three are pure libraries: no Postgres, no I/O, no DI registration. They are unit-tested in isolation in src/Findings/__Tests/StellaOps.Findings.Ledger.Runtime.Tests/.


1. Privacy filter contract

What gets redacted

RuntimeSymbolRedactor strips a single trailing address token of the form @0x[hex]+ (optionally chained with +0x[hex]+ continuations) from runtime symbols and file paths. The pattern is anchored to the end of the string:

FieldMethodPattern (anchored)
SymbolRuntimeSymbolRedactor.RedactSymbol@0x[0-9A-Fa-f]+(\+0x[0-9A-Fa-f]+)*$
FileRuntimeSymbolRedactor.RedactFilePath(same as symbol)
FrameRuntimeSymbolRedactor.Redact(RuntimeFrame)applies both

Examples (input → output)

InputOutputNotes
SSL_write@0x7FFEDEADBEEFSSL_writebasic case
SSL_write@0x7FFFAAAABBBBSSL_writedifferent address, same redaction
foo@0xAA+0xBB+0xCCfoochained addresses stripped together
/usr/lib/libssl.so@0x7FFEDEAD/usr/lib/libssl.sofile path
Foo@SuppressWarningsFoo@SuppressWarningsannotation (no hex tail) → preserved
user@hostuser@hostno hex tail → preserved
foo_0xdeadbeeffoo_0xdeadbeefno leading @ → preserved
foo@0xDEAD-0xBEEFfoo@0xDEAD-0xBEEF- is not the documented separator
std::vector::push_back@0x12345678std::vector::push_backC++/Rust qualified names handled

What is NOT redacted

Properties


2. Trace aggregation algorithm

Grouping

RuntimeTraceAggregator.Aggregate(traces, tenantId, findingId) groups input traces by

key = (ArtifactDigest, RedactSymbol(firstVulnerableFrame.Symbol))

and emits one RuntimeTraceAggregate per group. Grouping by the redacted vulnerable function symbol is the operative correctness requirement for the integration test RuntimeTraces_PrivacyFilter_RedactsAddresses_AndAggregatesHotSymbols: two traces of SSL_write from different @0x... addresses must collapse into a single aggregate with HitCount >= 2.

Per-group emitted fields

FieldComputation
HitCountcount(traces in group)
FirstSeenmin(trace.CapturedAt)
LastSeenmax(trace.CapturedAt)
ContainerCountcount(distinct trace.ContainerId) (null/empty ids collapse to one)
HasEntryPointany(any frame in trace where IsEntryPoint == true)
VulnerableFunctionRedacted symbol (the bucket key)

Skip rules

Determinism guarantee

The result list is sorted by (ArtifactDigest, VulnerableFunction) using string.CompareOrdinal before being returned. This means the aggregator produces byte-for-byte identical output regardless of the order in which traces arrive in the input enumerable.

This is asserted by RuntimeTraceAggregatorTests.Aggregate_DeterministicAcross100Shuffles, which serializes aggregates into a canonical string and compares against 100 shuffled-input runs.


3. Score formula reference

Full ADR: ADR-016 Findings Runtime Score Formula.

score = clamp01(
      0.40 * normalizedHits(aggregates)        // log10-scaled at saturation 100
    + 0.30 * entryPointPresence(aggregates)    // binary 0/1
    + 0.20 * vulnerableFunctionCoverage(traces) // ratio of distinct vuln to all functions
    + 0.10 * recencyWeight(aggregates, window) // linear decay over window
) * 100

Key properties (asserted by unit tests):

See the ADR for the full rationale on weight choices, alternatives considered, and boundary semantics.


4. Privacy / compliance posture

The privacy filter exists so that per-process address tokens never reach persistence. Specifically:

The 1-byte loss of fidelity (we cannot tell SSL_write@0xAA from SSL_write@0xBB apart in persistence) is an explicit design trade-off: the address tokens are per-process-image artifacts and have no value for correlating findings across containers, deployments, or restarts. Stripping them is what makes “same vulnerable function from different addresses” → one bucket possible.


5. Algorithm composition

The three algorithms compose into the runtime instrumentation pipeline as follows:

ingest (raw traces)
    │
    ▼
RuntimeSymbolRedactor.Redact(frame)         ◀── per frame, before persistence
    │
    ▼
RuntimeTraceAggregator.Aggregate(traces, …) ◀── periodic / on-demand
    │
    ▼
RuntimeScoreCalculator.Calculate(aggregates, recentTraces, window, now)
    │
    ▼
persist `RuntimeScore` for `(tenant, finding)`

Each stage is a pure function with no I/O, so the entire pipeline is trivially testable end-to-end with in-memory fixtures. The persistence


6. Test references

Total: 68 / 68 tests green as of sprint completion.