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.mddocs/operations/reachability-drift-guide.mdsrc/Scanner/StellaOps.Scanner.WebService/Endpoints/ReachabilityDriftEndpoints.cs