# 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

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. Ensure call graph snapshots exist for base and head scans.
  2. Compute drift by providing the base scan ID:
    • GET /api/v1/scans/{scanId}/drift?baseScanId={baseScanId}&language=dotnet&includeFullPath=false
  3. 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:

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 stored.
languagedotnetNormalised (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.
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).

When language is omitted it defaults to dotnet (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 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 or the ReachabilityDriftDetector yet (verified: neither ReachabilityDriftEndpoints.cs nor ReachabilityDriftDetector.cs create a Meter/Counter). 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 foundCall graph not ingested for that scan + languageIngest the call graph snapshot before running drift.
404 drift result not foundNo stored drift and no baseScanId providedProvide baseScanId to compute drift.
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 contextConflicting/invalid tenant headersResolve 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 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