# Reachability Drift Detection - Architecture
Module: Scanner Version: 1.1 Status: Implemented (core drift engine + API; Node Babel integration pending; tier-2 router wiring complete — Sprint 20260512_032). Last Updated: 2026-05-13
1. Overview
Reachability Drift Detection tracks function-level reachability changes between scans. It highlights when code changes create new paths to sensitive sinks or remove existing paths, producing deterministic evidence for triage and VEX workflows.
Key outcomes:
- Detect regressions when previously unreachable sinks become reachable.
- Validate mitigations when reachable sinks become unreachable.
- Provide deterministic evidence for audit and policy decisions.
2. Key Concepts
2.1 Call Graph
A directed graph of function calls:
- Nodes: functions, methods, lambdas with file and line metadata.
- Edges: call relationships (direct, virtual, dynamic).
- Entrypoints: public handlers (HTTP, CLI, background services).
- Sinks: security-sensitive APIs from the sink registry.
2.2 Reachability Analysis
Multi-source traversal from entrypoints to sinks to determine exploitability.
2.3 Drift Detection
Compares reachability between base and head scans:
became_reachable: risk increased (new path to sink).became_unreachable: risk decreased (path removed or mitigated).
2.4 Cause Attribution
Explains why drift happened by correlating code changes with paths.
3. Data Flow
flowchart TD
A[Source or binary] --> B[Call graph extractor]
B --> C[CallGraphSnapshot]
C --> D[Reachability analyzer]
D --> E[ReachabilityResult]
C --> F[Code change extractor]
E --> G[ReachabilityDriftDetector]
F --> G
G --> H[ReachabilityDriftResult]
H --> I[Storage + API]
4. Component Architecture
4.1 Call Graph Extractors
Registered extractors are configured in CallGraphServiceCollectionExtensions. The drift API tier is set by IDriftLanguageRouter (src/Scanner/__Libraries/StellaOps.Scanner.ReachabilityDrift/Routing/DriftLanguageRouter.cs), which is the canonical source of supported languages and alias normalization. Tier-1 aliases: csharp/c#/cs/.net/net -> dotnet, nodejs/javascript/js/typescript/ts -> node, golang -> go, py -> python, jvm -> java. Tier-2 aliases (SPRINT_20260512_032): bunjs -> bun, rb -> ruby, rs -> rust; deno and php have no aliases. The GET /api/v1/scans/{scanId}/drift endpoint returns HTTP 400 for any language outside the union of tier-1 and tier-2 sets (currently cobol, binary, and any unrecognised value).
| Language | Extractor | Status | Notes |
|---|---|---|---|
| .NET | DotNetCallGraphExtractor | Wired (tier 1) | Roslyn semantic model; ASP.NET controllers, minimal APIs, hosted services, CLI main. |
| Java | JavaCallGraphExtractor | Wired (tier 1) | ASM bytecode parsing; Spring MVC, JAX-RS (javax + jakarta), Servlet, Micronaut, Quarkus, CLI main. |
| Go | GoCallGraphExtractor | Wired (tier 1) | SSA analysis; net/http, gin, echo, chi, fiber, gorilla/mux, gRPC, cobra CLI. |
| Python | PythonCallGraphExtractor | Wired (tier 1) | AST analysis; Flask, FastAPI, Django, Celery, CLI main. |
| Node.js | NodeCallGraphExtractor | Wired (tier 1) | Babel AST / trace fallback; Express, Fastify, Next.js, Hapi, Koa, AWS Lambda. |
| Bun | BunCallGraphExtractor | Router wired (tier 2, SPRINT_20260512_032) | Bun.serve / Elysia / Hono routes, @cron/@scheduled annotations, WebSocket handlers. Extractor registration pending follow-up sprint. |
| Deno | DenoCallGraphExtractor | Router wired (tier 2, SPRINT_20260512_032) | Deno.serve, Fresh handler exports, Oak / Hono routes, Deno Deploy handlers, Deno.cron, WebSocket. Extractor registration pending follow-up sprint. |
| PHP | PhpCallGraphExtractor | Router wired (tier 2, SPRINT_20260512_032) | Symfony route attributes, Laravel *Controller actions, Job/Listener handle(), Symfony Command::execute, scheduled tasks. Extractor registration pending follow-up sprint. |
| Ruby | RubyCallGraphExtractor | Router wired (tier 2, SPRINT_20260512_032) | Rails controllers (CRUD + public), Sinatra::Base / Grape::API, Sidekiq/ActiveJob perform/call, rake tasks, *CLI/*Command. Extractor registration pending follow-up sprint. |
| Rust | RustCallGraphExtractor | Router wired (tier 2, SPRINT_20260512_032) | tokio/async_std main, actix-web / axum / rocket / warp / tonic, AWS Lambda runtime, clap CLI, FFI export. Extractor delivered by Sprint 030; registration pending follow-up sprint. |
| JavaScript | JavaScriptCallGraphExtractor | Library present, not router-wired (tier 3) | Use node (tier 1) for Node.js code; standalone JS pending. |
| Binary | BinaryCallGraphExtractor | Library present, not router-wired (tier 3) | Native call edge extraction. |
4.2 Reachability Analyzer
Located in src/Scanner/__Libraries/StellaOps.Scanner.CallGraph/Analysis/.
4.3 Drift Detector
ReachabilityDriftDetector compares base and head snapshots and produces ReachabilityDriftResult with compressed paths.
4.4 Path Compressor and Cause Explainer
PathCompressorreduces paths to key nodes and optionally includes full paths.DriftCauseExplainercorrelates changes to explain why drift happened.
5. Language Support Matrix
Sprint 20260512_032 (TASK-032-04) extended the drift router to ten canonical languages. Tier-1 has full extractor + classifier + drift wiring. Tier-2 has router recognition (alias normalization + HTTP 200 instead of 400) and an existing classifier source documenting recognised framework patterns; the extractor registration in CallGraphServiceCollectionExtensions is still pending a follow-up sprint.
| Capability | Tier-1 (.NET, Java, Go, Python, Node) | Tier-2 (Bun, Deno, PHP, Ruby, Rust) | Tier-3 (JavaScript-standalone, Binary) |
|---|---|---|---|
Drift router (IDriftLanguageRouter) | Wired (Sprint 031) | Wired (Sprint 032) | Not wired |
| Call graph extraction | Supported | Library present (extractor registration pending) | Library present, not router-wired |
| Entrypoint detection | Supported | Supported (per-language <Lang>EntrypointClassifier.cs) | Library present, not router-wired |
| Sink detection | Supported | Supported (per-language <Lang>SinkMatcher.cs) | Library present, not router-wired |
Per-tier-2 framework conventions recognised by the existing classifiers (pinned in DriftLanguageRouterTests.Frameworks_<Lang>_Match_..._Conventions):
| Language | Aliases | Frameworks (documentation surface) |
|---|---|---|
bun | bunjs | bun-serve, elysia, hono, scheduled-cron, websocket |
deno | (none) | deno-cron, deno-deploy, deno-serve, fresh, hono, oak, websocket |
php | (none) | laravel-controller, laravel-job, laravel-listener, laravel-scheduled, symfony-command, symfony-route |
ruby | rb | grape, rails-cli, rails-controller, rake-task, sidekiq, sinatra |
rust | rs | actix-web, aws-lambda, axum, clap-cli, ffi-export, grpc-tonic, rocket, tokio-main, warp |
The drift API supported set is the canonical contract; adding a new tier (e.g. JavaScript-standalone) requires a sprint task that:
- Extends
DriftLanguageRouter.BuildTier2Descriptors()(or a newBuildTier3Descriptors()helper) with the new descriptor + alias list + framework list — matching the patterns already recognised by the corresponding<Lang>EntrypointClassifier.cssource; - Registers the corresponding
ICallGraphExtractorinAddCallGraphServices(); - Adds tier-2a behavioral coverage to
ReachabilityDriftEndpointsTests.
6. Storage Schema
Migrations are in src/Scanner/__Libraries/StellaOps.Scanner.Storage/Postgres/Migrations/.
Core tables:
call_graph_snapshots:scan_id,language,graph_digest,extracted_at,node_count,edge_count,entrypoint_count,sink_count,snapshot_json.reachability_results:scan_id,language,graph_digest,result_digest,computed_at,reachable_node_count,reachable_sink_count,result_json.code_changes:scan_id,base_scan_id,language,node_id,file,symbol,change_kind,details,detected_at.reachability_drift_results:base_scan_id,head_scan_id,language,newly_reachable_count,newly_unreachable_count,detected_at,result_digest.drifted_sinks:drift_result_id,sink_node_id,sink_category,direction,cause_kind,cause_description,compressed_path,associated_vulns.material_risk_changes: extended withbase_scan_id,cause,cause_kind,path_nodes,associated_vulnsfor drift attachments.
7. Cache and Determinism
If the call graph cache is enabled (CallGraph:Cache), cached keys follow this pattern:
callgraph:graph:{scanId}:{language}callgraph:reachability:{scanId}:{language}
Determinism is enforced by stable ordering and deterministic IDs (see DeterministicIds).
8. API Endpoints
Base path: /api/v1
| Method | Path | Description |
|---|---|---|
| GET | /scans/{scanId}/drift | Get or compute drift results for a scan. |
| GET | /drift/{driftId}/sinks | List drifted sinks (paged). |
| POST | /scans/{scanId}/compute-reachability | Trigger reachability computation. |
| GET | /scans/{scanId}/reachability/components | List components with reachability. |
| GET | /scans/{scanId}/reachability/findings | List findings with reachability. |
| GET | /scans/{scanId}/reachability/explain | Explain reachability for a CVE and PURL. |
See docs/api/scanner-drift-api.md for details.
9. Integration Points
- Policy gates: planned in
SPRINT_3600_0005_0001_policy_ci_gate_integration.md. - VEX candidate emission: planned alongside policy gates.
- Attestation:
StellaOps.Scanner.ReachabilityDrift.Attestationprovides DSSE signing utilities (integration is optional).
10. Performance Characteristics (Targets)
| Metric | Target | Notes |
|---|---|---|
| Call graph extraction (100K LOC) | < 60s | Per language extractor. |
| Reachability analysis | < 5s | BFS traversal on trimmed graphs. |
| Drift detection | < 10s | Graph comparison and compression. |
| Cache hit improvement | 10x | Valkey cache vs recompute. |
11. References
docs/implplan/archived/SPRINT_3600_0002_0001_call_graph_infrastructure.mddocs/implplan/archived/SPRINT_3600_0003_0001_drift_detection_engine.mddocs/api/scanner-drift-api.mddocs/operations/reachability-drift-guide.mddocs/product/advisories/archived/17-Dec-2025 - Reachability Drift Detection.mdsrc/Scanner/__Libraries/StellaOps.Scanner.ReachabilityDrift/
