445 lines
11 KiB
Markdown
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`
|