ReachGraph Module Architecture
Audience: ReachGraph service owners and consumers (Policy Engine, Console, CLI, Export Center) that query “why reachable?” evidence. Source of truth:
src/ReachGraph/andsrc/__Libraries/StellaOps.ReachGraph*/StellaOps.Reachability.Core. Where a doc claim and the code disagree, the code wins.
Overview
The ReachGraph module provides a unified store for reachability subgraphs, giving Stella Ops fast, deterministic, audit-ready answers to “exactly why a dependency is reachable.” It consolidates data from Scanner, Signals, and Attestor into content-addressed artifacts with edge-level explainability.
Problem Statement
Before ReachGraph, reachability data was scattered across multiple modules:
| Module | Data | Limitation |
|---|---|---|
| Scanner.CallGraph | CallGraphSnapshot | No unified query API |
| Signals | ReachabilityFactDocument | Runtime-focused, not auditable |
| Attestor | PoE JSON | Per-CVE only, no slice queries |
| Graph | Generic nodes/edges | Not optimized for “why reachable?” |
Result: Answering “why is lodash reachable?” required querying multiple systems with no guarantee of consistency or auditability.
Solution
ReachGraph provides:
- Unified Schema: Extends PoE subgraph format with edge explainability
- Content-Addressed Store: All artifacts identified by BLAKE3 digest
- Slice Query API: Fast queries by package, CVE, entrypoint, or file
- Deterministic Replay: Verify that same inputs produce same graph
- DSSE Signing: Offline-verifiable proofs
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Consumers │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Policy │ │ Web │ │ CLI │ │ Export │ │
│ │ Engine │ │ Console │ │ │ │ Center │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└───────┼─────────────┼─────────────┼─────────────┼───────────────┘
│ │ │ │
└─────────────┴──────┬──────┴─────────────┘
│
┌────────────────────────────▼────────────────────────────────────┐
│ ReachGraph WebService │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ REST API (MVC controllers) │ │
│ │ ReachGraphController /v1/reachgraphs/* (store) │ │
│ │ ReachabilityController /v1/reachability/* (unified) │ │
│ │ CveMappingController /v1/cve-mappings/* (CVE↔symbol) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Slice Query Engine │ │
│ │ - Package slice (by PURL) │ │
│ │ - CVE slice (paths to vulnerable sinks) │ │
│ │ - Entrypoint slice (reachable from entry) │ │
│ │ - File slice (changed file impact) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Replay Service (ReachGraphReplayService) │ │
│ │ - Verify input digests vs stored provenance │ │
│ │ - Recompute stored graph digest (see note: not yet a │ │
│ │ full rebuild-from-inputs) and compare │ │
│ │ - Record every attempt in reachgraph.replay_log │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────────▼───────────────────────────────────┐
│ ReachGraph Core Library │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ Schema │ │ Serialization │ │ Signing │ │
│ │ │ │ │ │ │ │
│ │ ReachGraphMin │ │ Canonical JSON │ │ DSSE Wrapper │ │
│ │ EdgeExplanation│ │ BLAKE3 Digest │ │ Attestor Int. │ │
│ │ Provenance │ │ Compression │ │ │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
└─────────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────────▼───────────────────────────────────┐
│ Persistence Layer │
│ ┌────────────────────────┐ ┌────────────────────────┐ │
│ │ PostgreSQL │ │ Valkey │ │
│ │ schema: reachgraph │ │ Full graph (24h TTL) │ │
│ │ reachgraph.subgraphs │ │ Hot slice cache │ │
│ │ reachgraph.slice_cache│ │ (30min TTL) │ │
│ │ reachgraph.replay_log │ │ tenant-scoped keys │ │
│ └────────────────────────┘ └────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
▲
│
┌─────────────────────────────┴───────────────────────────────────┐
│ Producers │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Scanner │ │ Signals │ │ Attestor │ │
│ │ CallGraph │ │ RuntimeFacts │ │ PoE │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Data Model
ReachGraphMinimal (v1)
The core schema extends the PoE predicate format:
{
"schemaVersion": "reachgraph.min@v1",
"artifact": {
"name": "svc.payments",
"digest": "sha256:abc123...",
"env": ["linux/amd64"]
},
"scope": {
"entrypoints": ["/app/bin/svc"],
"selectors": ["prod"],
"cves": ["CVE-2024-1234"]
},
"nodes": [...],
"edges": [...],
"provenance": {...},
"signatures": [...]
}
The graph’s own content-addressed identity (digest) is a BLAKE3-256 hash in the form blake3:{64-hex} (see Content Addressing). The artifact.digest field above is the subject image/artifact digest and uses the sha256:/sha512: convention (validated by ReachGraphStoreService.ValidateGraph). Do not confuse the two: the row digest primary key is blake3:…, the artifact_digest column is sha256:…/sha512:….
Source: src/__Libraries/StellaOps.ReachGraph/Schema/ReachGraphMinimal.cs (ReachGraphMinimal, ReachGraphArtifact, ReachGraphScope, ReachGraphSignature), ReachGraphProvenance.cs (ReachGraphProvenance/ReachGraphInputs/ReachGraphAnalyzer).
Edge Explainability
Every edge (ReachGraphEdge: from, to, why) carries an EdgeExplanation (type, optional loc, optional guard, required confidence in [0.0, 1.0], optional metadata) explaining why it exists. The EdgeExplanationType enum (serialized as a JSON string) has eleven members:
| Type | Description | Example Guard |
|---|---|---|
Import | Static import (ES6 import, Python import, using) | - |
DynamicLoad | Runtime load (require(), dlopen, LoadLibrary) | - |
Reflection | Reflective call (Class.forName, Type.GetType) | - |
Ffi | Foreign function interface (JNI, P/Invoke, ctypes) | - |
EnvGuard | Env variable check (process.env.X, os.environ.get) | DEBUG=true |
FeatureFlag | Feature flag (LaunchDarkly, unleash, custom) | FEATURE_X=enabled |
PlatformArch | Platform/arch guard (process.platform, runtime.GOOS) | os=linux |
TaintGate | Taint gate (sanitization, validation) | - |
LoaderRule | Loader rule (PLT/IAT/GOT entry) | RTLD_LAZY |
DirectCall | Direct call (static, virtual, delegate) | - |
Unknown | Cannot determine explanation type | - |
Source: src/__Libraries/StellaOps.ReachGraph/Schema/EdgeExplanation.cs, ReachGraphEdge.cs, ReachGraphNode.cs (node kinds: Package, File, Function, Symbol, Class, Module).
Content Addressing
All graphs are identified by a BLAKE3-256 digest, emitted in the form blake3:{64-hex-lowercase} (ReachGraphDigestComputer.ComputeDigest):
- Computed from canonical JSON (sorted keys, no nulls) via
CanonicalReachGraphSerializer - Signatures excluded from hash computation (the graph is hashed with
Signatures = null) - Enables idempotent upserts and cache keying
- The
reachgraph.subgraphs.digestcolumn enforces the format with aCHECK (digest ~ '^blake3:[a-f0-9]{64}$')constraint
Source: src/__Libraries/StellaOps.ReachGraph/Hashing/ReachGraphDigestComputer.cs (uses the Blake3 package), Serialization/CanonicalReachGraphSerializer*.cs.
Persistence Schema
The durable store is the reachgraph PostgreSQL schema, created and migrated by the embedded SQL migration 001_reachgraph_store.sql in StellaOps.ReachGraph.Persistence. Migrations run on startup via AddStartupMigrations (module name ReachGraph.Persistence, schema reachgraph); they are skipped in the Testing environment and can be disabled with ReachGraph:StartupMigrations:Enabled=false.
| Table | Purpose | Key columns |
|---|---|---|
reachgraph.subgraphs | Content-addressed graph blobs | digest (PK, blake3:…), artifact_digest (sha256:/sha512:), tenant_id, scope JSONB, node_count, edge_count, blob BYTEA (gzip), blob_size_bytes, provenance JSONB, created_at |
reachgraph.slice_cache | Precomputed slices (DB-side) | cache_key (PK), subgraph_digest (FK → subgraphs, ON DELETE CASCADE), slice_blob, query_type (package/cve/entrypoint/file), query_params JSONB, expires_at, hit_count |
reachgraph.replay_log | Audit log of replay attempts | id (UUID PK), subgraph_digest, input_digests JSONB, computed_digest, matches, divergence JSONB, tenant_id, duration_ms, computed_at |
Indexes back the common access paths: composite (tenant_id, artifact_digest, created_at DESC), artifact-only lookup, GIN indexes on scope->'cves', scope->'entrypoints', and provenance->'inputs', plus replay-log indexes by digest, by tenant, and a partial index on matches = false.
Row-Level Security. All three tables have RLS enabled with USING (tenant_id = current_setting('app.tenant_id', true)) policies (the slice cache inherits its tenant through the subgraph FK). Because current_setting(..., true) returns NULL when unset, an unset tenant blocks all rows.
Note: reachgraph.slice_cache (Postgres-side) and the Valkey slice cache are two distinct caches — the runtime slice path (ReachGraphSliceService) currently uses the Valkey IReachGraphCache; the slice_cache table is defined and migrated but is not the live read path for slices.
Source: src/__Libraries/StellaOps.ReachGraph.Persistence/Migrations/001_reachgraph_store.sql, Postgres/ReachGraphDataSource.cs, PostgresReachGraphRepository.*.cs, Program.cs (AddStartupMigrations).
API Design
Core Endpoints
The store surface is the ReachGraphController ([Route("v1/reachgraphs")]). Reads use the reachgraph-read rate-limiter policy (100 req/min/tenant); writes use reachgraph-write (20 req/min/tenant).
| Method | Path | Description |
|---|---|---|
| POST | /v1/reachgraphs | Upsert subgraph (idempotent by digest; 201 on create, 200 if already stored) |
| GET | /v1/reachgraphs/{digest} | Get full subgraph (sets ETag, Cache-Control 24h) |
| GET | /v1/reachgraphs/{digest}/slice | Query slice (one of q/cve/entrypoint/file required) |
| GET | /v1/reachgraphs/by-artifact/{artifactDigest} | List subgraph summaries for an artifact (limit, default 50) |
| POST | /v1/reachgraphs/replay | Verify determinism |
| DELETE | /v1/reachgraphs/{digest} | Delete a subgraph; requires an authenticated StellaOps principal with graph:admin |
Authentication & tenancy. Outside the Testing environment the service registers Authority resource-server auth (AddStellaOpsResourceServerAuthentication, config section Authority:ResourceServer) and a fallback authorization policy that requires an authenticated user on every endpoint not marked [AllowAnonymous] (only /healthz, /readyz, the OpenAPI document, and the build-info endpoint are anonymous). Tenant resolution is claim-first and header-refusing: the validated stellaops:tenant claim wins (with tenant_id/tenant/tid aliases), via StellaOpsTenantResolver; the canonical X-StellaOps-TenantId header is honoured only for stellaops:admin principals, and the legacy X-Tenant-ID header is never a tenancy source. A request with no resolvable tenant claim is rejected (400 tenant_missing in the controller, InvalidOperationException on the evidence-cache path) — it never defaults. (Program.cs:270-288 ResolveTenantId; ReachGraphController.cs:301-322 TryResolveTenant; StellaOps.Auth.ServerIntegration/Tenancy/StellaOpsTenantResolver.cs.) Note: aside from the graph:admin gate on DELETE, individual store/slice/replay endpoints do not require a specific read/write scope today — they rely on the fallback “authenticated user” policy plus tenant scoping. (graph:read/graph:write scopes exist in StellaOpsScopes.cs but are not enforced per-route in this service.)
Source: ReachGraphController.cs, Program.cs (auth/rate-limit/health registration), Security/ReachGraphDestructiveActionAuthorizer.cs.
Slice Query Types
Package Slice (
?q=pkg:npm/lodash@4.17.21)- Returns subgraph containing package and neighbors
- Configurable depth and direction
CVE Slice (
?cve=CVE-2024-1234)- Returns all paths from entrypoints to vulnerable sinks
- Includes edge explanations for each hop
Entrypoint Slice (
?entrypoint=/app/bin/svc)- Returns everything reachable from entry
- Optionally filtered to paths reaching sinks
File Slice (
?file=src/**/*.ts)- Returns impact of changed files
- Useful for PR-based analysis
Integration Points
Upstream (Data Producers)
- Scanner.CallGraph: Produces nodes and edges with edge explanations
- Scanner.Cartographer: Assembles
CallGraphSnapshotplus SBOM/callgraph provenance intoReachGraphMinimaland emits an overlay containing reachable node ids, reachable sink ids, and deterministic witness paths. This is the Scanner-owned graph assembly boundary; durable storage remains the ReachGraph WebService/repository responsibility. - Scanner.Worker source/build-stage path: Emits a local
richgraph-v1artifact plus astellaops.scanner.reachability-witness@v1witness. The witness includes PolicyGateEvidence-compatiblesubject/evidencefields and Release Evidence component identity/digest fields, includingcomponentId, so Phase 3 policy-gate integration can consume the shape without reinterpreting Scanner internals. Local witness output is not durable ReachGraph storage by itself. - Signals: Provides runtime confirmation of reachability. This is a runtime query dependency, not a stored-graph producer:
SignalsHttpAdaptercallsGET /signals/facts/{artifactDigest}on the live Signals service when serving the unified/v1/reachability/*endpoints. The base URL resolves fromSignals:BaseUrl,STELLAOPS_SIGNALS_URL, or thesignals.stella-ops.localalias. - Attestor / DSSE signing: The Core library ships
IReachGraphSignerService(DSSE wrap/verify hooks). On upsert,ReachGraphStoreServiceverifies any signatures already present on the graph if a signer service is registered. The WebService does not register anIReachGraphSignerServicein DI today, so signature verification on upsert is effectively a no-op in the running service and the service does not itself sign graphs — signing/verification is a roadmap integration. (ReachGraphStoreServiceaccepts an optionalIReachGraphSignerService?.)
Relationship to the generic Graph module. ReachGraph is a distinct store specialized for “why reachable?” queries; it does not consume node/edge events from the generic
Graphmodule today. The Graph row in the Problem Statement table is a capability comparison, not a live event-ingestion integration. ReachGraph is populated by HTTP upsert (POST /v1/reachgraphs) from Scanner.Worker, not by a Graph event bus.
Downstream (Data Consumers)
- Policy Engine:
ReachabilityRequirementGatequeries slices - Web Console: “Why Reachable?” panel displays paths
- CLI:
stella reachgraph slice,stella reachgraph replay, andstella reachgraph verifycommands (ReachGraphCommandGroup.cs) - ExportCenter: Includes subgraphs in evidence bundles
Determinism Guarantees
Canonical Serialization
- Sorted object keys (lexicographic)
- Sorted arrays by deterministic field
- UTC ISO-8601 timestamps
- No null fields (omit when null)
Replay Verification
- POST
/v1/reachgraphs/replayloads the stored graph forexpectedDigest, verifies the supplied input digests against the stored provenance (inputsVerified), and recomputes the digest of the stored graph - Returns
{match, computedDigest, expectedDigest, durationMs, inputsVerified};matchis true when the recomputed digest equalsexpectedDigest - Records the attempt in
reachgraph.replay_logfor the audit trail when the expected graph is found — including a digest mismatch on a found graph (Matches=false). Two branches deliberately do not write a row: the graph-not-found case (nothing to replay) and thecatchfallback (the response carriescomputedDigest="ERROR"). Failed-lookup and errored replays are therefore an intentional audit gap, not a guaranteed record (ReachGraphReplayService.RecordReplayAsync, success path only). - Current limitation: the service recomputes the digest of the stored graph rather than rebuilding the graph from the supplied SBOM/VEX/callgraph/ runtime-facts inputs. A full rebuild-from-inputs replay is a roadmap item (see the explicit comment in
ReachGraphReplayService.ReplayAsync).
Source:
src/ReachGraph/StellaOps.ReachGraph.WebService/Services/ReachGraphReplayService.cs.- POST
Content Addressing
- Same content always produces same digest
- Enables cache keying and deduplication
Performance Characteristics
These are design targets (not measured SLOs enforced in code):
| Operation | Target Latency | Notes |
|---|---|---|
| Slice query | P95 < 200ms | Cached in Valkey (30 min slice TTL) |
| Full graph retrieval | P95 < 500ms | gzip blob; full-graph cache TTL 24h |
| Upsert | P95 < 1s | Idempotent, gzip compression |
| Replay | P95 < 5s | Currently bounded by stored-graph re-hash, not full rebuild |
Security Considerations
- Tenant Isolation: RLS policies enforce at database level
- Rate Limiting: 100 req/min reads, 20 req/min writes
- DSSE Signing: The schema reserves a
signaturesarray and the Core library provides DSSE wrap/verify hooks so graphs can be made offline-verifiable. The running WebService does not currently sign graphs or register a signer (see Integration Points → Attestor); when signatures are present and a signer is wired, upsert verifies them and rejects invalid signatures. - Input Validation:
ReachGraphStoreService.ValidateGraphrequiresSchemaVersion,Artifact.Name,Artifact.Digest(must besha256:/sha512:), andProvenance.Inputs.Sbom; it warns (does not reject) when the provenance timestamp is older than 24 hours. The unified-query and CVE-mapping controllers validate required body fields and return400otherwise. - Cache Tenant Authority: Graph and slice caches are acceleration only. The durable repository must confirm the request tenant owns the graph before cached graph or slice evidence can be returned. Live cache keys are tenant-scoped from the current request tenant, not a process-wide default.
- Startup Migrations: The WebService registers
ReachGraphDataSourceand runs embeddedStellaOps.ReachGraph.PersistenceSQL migrations for thereachgraphschema on startup. Fresh PostgreSQL databases must converge without manualpsql; test harnesses may disable the migration host explicitly while replacing storage with in-memory doubles. - Destructive Authorization: Graph deletion is fail-closed.
DELETE /v1/reachgraphs/{digest}requires an authenticated StellaOps principal carryinggraph:admin; tenant headers, graph write scope, or anonymous direct service access are insufficient and must leave the stored graph intact.
Related Documentation
- Sprint 1227.0012.0001 - Core Library (archived)
- Sprint 1227.0012.0002 - Store APIs (archived)
- Sprint 1227.0012.0003 - Integration (archived)
- richgraph-v1 contract
- PoE Predicate Spec
- Module AGENTS.md
Unified Query Interface
The ReachGraph module exposes a Unified Reachability Query API that provides a single facade for static, runtime, and hybrid queries.
API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/v1/reachability/static | POST | Query static reachability from call graph analysis |
/v1/reachability/runtime | POST | Query runtime reachability from observed execution facts |
/v1/reachability/hybrid | POST | Combine static and runtime for best-effort verdict |
/v1/reachability/batch | POST | Batch query for CVE vulnerability analysis |
Adapters
The unified query interface is backed by two adapters:
ReachGraphStoreAdapter: Implements
IReachGraphAdapterfromStellaOps.Reachability.Core- Queries static reachability from stored call graphs
- Uses BFS from entrypoints to target symbols
- Returns
StaticReachabilityResultwith distance, path, and evidence URIs
SignalsHttpAdapter: Implements
ISignalsAdapterfromStellaOps.Reachability.Core- Queries runtime observation facts from the live Signals service over HTTP
- Resolves the base URL from
Signals:BaseUrl,STELLAOPS_SIGNALS_URL, or the localsignals.stella-ops.localalias - Pulls
GET /signals/facts/{subjectKey}and translates the stored reachability fact intoRuntimeReachabilityResult - The in-memory adapter remains test-only and is no longer wired into the live host
Hybrid Query Flow
┌────────────────┐
│ Hybrid Query │
│ Request │
└───────┬────────┘
│
▼
┌───────────────────────────────────────────┐
│ ReachabilityIndex Facade │
│ (StellaOps.Reachability.Core) │
└───────┬───────────────────────┬───────────┘
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ ReachGraph │ │ Signals │
│ StoreAdapter │ │ Adapter │
└───────┬───────┘ └───────┬───────┘
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ PostgreSQL + │ │ Signals svc │
│ Valkey Cache │ │ (HTTP facts) │
└───────────────┘ └───────────────┘
The unified reachability surface is ReachabilityController ([Route("v1/reachability")]) backed by IReachabilityIndex / ReachabilityIndex from StellaOps.Reachability.Core. The four endpoints accept a SymbolRef plus an artifactDigest; hybrid/batch also accept includeStatic, includeRuntime, observationWindow (default 7 days), and minConfidenceThreshold (default 0.8). The static adapter is ReachGraphStoreAdapter; the runtime adapter is SignalsHttpAdapter, which calls GET /signals/facts/{artifactDigest} on the live Signals service. Source: Controllers/ReachabilityController.cs, Services/ReachGraphStoreAdapter.cs, Services/SignalsHttpAdapter.cs, Program.cs.
Query Models
SymbolRef - Identifies a code symbol. purl, namespace, type, and method are required; signature defaults to empty. Use "_" for type (and namespace) when there is no enclosing class/namespace. The record also exposes computed canonicalId (SHA-256 of purl|namespace|type|method|signature) and displayName properties:
{
"purl": "pkg:nuget/System.Net.Http@8.0.0",
"namespace": "System.Net.Http",
"type": "HttpClient",
"method": "GetAsync",
"signature": "(string)"
}
StaticReachabilityResult:
{
"symbol": { "purl": "...", "namespace": "...", "type": "...", "method": "..." },
"artifactDigest": "sha256:abc123...",
"isReachable": true,
"pathCount": 2,
"shortestPathLength": 3,
"entrypoints": ["/app/bin/svc"],
"guards": [{ "type": "FeatureFlag", "key": "FEATURE_X", "value": "enabled" }],
"evidenceUris": ["stella:evidence/reachgraph/sha256:abc123/symbol:..."],
"analyzedAt": "2025-06-15T12:00:00Z",
"analyzerVersion": "..."
}
RuntimeReachabilityResult:
{
"symbol": { ... },
"artifactDigest": "sha256:abc123...",
"wasObserved": true,
"observationWindow": "7.00:00:00",
"windowStart": "2025-06-08T12:00:00Z",
"windowEnd": "2025-06-15T12:00:00Z",
"hitCount": 1250,
"firstSeen": "2025-06-10T08:00:00Z",
"lastSeen": "2025-06-15T12:00:00Z",
"contexts": [{ "containerId": "abc", "processId": 42, "route": "/api", "environment": "production", "frequency": 0.85 }],
"evidenceUris": ["stella:evidence/signals/sha256:abc123/symbol:..."],
"traffic": { "requestCount": 100000, "percentile": "p95", "environments": ["production"] },
"agentVersion": "..."
}
HybridReachabilityResult: verdict is a VerdictRecommendation object (vexStatus is one of affected/not_affected/under_investigation, with an optional justification/statusNotes/requiresManualReview), not a bare string. latticeState carries the computed 8-state lattice value, and contentDigest is a computed sha256: hash of the canonical content:
{
"symbol": { ... },
"artifactDigest": "sha256:abc123...",
"latticeState": "ConfirmedReachable",
"confidence": 0.92,
"staticResult": { ... },
"runtimeResult": { ... },
"verdict": { "vexStatus": "affected", "justification": null },
"evidence": { "uris": [...], "collectedAt": "2025-06-15T12:00:00Z" },
"computedAt": "2025-06-15T12:00:00Z"
}
Source: src/__Libraries/StellaOps.Reachability.Core/SymbolRef.cs, StaticReachabilityResult.cs, RuntimeReachabilityResult.cs, HybridReachabilityResult.cs.
CVE-Symbol Mapping API
The WebService also hosts CveMappingController ([Route("v1/cve-mappings")]), which maps CVE identifiers to the specific vulnerable functions/methods used during reachability analysis. It is backed by ICveSymbolMappingService from StellaOps.Reachability.Core.CveMapping.
| Method | Path | Description |
|---|---|---|
| GET | /v1/cve-mappings/{cveId} | All symbol mappings for a CVE (404 if none) |
| GET | /v1/cve-mappings/by-package?purl=… | Mappings affecting a package |
| GET | /v1/cve-mappings/by-symbol?symbol=…&language=… | Search mappings by symbol name |
| POST | /v1/cve-mappings | Add/update a CVE↔symbol mapping (reachgraph-write) |
| POST | /v1/cve-mappings/analyze-patch | Extract vulnerable symbols from a commit URL or inline diff |
| POST | /v1/cve-mappings/{cveId}/enrich | Enrich a CVE mapping from the OSV database |
| GET | /v1/cve-mappings/stats | Mapping-corpus statistics (totals, by-source, by-type, avg confidence) |
A VulnerableSymbol carries a CanonicalSymbol (namespace/type/method/signature, SymbolSource, optional PURL), a VulnerabilityType, a confidence score, an optional source file, and an optional LineRange. MappingSource and VulnerabilityType are parsed case-insensitively and default to Unknown.
Source: src/ReachGraph/StellaOps.ReachGraph.WebService/Controllers/CveMappingController.cs, StellaOps.Reachability.Core (CveMapping, Symbols namespaces).
Lattice Triage Service
Overview
The Lattice Triage Service provides a workflow-oriented surface on top of the 8-state reachability lattice, enabling operators to visualise lattice states, apply evidence, perform manual overrides, and maintain a full audit trail of every state transition.
Library: StellaOps.Reachability.Core Namespace: StellaOps.Reachability.Core
Models
| Type | Purpose |
|---|---|
LatticeTriageEntry | Per-(component, CVE) snapshot: current state, confidence, VEX status, full transition history. Content-addressed EntryId (triage:sha256:…). Computed RequiresReview / HasOverride. |
LatticeTransitionRecord | Immutable log entry per state change: from/to state, confidence before/after, trigger, reason, actor, evidence digests, timestamp. Computed IsManualOverride. |
LatticeTransitionTrigger | Enum: StaticAnalysis, RuntimeObservation, ManualOverride, SystemReset, AutomatedRule. Serialised as JsonStringEnumConverter. |
LatticeOverrideRequest | Operator request to force a target state with reason, actor, and evidence digests. |
LatticeOverrideResult | Outcome of an override: applied flag, updated entry, transition, optional warning. |
LatticeTriageQuery | Filters: State?, RequiresReview?, ComponentPurlPrefix?, Cve?, Limit (default 100), Offset. |
Service Interface (ILatticeTriageService)
| Method | Description |
|---|---|
GetOrCreateEntryAsync(componentPurl, cve) | Returns existing entry or creates one at Unknown state. |
ApplyEvidenceAsync(componentPurl, cve, evidenceType, reason?, evidenceDigests?) | Delegates to ReachabilityLattice.ApplyEvidence, records transition. (No actor parameter — only OverrideStateAsync/ResetAsync capture an actor.) |
OverrideStateAsync(request) | Forces target state via Reset + ForceState sequence. Warns when overriding Confirmed* states. |
ListAsync(query) | Filters + pages entries; ordered by UpdatedAt descending. |
GetHistoryAsync(purl, cve) | Returns full transition log for an entry. |
ResetAsync(purl, cve, actor, reason) | Resets entry to Unknown, records SystemReset transition. |
VEX Status Mapping
| Lattice State | VEX Status |
|---|---|
Unknown, StaticReachable, Contested | under_investigation |
StaticUnreachable, RuntimeUnobserved, ConfirmedUnreachable | not_affected |
RuntimeObserved, ConfirmedReachable | affected |
Manual Override Behaviour
When an operator overrides state, the service:
- Resets the lattice to
Unknown. - Applies the minimal evidence sequence to reach the target state (e.g.,
ConfirmedReachable=StaticReachable+RuntimeObserved). - Sets confidence to the midpoint of the target state’s confidence range.
- Returns a warning when overriding from
ConfirmedReachableorConfirmedUnreachable, since these are high-certainty states.
DI Registration
AddReachabilityCore() (in StellaOps.Reachability.Core/ServiceCollectionExtensions.cs) registers ILatticeTriageService → LatticeTriageService (singleton, via TryAddSingleton). Note: the ReachGraph WebService Program.cs does not call AddReachabilityCore() and does not register the triage service — it wires IReachabilityIndex, IReachGraphAdapter, and ISignalsAdapter directly. The lattice triage service is therefore a library-level capability with no HTTP endpoint in the ReachGraph WebService today; consumers that need it must register and host it themselves.
Observability (OTel Metrics)
Meter: StellaOps.Reachability.Core.LatticeTriage (created via the injected IMeterFactory).
| Metric | Type | Description |
|---|---|---|
stellaops.lattice.triage.entries_created_total | Counter | Entries created |
stellaops.lattice.triage.evidence_applied_total | Counter | Evidence applications |
stellaops.lattice.triage.overrides_applied_total | Counter | Manual overrides |
stellaops.lattice.triage.resets_total | Counter | Lattice resets |
stellaops.lattice.triage.contested_total | Counter | Entries that entered the Contested state |
Source: src/__Libraries/StellaOps.Reachability.Core/LatticeTriageService.cs.
Test Coverage
21 test methods (20 [Fact] + 1 [Theory] with 3 [InlineData] rows) in StellaOps.Reachability.Core.Tests/LatticeTriageServiceTests.cs:
- Entry creation (new, idempotent, distinct keys)
- Evidence application (static→reachable, confirmed paths, conflicting→contested, digest recording)
- Override (target state, warnings on confirmed, HasOverride flag)
- Listing with filters (state, review, PURL prefix)
- History retrieval
- Reset transitions
- VEX mapping (theory test)
- Edge-case validation (null PURL, empty reason)
Last updated: 2026-05-30 (doc↔code reconciliation against src/ReachGraph/, src/__Libraries/StellaOps.ReachGraph*, and src/__Libraries/StellaOps.Reachability.Core).
