# 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

ComponentMinimumRecommendedNotes
CPU4 cores8 coresCall graph extraction is CPU heavy.
Memory4 GB8 GBLarge graphs need more memory.
PostgreSQL16+16+Required for call graph + drift tables.
Valkey8.0+8.0+Redis-compatible cache; optional call graph cache.
.NET Runtime10.010.0Scanner WebService runtime.

2.2 Required Services

Optional:

Drift attestation is not wired into the WebService (roadmap). A drift attestation subsystem exists in the StellaOps.Scanner.ReachabilityDrift library (Attestation/DriftAttestationService.cs, IDriftSignerClient, DriftAttestationOptions with section DriftAttestation, plus the AddDriftAttestation registration extension), but as of this revision it is not registered or invoked anywhere in src/ — the Scanner WebService does not call AddDriftAttestation, and the two drift endpoints do not inject IDriftAttestationService. The two GET drift 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 = true for its scanner-storage registration (Program.cs, AddScannerStorage(... AutoMigrate = true)). There is no scanner.storage.autoMigrate config key — StorageOptions exposes only driver, dsn, database, commandTimeoutSeconds, healthCheckTimeoutSeconds, and migrations (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

  1. Scan the base: the worker’s reachability-drift stage stores the scan’s call graph in scanner.call_graph_snapshots under the drift router’s canonical language.
  2. Submit the head scan through the scan submit route (scanner.scans.submit) with the metadata key reachability.drift.base_scan_id set to the base scan id. The route refuses a base scan the submitting tenant cannot see with 400. The head scan’s reachability-drift stage stores its call graph, the code changes and the drift result against the base.
  3. 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:

ParameterDefaultNotes
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.
includeFullPathfalseWhen true, sink records carry the full reachability path instead of the compressed path.

GET /drift/{driftId}/sinks:

ParameterDefaultNotes
directionbecame_reachableAccepts became_reachable / became_unreachable and the aliases newly_reachable/reachable/up and newly_unreachable/unreachable/down.
offset0Must be >= 0.
limit100Must be between 1 and 500.

Supported languages. The drift pipeline routes through IDriftLanguageRouter, which seeds ten canonical languages:

  • Tier 1: dotnet (aliases csharp/c#/cs/.net/net), java (alias jvm), go (alias golang), python (alias py), and node (aliases nodejs/javascript/js/typescript/ts).
  • Tier 2: bun (alias bunjs), deno (no aliases), php (no aliases), ruby (alias rb), and rust (alias rs).

An omitted language returns 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 from languageRouter.SupportedLanguages.Select(d => d.Canonical) (src/Scanner/__Libraries/StellaOps.Scanner.ReachabilityDrift/Routing/DriftLanguageRouter.cs).


5. Deployment Modes

5.1 Standalone

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


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:


7. Troubleshooting

SymptomLikely CauseResolution
400 invalid scan identifierscanId not parseableSupply a valid scan id.
400 unsupported drift languagelanguage not a supported tier-1 or tier-2 language/aliasUse 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 identifierbaseScanId not parseableSupply a valid scan id.
404 scan not found / base scan not foundInvalid scan IDVerify scan ID or resolve by image reference.
404 base/head call graph not foundNo call_graph_snapshots row for that scan + languageThe 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 foundNo stored drift result for the head scan + languageThe 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 directionUnsupported direction valueUse became_reachable or became_unreachable (aliases in §4.1).
400 invalid offset / invalid limitoffset < 0 or limit outside 1…500Use offset >= 0 and 1 <= limit <= 500.
400 invalid tenant contextNo 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 endpoint POST /api/v1/scans/{scanId}/compute-reachability (ReachabilityEndpoints.cs, which maps it under the scans route group and declares Status409Conflict), and that endpoint requires the ScansWrite policy — not ScansRead. It is not part of this drift API.


8. References