Files
git.stella-ops.org/docs/api/scanner-drift-api.md

11 KiB

Scanner Drift API Reference

Module: Scanner Version: 1.0 Base Path: /api/v1 Last Updated: 2025-12-22


1. Overview

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.


2. Authentication and Authorization

Required Scopes

Endpoint Scope
Read drift results scanner.scans.read
Compute reachability scanner.scans.write

Headers

Authorization: Bearer <access_token>
X-Tenant-Id: <tenant_uuid>  # optional fallback for rate limiting

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

Name In Type Required Description
scanId path string yes Head scan identifier
baseScanId query string no Base scan identifier
language query string no Language (default: dotnet)
includeFullPath query boolean no Include full path nodes in compressed paths

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: 404 Not Found

Returned if the scan or drift result is missing or if call graph snapshots are not available.


3.2 GET /drift/{driftId}/sinks

Returns drifted sinks for a drift result.

Parameters

Name In Type Required Description
driftId path uuid yes Drift result identifier
direction query string no became_reachable or became_unreachable
offset query integer no Offset (default: 0)
limit query integer no Page size (default: 100, max: 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": []
    }
  ]
}

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

Triggers reachability computation for a scan.

Parameters

Name In Type Required Description
scanId path string yes Scan 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
}

Response: 409 Conflict

Returned when computation is already in progress for the scan.


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

Lists components with reachability status.

Parameters

Name In Type Required Description
scanId path string yes Scan identifier
purl query string no Filter by PURL
status query string no Filter 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

Name In Type Required Description
scanId path string yes Scan identifier
cve query string no Filter by CVE
status query string no Filter 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

Name In Type Required Description
scanId path string yes Scan identifier
cve query string yes CVE identifier
purl query string yes Package 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:..."
}

4. Request and Response Models

Key models (JSON names shown):

  • ReachabilityDriftResult: id, baseScanId, headScanId, language, detectedAt, newlyReachable, newlyUnreachable, resultDigest, totalDriftCount, hasMaterialDrift.
  • DriftedSink: id, sinkNodeId, symbol, sinkCategory, direction, cause, path, associatedVulns.
  • DriftCause: kind, description, changedSymbol, changedFile, changedLine, codeChangeId.
  • CompressedPath: entrypoint, sink, intermediateCount, keyNodes, fullPath (optional).
  • PathNode: nodeId, symbol, file, line, package, isChanged, changeKind.
  • ComputeReachabilityRequestDto: forceRecompute, entrypoints, targets.
  • ComputeReachabilityResponseDto: jobId, status, estimatedDuration.

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
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. Common cases:

  • 400: invalid scan identifier, invalid direction, missing query parameters.
  • 404: scan not found, call graph snapshot missing, drift result not found.
  • 409: reachability computation already in progress.
  • 500: unexpected server error.

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

  • docs/modules/scanner/reachability-drift.md
  • docs/operations/reachability-drift-guide.md
  • src/Scanner/StellaOps.Scanner.WebService/Endpoints/ReachabilityDriftEndpoints.cs