# Scanner Drift API Reference

Module: Scanner Version: 1.0 Base Path: /api/v1 (configurable via Api.BasePath; scan segment via Api.ScansSegment, default scans) Last Updated: 2026-05-30


1. Overview

Audience: CI engineers and platform integrators gating releases on reachability drift, and tools that read or recompute drift between two scans.

The Scanner Drift API computes and retrieves reachability drift between scans. Drift detection identifies when code changes introduce new paths to sensitive sinks or remove existing paths to them — the signal that a release has materially changed an artifact’s attack surface.


2. Authentication and Authorization

Required Scopes

EndpointAuthorization policy / scope
Read drift results (GET /scans/{scanId}/drift)scanner.scans.read
List drifted sinks (GET /drift/{driftId}/sinks)scanner.scans.read
List reachability components / findings / explain / trace exportscanner.scans.read
Compute reachability (POST /scans/{scanId}/compute-reachability)scanner.scans.write

The Scanner WebService enforces these via the ScannerPolicies.ScansRead (scanner.scans.read) and ScannerPolicies.ScansWrite (scanner.scans.write) authorization policies, which map directly to the same scope strings (ScannerAuthorityScopes.ScansRead/.ScansWrite). These are Scanner-local scopes and are not part of the central StellaOpsScopes catalog (which exposes scanner:read, scanner:scan, scanner:write); Authority must grant the scanner.scans.* scopes to clients that call these endpoints.

Headers

Authorization: Bearer <access_token>
X-StellaOps-TenantId: <tenant_id>   # optional; tenant scoping for drift/reachability data

Tenant resolution (ScannerRequestContextResolver.TryResolveTenant) is used to isolate stored drift results and call-graph snapshots. The tenant is taken from the stellaops:tenant JWT claim when present; otherwise from a tenant header. The canonical header is X-StellaOps-TenantId, with X-Stella-Tenant (legacy) and X-Tenant-Id (alternate) accepted as aliases. When the request carries no claim and no header, the endpoints fall back to the default tenant. If a tenant header conflicts with the JWT claim, or two tenant headers disagree, the request fails with 400 Bad Request (tenant_conflict).


3. Endpoints

3.1 GET /scans/{scanId}/drift

Returns drift results for the scan. If baseScanId is provided, drift is computed and stored. If omitted, the most recent stored drift result is returned.

Parameters

NameInTypeRequiredDescription
scanIdpathstringyesHead scan identifier
baseScanIdquerystringnoBase scan identifier. When provided, drift is recomputed and stored; when omitted, the most recent stored result is returned.
languagequerystringnoDrift language. Normalized through IDriftLanguageRouter; only wired languages are accepted (canonical set includes dotnet, java, go, python, node; aliases such as csharp are normalized). An unsupported/unrecognized language returns 400. When omitted, the router default applies (pinned to dotnet for backward compatibility).
includeFullPathquerybooleannoInclude full path nodes (fullPath) in compressed paths (default false). Only honoured when drift is recomputed (i.e. baseScanId is supplied).

Response: 200 OK

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "baseScanId": "base123",
  "headScanId": "head456",
  "language": "dotnet",
  "detectedAt": "2025-12-22T10:30:00Z",
  "newlyReachable": [
    {
      "id": "660e8400-e29b-41d4-a716-446655440001",
      "sinkNodeId": "MyApp.Services.DbService.ExecuteQuery(string)",
      "symbol": "DbService.ExecuteQuery",
      "sinkCategory": "SQL_RAW",
      "direction": "became_reachable",
      "cause": {
        "kind": "guard_removed",
        "description": "Guard condition removed in AuthMiddleware.Validate",
        "changedSymbol": "AuthMiddleware.Validate",
        "changedFile": "src/Middleware/AuthMiddleware.cs",
        "changedLine": 42,
        "codeChangeId": "770e8400-e29b-41d4-a716-446655440002"
      },
      "path": {
        "entrypoint": {
          "nodeId": "MyApp.Controllers.UserController.GetUser(int)",
          "symbol": "UserController.GetUser",
          "file": "src/Controllers/UserController.cs",
          "line": 15,
          "package": "app",
          "isChanged": false,
          "changeKind": null
        },
        "sink": {
          "nodeId": "MyApp.Services.DbService.ExecuteQuery(string)",
          "symbol": "DbService.ExecuteQuery",
          "file": "src/Services/DbService.cs",
          "line": 88,
          "package": "app",
          "isChanged": false,
          "changeKind": null
        },
        "intermediateCount": 3,
        "keyNodes": [
          {
            "nodeId": "MyApp.Middleware.AuthMiddleware.Validate()",
            "symbol": "AuthMiddleware.Validate",
            "file": "src/Middleware/AuthMiddleware.cs",
            "line": 42,
            "package": "app",
            "isChanged": true,
            "changeKind": "guard_changed"
          }
        ]
      },
      "associatedVulns": []
    }
  ],
  "newlyUnreachable": [],
  "resultDigest": "sha256:a1b2c3d4...",
  "totalDriftCount": 1,
  "hasMaterialDrift": true
}

Response: 400 Bad Request

Returned for an invalid head scanId, an invalid baseScanId, an unsupported language, an invalid tenant context (tenant_conflict), or an invalid drift request surfaced by the detector (ArgumentException).

Response: 404 Not Found

Returned when any of the following cannot be located (each is a distinct, specific Problem Details title):


3.2 GET /drift/{driftId}/sinks

Returns drifted sinks for a drift result.

Parameters

NameInTypeRequiredDescription
driftIdpathuuidyesDrift result identifier (must be a non-empty GUID)
directionquerystringnobecame_reachable or became_unreachable. Defaults to became_reachable when omitted. Aliases are accepted and normalized: newly_reachable/reachable/up -> became_reachable; newly_unreachable/unreachable/down -> became_unreachable. Any other value returns 400.
offsetqueryintegernoOffset (default: 0; must be >= 0)
limitqueryintegernoPage size (default: 100; must be between 1 and 500)

Response: 200 OK

{
  "driftId": "550e8400-e29b-41d4-a716-446655440000",
  "direction": "became_reachable",
  "offset": 0,
  "limit": 100,
  "count": 1,
  "sinks": [
    {
      "id": "660e8400-e29b-41d4-a716-446655440001",
      "sinkNodeId": "MyApp.Services.DbService.ExecuteQuery(string)",
      "symbol": "DbService.ExecuteQuery",
      "sinkCategory": "SQL_RAW",
      "direction": "became_reachable",
      "cause": {
        "kind": "guard_removed",
        "description": "Guard condition removed in AuthMiddleware.Validate",
        "changedSymbol": "AuthMiddleware.Validate",
        "changedFile": "src/Middleware/AuthMiddleware.cs",
        "changedLine": 42,
        "codeChangeId": "770e8400-e29b-41d4-a716-446655440002"
      },
      "path": {
        "entrypoint": {
          "nodeId": "MyApp.Controllers.UserController.GetUser(int)",
          "symbol": "UserController.GetUser",
          "file": "src/Controllers/UserController.cs",
          "line": 15,
          "package": "app",
          "isChanged": false,
          "changeKind": null
        },
        "sink": {
          "nodeId": "MyApp.Services.DbService.ExecuteQuery(string)",
          "symbol": "DbService.ExecuteQuery",
          "file": "src/Services/DbService.cs",
          "line": 88,
          "package": "app",
          "isChanged": false,
          "changeKind": null
        },
        "intermediateCount": 3,
        "keyNodes": []
      },
      "associatedVulns": []
    }
  ]
}

Response: 400 Bad Request

Returned for an empty/invalid driftId, an invalid direction, a negative offset, a limit outside 1..500, or an invalid tenant context.

Response: 404 Not Found

Returned when no drift result exists for the supplied driftId (and tenant).


3.3 POST /scans/{scanId}/compute-reachability

Triggers reachability computation for a scan.

Parameters

NameInTypeRequiredDescription
scanIdpathstringyesScan identifier

Request Body

{
  "forceRecompute": false,
  "entrypoints": ["MyApp.Controllers.UserController.GetUser"],
  "targets": ["pkg:nuget/Dapper@2.0.123"]
}

Response: 202 Accepted

{
  "jobId": "reachability_head456",
  "status": "scheduled",
  "estimatedDuration": null
}

estimatedDuration is a nullable string (e.g. an ISO-8601 duration), not a numeric field. This endpoint is audited (AuditModules.Scanner / AuditActions.Scanner.Compute, target reachability).

Response: 400 Bad Request

Returned for an invalid scanId.

Response: 404 Not Found

Returned when the scan cannot be located.

Response: 409 Conflict

Returned when computation is already in progress for the scan.

Response: 501 Not Implemented

Returned when no reachability compute runtime is configured for this Scanner deployment (ReachabilityRuntimeUnavailableException). The same 501 applies to §3.4–§3.7 below when the reachability query/explain backend is unavailable.


3.4 GET /scans/{scanId}/reachability/components

Lists components with reachability status.

Parameters

NameInTypeRequiredDescription
scanIdpathstringyesScan identifier
purlquerystringnoFilter by PURL
statusquerystringnoFilter by status

Response: 200 OK

{
  "items": [
    {
      "purl": "pkg:nuget/Newtonsoft.Json@13.0.1",
      "status": "reachable",
      "confidence": 0.92,
      "latticeState": "confirmed",
      "why": ["entrypoint:UserController.GetUser"]
    }
  ],
  "total": 1
}

3.5 GET /scans/{scanId}/reachability/findings

Lists reachability findings for CVEs.

Parameters

NameInTypeRequiredDescription
scanIdpathstringyesScan identifier
cvequerystringnoFilter by CVE
statusquerystringnoFilter by status

Response: 200 OK

{
  "items": [
    {
      "cveId": "CVE-2024-12345",
      "purl": "pkg:nuget/Dapper@2.0.123",
      "status": "reachable",
      "confidence": 0.81,
      "latticeState": "likely",
      "severity": "critical",
      "affectedVersions": "< 2.0.200"
    }
  ],
  "total": 1
}

3.6 GET /scans/{scanId}/reachability/explain

Explains reachability for a CVE and PURL.

Parameters

NameInTypeRequiredDescription
scanIdpathstringyesScan identifier
cvequerystringyesCVE identifier
purlquerystringyesPackage URL

Response: 200 OK

{
  "cveId": "CVE-2024-12345",
  "purl": "pkg:nuget/Dapper@2.0.123",
  "status": "reachable",
  "confidence": 0.81,
  "latticeState": "likely",
  "pathWitness": ["entrypoint:UserController.GetUser", "sink:Dapper.Query"],
  "why": [
    { "code": "call_graph", "description": "Path exists from HTTP entrypoint", "impact": 0.6 }
  ],
  "evidence": {
    "staticAnalysis": {
      "callgraphDigest": "sha256:...",
      "pathLength": 4,
      "edgeTypes": ["direct", "virtual"]
    },
    "runtimeEvidence": {
      "observed": false,
      "hitCount": 0,
      "lastObserved": null
    },
    "policyEvaluation": {
      "policyDigest": "sha256:...",
      "verdict": "block",
      "verdictReason": "delta_reachable > 0"
    }
  },
  "spineId": "spine:sha256:..."
}

Response: 400 Bad Request

Returned for an invalid scanId, or when cve or purl is missing/blank (both are required).

Response: 404 Not Found

Returned when the scan cannot be located, or when no explanation exists for the supplied cve/purl pair.


3.7 GET /scans/{scanId}/reachability/traces/export

Exports a callable-path reachability trace (with runtime-evidence sensor gaps) for the scan. Output is deterministic and content-addressed (contentDigest is a SHA-256 over the canonicalized export).

Authorization: scanner.scans.read.

Parameters

NameInTypeRequiredDescription
scanIdpathstringyesScan identifier
formatquerystringnoExport format (default json-lines). Accepted: json-lines (aliases jsonl, ndjson), json, graphson, sarif. Any other value returns 400.
includeRuntimeEvidencequerybooleannoInclude runtime-evidence fields (default true).
minReachabilityScorequerynumbernoMinimum reachability score filter; must be between 0 and 1 inclusive.
runtimeConfirmedOnlyquerybooleannoRestrict to runtime-confirmed nodes/edges (default false).

Response: 200 OK

When reachable callable paths are available, status is callable_path_proof and nodes/edges are populated; otherwise status is sensor_gap with an explanatory sensorGaps entry. Runtime trace evidence is reported as a sensor gap (runtime-trace-backend-unavailable) unless a runtime trace collector is configured.

{
  "scanId": "head456",
  "status": "sensor_gap",
  "format": "json-lines",
  "canonicalizationMethod": "stellaops.reachability.trace-export.sensor-gap.v1",
  "contentDigest": "sha256:...",
  "timestamp": "2026-05-30T10:30:00Z",
  "nodeCount": 0,
  "edgeCount": 0,
  "runtimeCoverage": 0,
  "averageReachabilityScore": null,
  "nodes": [],
  "edges": [],
  "sensorGaps": [
    {
      "id": "runtime-trace-backend-unavailable",
      "category": "runtime-sensor",
      "severity": "gap",
      "summary": "Runtime trace evidence is not available for this scan.",
      "detail": "No runtime trace collector or durable trace store is configured ...",
      "evidence": ["sensor-gap:runtime-trace-backend-unavailable", "scan:head456"],
      "remediation": "Configure runtime sensor ingestion and trace storage, then rerun reachability ..."
    }
  ],
  "evidence": ["scan:head456", "sensor-gap:runtime-trace-backend-unavailable"]
}

Response: 400 Bad Request

Returned for an invalid scanId, an unsupported format, or a minReachabilityScore outside 0..1.

Response: 404 Not Found

Returned when the scan cannot be located.


4. Request and Response Models

Key models (JSON names shown). totalDriftCount and hasMaterialDrift are server-computed (read-only): totalDriftCount = newlyReachable.length + newlyUnreachable.length, and hasMaterialDrift = newlyReachable.length > 0.

Drift models (StellaOps.Scanner.ReachabilityDrift):

Compute / query / explain DTOs (StellaOps.Scanner.WebService.Contracts):

Trace export DTOs (StellaOps.Scanner.WebService.Endpoints.ReachabilityEndpoints):


5. Enumerations

DriftDirection

became_reachable
became_unreachable

DriftCauseKind

guard_removed
guard_added
new_public_route
visibility_escalated
dependency_upgraded
symbol_removed
unknown

CodeChangeKind

added
removed
signature_changed
guard_changed
dependency_changed
visibility_changed

SinkCategory

CMD_EXEC
UNSAFE_DESER
SQL_RAW
SQL_INJECTION
SSRF
FILE_WRITE
PATH_TRAVERSAL
TEMPLATE_INJECTION
CRYPTO_WEAK
AUTHZ_BYPASS
LDAP_INJECTION
XPATH_INJECTION
XXE
CODE_INJECTION
LOG_INJECTION
REFLECTION
OPEN_REDIRECT

6. Errors

Endpoints return Problem Details (RFC 7807) for errors. The type is one of the https://stellaops.org/problems/... URIs (validation, not-found, conflict, not-implemented, internal). Common cases:


7. Examples

7.1 cURL - Get Drift Results

curl -X GET \
  'https://scanner.example/api/v1/scans/head456/drift?baseScanId=base123&language=dotnet' \
  -H 'Authorization: Bearer <token>'

7.2 cURL - List Drifted Sinks

curl -X GET \
  'https://scanner.example/api/v1/drift/550e8400-e29b-41d4-a716-446655440000/sinks?direction=became_reachable&offset=0&limit=100' \
  -H 'Authorization: Bearer <token>'

7.3 cURL - Compute Reachability

curl -X POST \
  'https://scanner.example/api/v1/scans/head456/compute-reachability' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"forceRecompute": false}'

8. References