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

445 lines
11 KiB
Markdown

# 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 <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**
```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 <token>'
```
### 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 <token>'
```
### 7.3 cURL - Compute Reachability
```bash
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`