# 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 ```http Authorization: Bearer X-Tenant-Id: # 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** ```json { "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** ```json { "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** ```json { "forceRecompute": false, "entrypoints": ["MyApp.Controllers.UserController.GetUser"], "targets": ["pkg:nuget/Dapper@2.0.123"] } ``` **Response: 202 Accepted** ```json { "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** ```json { "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** ```json { "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** ```json { "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 ```text became_reachable became_unreachable ``` ### DriftCauseKind ```text guard_removed guard_added new_public_route visibility_escalated dependency_upgraded symbol_removed unknown ``` ### CodeChangeKind ```text added removed signature_changed guard_changed dependency_changed visibility_changed ``` ### SinkCategory ```text 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 ```bash curl -X GET \ 'https://scanner.example/api/v1/scans/head456/drift?baseScanId=base123&language=dotnet' \ -H 'Authorization: Bearer ' ``` ### 7.2 cURL - List Drifted Sinks ```bash 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 ' ``` ### 7.3 cURL - Compute Reachability ```bash curl -X POST \ 'https://scanner.example/api/v1/scans/head456/compute-reachability' \ -H 'Authorization: Bearer ' \ -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`