Here’s a simple, practical way to make vulnerability “reachability” auditable and offline‑verifiable in Stella Ops without adding a lot of UI or runtime cost. ![diagram of call graph to subgraph proof flow](https://dummyimage.com/1200x400/ededed/333\&text=Call+graph+%E2%86%92+Resolved+subgraph+%E2%86%92+Proof+of+Exposure) # What this is (plain English) * **Call‑stack subgraph:** when we say a vuln is “reachable,” we really mean *some* functions in your code can eventually call the risky function. That tiny slice of the big call graph is the **subgraph**. * **Proof of exposure (PoE):** a compact bundle (think: a few kilobytes) that cryptographically proves *which* functions and edges make the vuln reachable in a specific build. * **Offline‑verifiable:** auditors can check the proof later, in an air‑gapped setting, using only hashes and your reproducible build IDs. # The minimal data model * **BuildID:** deterministic identifier (e.g., ELF Build‑ID or source‑of‑truth content hash). * **Nodes:** function identifiers `(module, symbol, debug‑addr, source:line?)`. * **Edges:** caller → callee (with optional guard predicates like feature flags). * **Entry set:** the function(s)/handlers reachable from runtime entrypoints (HTTP handlers, cron, CLI). * **Sink set:** vulnerable API(s)/function(s) tied to a CVE. * **Reachability proof:** `{BuildID, nodes[N], edges[E], entryRefs, sinkRefs, policyContext, toolVersions}` + DSSE signature. # How it fits the Stella Ops ledger * Store each **resolved call‑stack** as a **subgraph object** keyed by `(BuildID, vulnID, package@version)`. * Link it to: * SBOM component node (CycloneDX/SPDX ref). * VEX claim (affected/not‑affected/under‑investigation). * Scan recipe (so anyone can replay the result). * Emit one **PoE artifact** per “(vuln, component) with reachability=true”. # Why this helps * **Binary precision + explainability:** even if you only have a container image, the PoE explains *why* it’s reachable. * **Auditor‑friendly:** tiny artifact, DSSE‑signed, replayable with a known scanner build. * **Noise control:** store reachability as first‑class evidence; triage focuses on subgraphs, not global graphs. # Implementation guide (short and concrete) **1) Extraction (per build)** * Prefer source‑level graphs when available; otherwise: * ELF/PE/Mach‑O symbol harvest + debug info (DWARF/PDB) if present. * Lightweight static call‑edge inference (import tables, PLT/GOT, relocation targets). * Optional dynamic trace sampling (eBPF hooks) to confirm hot edges. **2) Resolution pipeline** * Normalize function IDs: `ModuleHash:Symbol@Addr[:File:Line]`. * Compute **entry set** (framework adapters know HTTP/GRPC/CLI entrypoints). * Compute **sink set** via rulepack mapping CVEs → {module:function(s)}. * Run bounded graph search with **policy guards** (feature flags, platform, build tags). * Persist the **subgraph** + metadata. **3) PoE artifact (OCI‑attached attestation)** * Canonical JSON (stable sort, normalized IDs). * Include: BuildID, tool versions, policy digest, SBOM refs, VEX claim link, subgraph nodes/edges, minimal repro steps. * Sign via DSSE; attach as OCI ref to the image digest. **4) Offline verification (auditor)** * Inputs: PoE, image digest, SBOM slice. * Steps: verify DSSE → check BuildID ↔ image digest → confirm nodes/edges hashes → re‑evaluate policy (optional) → show minimal path(s) entry→sink. # UI: keep it small * **Evidence tab → “Proof of exposure”** pill on any reachable vuln row. * Click opens a tiny **path viewer** (entry→…→sink) with: * path count, shortest path, guarded edges (badges for feature flags). * “Copy PoE JSON” and “Verify offline” instructions. * No separate heavy UI needed; reuse the existing vulnerability details drawer. # C# shape (sketch) ```csharp record FunctionId(string ModuleHash, string Symbol, ulong Addr, string? File, int? Line); record Edge(FunctionId Caller, FunctionId Callee, string[] Guards); record Subgraph(string BuildId, string ComponentRef, string VulnId, IReadOnlyList Nodes, IReadOnlyList Edges, string[] EntryRefs, string[] SinkRefs, string PolicyDigest, string ToolchainDigest); interface IReachabilityResolver { Subgraph Resolve(string buildId, string componentRef, string vulnId, ResolverOptions opts); } interface IProofEmitter { byte[] EmitPoE(Subgraph g, PoeMeta meta); // canonical JSON bytes } ``` # Policy hooks you’ll want from day one * `fail_if_unknown_edges > N` in prod. * `require_guard_evidence` for claims like “feature off”. * `max_paths`/`max_depth` to keep proofs compact. * `source-first-but-fallback-binary` selection. # Rollout plan (2 sprints) * **Sprint A (MVP):** static graph, per‑component sinks, shortest path only, PoE JSON + DSSE sign, attach to image, verify‑cli. * **Sprint B (Hardening):** guard predicates, multiple paths with cap, eBPF confirmation toggle, UI path viewer, policy gates wired to release checks.