# Reachability Drift Detection - Operations Guide
Module: Scanner Version: 1.0 Last Updated: 2025-12-22
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.
- Call graph ingestion pipeline populating
call_graph_snapshots(Scanner Worker or external ingestion). - 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
- Ensure call graph snapshots exist for base and head scans.
- Compute drift by providing the base scan ID:
GET /api/v1/scans/{scanId}/drift?baseScanId={baseScanId}&language=dotnet&includeFullPath=false
- Page through sinks:
GET /api/v1/drift/{driftId}/sinks?direction=became_reachable&offset=0&limit=100
If baseScanId is omitted, the API returns the most recent stored drift result for the head scan.
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 stored. |
language | dotnet | Normalised (case-insensitively) through the drift language router. Accepts any tier-1 or tier-2 canonical name/alias (see callout below). An unsupported value returns 400. |
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).When
languageis omitted it defaults todotnet(LegacyDefaultLanguage). 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 or the ReachabilityDriftDetector yet (verified: neither ReachabilityDriftEndpoints.cs nor ReachabilityDriftDetector.cs create a Meter/Counter). 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 | Call graph not ingested for that scan + language | Ingest the call graph snapshot before running drift. |
| 404 drift result not found | No stored drift and no baseScanId provided | Provide baseScanId to compute drift. |
| 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 | Conflicting/invalid tenant headers | Resolve the tenant conflict; the resolver allows the 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.Storage/Postgres/Migrations/009_call_graph_tables.sql(createscall_graph_snapshots)src/Scanner/__Libraries/StellaOps.Scanner.Storage/Postgres/Migrations/010_reachability_drift_tables.sql(createscode_changes,reachability_drift_results,drifted_sinks; extendsmaterial_risk_changes)src/Scanner/StellaOps.Scanner.WebService/Endpoints/ReachabilityDriftEndpoints.cs(the two drift endpoints)src/Scanner/__Libraries/StellaOps.Scanner.CallGraph/Caching/CallGraphCacheConfig.cs(CallGraph:Cacheoption keys)
