# Reachability Drift Detection - Operations Guide
Module: Scanner Version: 1.0 Last Updated: 2026-10-19
Audience: operators running the Stella Ops Scanner WebService who need to configure, run, and troubleshoot reachability drift analysis.
1. Overview
Reachability Drift Detection compares call-graph reachability between two scans and surfaces newly reachable or newly unreachable sinks. The API lives in the Scanner WebService and relies on call-graph snapshots stored in PostgreSQL.
2. Prerequisites
2.1 Infrastructure Requirements
| Component | Minimum | Recommended | Notes |
|---|---|---|---|
| CPU | 4 cores | 8 cores | Call graph extraction is CPU heavy. |
| Memory | 4 GB | 8 GB | Large graphs need more memory. |
| PostgreSQL | 16+ | 16+ | Required for call graph + drift tables. |
| Valkey | 8.0+ | 8.0+ | Redis-compatible cache; optional call graph cache. |
| .NET Runtime | 10.0 | 10.0 | Scanner WebService runtime. |
2.2 Required Services
- Scanner WebService running with storage configured.
- Scanner Worker with reachability input preparation enabled: its
reachability-driftstage stores each scan’s extracted call graph incall_graph_snapshots(SPRINT_20261017_006 PCD-874). - PostgreSQL migrations for call graph and drift tables applied (auto-migrate is enabled by default).
Optional:
- Valkey call graph cache (
CallGraph:Cache).
Drift attestation is not wired into the WebService (roadmap). A drift attestation subsystem exists in the
StellaOps.Scanner.ReachabilityDriftlibrary (Attestation/DriftAttestationService.cs,IDriftSignerClient,DriftAttestationOptionswith sectionDriftAttestation, plus theAddDriftAttestationregistration extension), but as of this revision it is not registered or invoked anywhere insrc/— the Scanner WebService does not callAddDriftAttestation, and the two drift endpoints do not injectIDriftAttestationService. The twoGETdrift endpoints described here do not produce signed attestations, so a Signer service is not a prerequisite for running drift analysis today. Treat the attestation classes as dormant wiring until a host registers and calls them.
3. Configuration
3.1 Scanner WebService
File: etc/scanner.yaml (path depends on deployment)
scanner:
storage:
dsn: "Host=postgres;Database=stellaops;Username=scanner;Password=${SCANNER_DB_PASSWORD}"
database: "scanner" # optional; defaults to schema "scanner" when omitted
commandTimeoutSeconds: 30
api:
basePath: "/api/v1"
scansSegment: "scans"
Auto-migration is not configurable here. The Scanner WebService hard-codes
AutoMigrate = truefor its scanner-storage registration (Program.cs,AddScannerStorage(... AutoMigrate = true)). There is noscanner.storage.autoMigrateconfig key —StorageOptionsexposes onlydriver,dsn,database,commandTimeoutSeconds,healthCheckTimeoutSeconds, andmigrations(ScannerWebServiceOptions.cs,StorageOptions). Scanner storage migrations always run on startup.
3.2 Call Graph Cache (Optional)
CallGraph:
Cache:
enabled: true
connection_string: "valkey:6379"
key_prefix: "callgraph:"
ttl_seconds: 3600
gzip: true
circuit_breaker:
failure_threshold: 5
timeout_seconds: 30
half_open_timeout: 10
3.3 Authorization (Optional)
scanner:
authority:
enabled: true
issuer: "https://authority.local"
requiredScopes:
- "scanner.scans.read"
- "scanner.scans.write"
Both drift endpoints require the ScansRead policy (src/Scanner/StellaOps.Scanner.WebService/Endpoints/ReachabilityDriftEndpoints.cs). The policy is registered as an any-of assertion (Program.cs, AddStellaOpsAnyScopePolicy(ScannerPolicies.ScansRead, ...)), so a token satisfies it with either the service-local scope scanner.scans.read or the canonical catalog scope scanner:read (StellaOpsScopes.ScannerRead). When scanner.authority.enabled is false, the policies fall through to allow-all (policy.RequireAssertion(_ => true)) and allowAnonymousFallback defaults to true.
4. Running Drift Analysis
- Scan the base: the worker’s
reachability-driftstage stores the scan’s call graph inscanner.call_graph_snapshotsunder the drift router’s canonical language. - Submit the head scan through the scan submit route (
scanner.scans.submit) with the metadata keyreachability.drift.base_scan_idset to the base scan id. The route refuses a base scan the submitting tenant cannot see with 400. The head scan’sreachability-driftstage stores its call graph, the code changes and the drift result against the base. - Read the stored result (the drift GET without
baseScanId) and page its sinks with the drifted-sinks route, or open the Console screen/security/reachability/drift.
The drift GET with baseScanId computes a drift from the two stored graphs and returns it in full without storing it: the GET performs no writes (SPRINT_20261017_006 PCD-847). The drifted-sinks route pages stored results only, so it does not find the id of a computed result. Worker logs name the outcome of each stage run (reachability-drift: stored drift ..., or a warning when the base scan belongs to another tenant or has no stored graph in that language).
4.1 Query Parameters
GET /scans/{scanId}/drift:
| Parameter | Default | Notes |
|---|---|---|
baseScanId | (none) | When omitted, the latest stored drift result for the head scan + language is returned (404 if none). When supplied, drift is computed and returned without being stored. |
language | (required) | Normalised (case-insensitively) through the drift language router. Accepts any tier-1 or tier-2 canonical name/alias (see callout below). An unsupported or omitted value returns 400; there is no implicit dotnet default. |
includeFullPath | false | When true, sink records carry the full reachability path instead of the compressed path. |
GET /drift/{driftId}/sinks:
| Parameter | Default | Notes |
|---|---|---|
direction | became_reachable | Accepts became_reachable / became_unreachable and the aliases newly_reachable/reachable/up and newly_unreachable/unreachable/down. |
offset | 0 | Must be >= 0. |
limit | 100 | Must be between 1 and 500. |
Supported languages. The drift pipeline routes through
IDriftLanguageRouter, which seeds ten canonical languages:
- Tier 1:
dotnet(aliasescsharp/c#/cs/.net/net),java(aliasjvm),go(aliasgolang),python(aliaspy), andnode(aliasesnodejs/javascript/js/typescript/ts).- Tier 2:
bun(aliasbunjs),deno(no aliases),php(no aliases),ruby(aliasrb), andrust(aliasrs).An omitted
languagereturns 400; there is no default (DriftLanguageRouter.TryNormalize). Matching is case-insensitive and any of the canonical names or aliases above is accepted. Any value the router cannot normalise to one of those ten languages returns a 400 whose body lists every canonical language (tier 1 and tier 2), built fromlanguageRouter.SupportedLanguages.Select(d => d.Canonical)(src/Scanner/__Libraries/StellaOps.Scanner.ReachabilityDrift/Routing/DriftLanguageRouter.cs).
5. Deployment Modes
5.1 Standalone
- Run Scanner WebService with PostgreSQL reachable.
- Provide
scanner.storage.dsnandscanner.api.basePath.
5.2 Unsupported Kubernetes Mode
Kubernetes deployment mode is retired. Configure readiness/liveness checks, scanner.yaml, secrets, PostgreSQL connectivity, and startup migrations through the supported Compose profile or host service manager.
5.3 Air-Gapped
- Use Offline Kit flows for advisory data and signatures.
- Avoid external endpoints; configure any optional integrations to local services.
6. Monitoring and Metrics
There are no drift-specific metrics emitted by the drift endpoints, the reachability-drift worker stage or the ReachabilityDriftDetector yet (none of them creates a Meter/Counter); the worker’s generic per-stage metrics cover the stage. Recommended operational checks:
- API logs for
/api/v1/scans/{scanId}/driftand/api/v1/drift/{driftId}/sinks. - PostgreSQL table sizes and growth for
call_graph_snapshots(migration 009), andcode_changes,reachability_drift_results,drifted_sinksplus thematerial_risk_changesdrift columns (migration 010). - Valkey connectivity and cache hit rates if
CallGraph:Cacheis enabled.
7. Troubleshooting
| Symptom | Likely Cause | Resolution |
|---|---|---|
| 400 invalid scan identifier | scanId not parseable | Supply a valid scan id. |
| 400 unsupported drift language | language not a supported tier-1 or tier-2 language/alias | Use one of dotnet, java, go, python, node (tier 1) or bun, deno, php, ruby, rust (tier 2), or one of their aliases (see §4.1); the 400 body lists the supported set. |
| 400 invalid base scan identifier | baseScanId not parseable | Supply a valid scan id. |
| 404 scan not found / base scan not found | Invalid scan ID | Verify scan ID or resolve by image reference. |
| 404 base/head call graph not found | No call_graph_snapshots row for that scan + language | The scan extracted no call graph, its graph is in another language, or it ran before the reachability-drift stage existed. Re-scan, then compare. |
| 404 drift result not found | No stored drift result for the head scan + language | The head scan was submitted without reachability.drift.base_scan_id, or its base had no stored graph in that language (see the worker warning). Supplying baseScanId computes a result but does not store one. |
| 400 invalid direction | Unsupported direction value | Use became_reachable or became_unreachable (aliases in §4.1). |
| 400 invalid offset / invalid limit | offset < 0 or limit outside 1…500 | Use offset >= 0 and 1 <= limit <= 500. |
| 400 invalid tenant context | No stellaops:tenant claim (tenant_missing) | Use a token that carries a tenant claim; only a global admin may select a tenant with X-StellaOps-TenantId. There is no default tenant. |
Note: The drift endpoints do not return
409 Conflict. The “computation already in progress” 409 belongs to the separate reachability compute endpointPOST /api/v1/scans/{scanId}/compute-reachability(ReachabilityEndpoints.cs, which maps it under thescansroute group and declaresStatus409Conflict), and that endpoint requires theScansWritepolicy — notScansRead. It is not part of this drift API.
8. References
- Scanner module: reachability drift
- Scanner drift API reference
- Air-gap: reachability drift workflows
src/Scanner/__Libraries/StellaOps.Scanner.Persistence/Migrations/001_scanner_consolidated_baseline.sql(createscall_graph_snapshots,code_changes,reachability_drift_results,drifted_sinks, and the drift columns ofmaterial_risk_changes)src/Scanner/StellaOps.Scanner.WebService/Endpoints/ReachabilityDriftEndpoints.cs(the two drift endpoints)src/Scanner/StellaOps.Scanner.Worker/Processing/Reachability/ReachabilityDriftStageExecutor.cs(the producer)src/Scanner/__Libraries/StellaOps.Scanner.CallGraph/Caching/CallGraphCacheConfig.cs(CallGraph:Cacheoption keys)
