feat: Implement distro-native version comparison for RPM, Debian, and Alpine packages
- Add RpmVersionComparer for RPM version comparison with epoch, version, and release handling. - Introduce DebianVersion for parsing Debian EVR (Epoch:Version-Release) strings. - Create ApkVersion for parsing Alpine APK version strings with suffix support. - Define IVersionComparator interface for version comparison with proof-line generation. - Implement VersionComparisonResult struct to encapsulate comparison results and proof lines. - Add tests for Debian and RPM version comparers to ensure correct functionality and edge case handling. - Create project files for the version comparison library and its tests.
This commit is contained in:
526
docs/api/scanner-drift-api.md
Normal file
526
docs/api/scanner-drift-api.md
Normal file
@@ -0,0 +1,526 @@
|
||||
# Scanner Drift API Reference
|
||||
|
||||
**Module:** Scanner
|
||||
**Version:** 1.0
|
||||
**Base Path:** `/api/scanner`
|
||||
**Last Updated:** 2025-12-22
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
The Scanner Drift API provides endpoints for computing and retrieving reachability drift analysis between scans. Drift detection identifies when code changes create new paths to vulnerable sinks or mitigate existing risks.
|
||||
|
||||
---
|
||||
|
||||
## 2. Authentication & Authorization
|
||||
|
||||
### Required Scopes
|
||||
|
||||
| Endpoint | Scope |
|
||||
|----------|-------|
|
||||
| Read drift results | `scanner:read` |
|
||||
| Compute reachability | `scanner:write` |
|
||||
| Admin operations | `scanner:admin` |
|
||||
|
||||
### Headers
|
||||
|
||||
```http
|
||||
Authorization: Bearer <access_token>
|
||||
X-Tenant-Id: <tenant_uuid>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Endpoints
|
||||
|
||||
### 3.1 GET /scans/{scanId}/drift
|
||||
|
||||
Retrieves drift analysis results comparing the specified scan against its base scan.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|-----|------|----------|-------------|
|
||||
| scanId | path | string | Yes | Head scan identifier |
|
||||
| baseScanId | query | string | No | Base scan ID (defaults to previous scan) |
|
||||
| language | query | string | No | Filter by language (dotnet, node, java, etc.) |
|
||||
|
||||
**Response: 200 OK**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"baseScanId": "abc123",
|
||||
"headScanId": "def456",
|
||||
"language": "dotnet",
|
||||
"detectedAt": "2025-12-22T10:30:00Z",
|
||||
"newlyReachableCount": 3,
|
||||
"newlyUnreachableCount": 1,
|
||||
"totalDriftCount": 4,
|
||||
"hasMaterialDrift": true,
|
||||
"resultDigest": "sha256:a1b2c3d4..."
|
||||
}
|
||||
```
|
||||
|
||||
**Response: 404 Not Found**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "DRIFT_NOT_FOUND",
|
||||
"message": "No drift analysis found for scan def456"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 GET /drift/{driftId}/sinks
|
||||
|
||||
Retrieves individual drifted sinks with pagination.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|-----|------|----------|-------------|
|
||||
| driftId | path | uuid | Yes | Drift result identifier |
|
||||
| direction | query | string | No | Filter: `became_reachable` or `became_unreachable` |
|
||||
| sinkCategory | query | string | No | Filter by sink category |
|
||||
| offset | query | int | No | Pagination offset (default: 0) |
|
||||
| limit | query | int | No | Page size (default: 100, max: 1000) |
|
||||
|
||||
**Response: 200 OK**
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"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
|
||||
},
|
||||
"sink": {
|
||||
"nodeId": "MyApp.Services.DbService.ExecuteQuery(string)",
|
||||
"symbol": "DbService.ExecuteQuery",
|
||||
"file": "src/Services/DbService.cs",
|
||||
"line": 88
|
||||
},
|
||||
"intermediateCount": 3,
|
||||
"keyNodes": [
|
||||
{
|
||||
"nodeId": "MyApp.Middleware.AuthMiddleware.Validate()",
|
||||
"symbol": "AuthMiddleware.Validate",
|
||||
"file": "src/Middleware/AuthMiddleware.cs",
|
||||
"line": 42,
|
||||
"isChanged": true,
|
||||
"changeKind": "guard_changed"
|
||||
}
|
||||
]
|
||||
},
|
||||
"associatedVulns": [
|
||||
{
|
||||
"cveId": "CVE-2024-12345",
|
||||
"epss": 0.85,
|
||||
"cvss": 9.8,
|
||||
"vexStatus": "affected",
|
||||
"packagePurl": "pkg:nuget/Dapper@2.0.123"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"totalCount": 3,
|
||||
"offset": 0,
|
||||
"limit": 100
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.3 POST /scans/{scanId}/compute-reachability
|
||||
|
||||
Triggers reachability computation for a scan. Idempotent - returns cached result if already computed.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|-----|------|----------|-------------|
|
||||
| scanId | path | string | Yes | Scan identifier |
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"languages": ["dotnet", "node"],
|
||||
"baseScanId": "abc123",
|
||||
"forceRecompute": false
|
||||
}
|
||||
```
|
||||
|
||||
**Response: 202 Accepted**
|
||||
|
||||
```json
|
||||
{
|
||||
"jobId": "880e8400-e29b-41d4-a716-446655440003",
|
||||
"status": "queued",
|
||||
"estimatedCompletionSeconds": 30
|
||||
}
|
||||
```
|
||||
|
||||
**Response: 200 OK** (cached result)
|
||||
|
||||
```json
|
||||
{
|
||||
"jobId": "880e8400-e29b-41d4-a716-446655440003",
|
||||
"status": "completed",
|
||||
"driftResultId": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 GET /scans/{scanId}/reachability/components
|
||||
|
||||
Lists components with their reachability status.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|-----|------|----------|-------------|
|
||||
| scanId | path | string | Yes | Scan identifier |
|
||||
| language | query | string | No | Filter by language |
|
||||
| reachable | query | bool | No | Filter by reachability |
|
||||
| offset | query | int | No | Pagination offset |
|
||||
| limit | query | int | No | Page size |
|
||||
|
||||
**Response: 200 OK**
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"purl": "pkg:nuget/Newtonsoft.Json@13.0.1",
|
||||
"language": "dotnet",
|
||||
"reachableSinkCount": 2,
|
||||
"unreachableSinkCount": 5,
|
||||
"totalSinkCount": 7,
|
||||
"highestSeveritySink": "unsafe_deser",
|
||||
"reachabilityGate": 5
|
||||
}
|
||||
],
|
||||
"totalCount": 42,
|
||||
"offset": 0,
|
||||
"limit": 100
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.5 GET /scans/{scanId}/reachability/findings
|
||||
|
||||
Lists reachable vulnerable sinks with CVE associations.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|-----|------|----------|-------------|
|
||||
| scanId | path | string | Yes | Scan identifier |
|
||||
| minCvss | query | float | No | Minimum CVSS score |
|
||||
| kevOnly | query | bool | No | Only KEV vulnerabilities |
|
||||
| offset | query | int | No | Pagination offset |
|
||||
| limit | query | int | No | Page size |
|
||||
|
||||
**Response: 200 OK**
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"sinkNodeId": "MyApp.Services.CryptoService.Encrypt(string)",
|
||||
"symbol": "CryptoService.Encrypt",
|
||||
"sinkCategory": "crypto_weak",
|
||||
"isReachable": true,
|
||||
"shortestPathLength": 4,
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cveId": "CVE-2024-54321",
|
||||
"cvss": 7.5,
|
||||
"epss": 0.42,
|
||||
"isKev": false,
|
||||
"vexStatus": "affected"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"totalCount": 15,
|
||||
"offset": 0,
|
||||
"limit": 100
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.6 GET /scans/{scanId}/reachability/explain
|
||||
|
||||
Explains why a specific sink is reachable or unreachable.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|-----|------|----------|-------------|
|
||||
| scanId | path | string | Yes | Scan identifier |
|
||||
| sinkNodeId | query | string | Yes | Sink node identifier |
|
||||
| includeFullPath | query | bool | No | Include full path (default: false) |
|
||||
|
||||
**Response: 200 OK**
|
||||
|
||||
```json
|
||||
{
|
||||
"sinkNodeId": "MyApp.Services.DbService.ExecuteQuery(string)",
|
||||
"isReachable": true,
|
||||
"reachabilityGate": 6,
|
||||
"confidence": "confirmed",
|
||||
"explanation": "Sink is reachable from 2 HTTP entrypoints via direct call paths",
|
||||
"entrypoints": [
|
||||
{
|
||||
"nodeId": "MyApp.Controllers.UserController.GetUser(int)",
|
||||
"entrypointType": "http_handler",
|
||||
"pathLength": 4
|
||||
},
|
||||
{
|
||||
"nodeId": "MyApp.Controllers.AdminController.Query(string)",
|
||||
"entrypointType": "http_handler",
|
||||
"pathLength": 2
|
||||
}
|
||||
],
|
||||
"shortestPath": {
|
||||
"entrypoint": {...},
|
||||
"sink": {...},
|
||||
"intermediateCount": 1,
|
||||
"keyNodes": [...]
|
||||
},
|
||||
"fullPath": ["node1", "node2", "node3", "sink"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Request/Response Models
|
||||
|
||||
### 4.1 DriftDirection
|
||||
|
||||
```typescript
|
||||
enum DriftDirection {
|
||||
became_reachable = "became_reachable",
|
||||
became_unreachable = "became_unreachable"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 DriftCauseKind
|
||||
|
||||
```typescript
|
||||
enum DriftCauseKind {
|
||||
guard_removed = "guard_removed",
|
||||
guard_added = "guard_added",
|
||||
new_public_route = "new_public_route",
|
||||
visibility_escalated = "visibility_escalated",
|
||||
dependency_upgraded = "dependency_upgraded",
|
||||
symbol_removed = "symbol_removed",
|
||||
unknown = "unknown"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 SinkCategory
|
||||
|
||||
```typescript
|
||||
enum SinkCategory {
|
||||
cmd_exec = "cmd_exec",
|
||||
unsafe_deser = "unsafe_deser",
|
||||
sql_raw = "sql_raw",
|
||||
ssrf = "ssrf",
|
||||
file_write = "file_write",
|
||||
path_traversal = "path_traversal",
|
||||
template_injection = "template_injection",
|
||||
crypto_weak = "crypto_weak",
|
||||
authz_bypass = "authz_bypass",
|
||||
ldap_injection = "ldap_injection",
|
||||
xpath_injection = "xpath_injection",
|
||||
xxe_injection = "xxe_injection",
|
||||
code_injection = "code_injection",
|
||||
log_injection = "log_injection",
|
||||
reflection = "reflection",
|
||||
open_redirect = "open_redirect"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 CodeChangeKind
|
||||
|
||||
```typescript
|
||||
enum CodeChangeKind {
|
||||
added = "added",
|
||||
removed = "removed",
|
||||
signature_changed = "signature_changed",
|
||||
guard_changed = "guard_changed",
|
||||
dependency_changed = "dependency_changed",
|
||||
visibility_changed = "visibility_changed"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Error Codes
|
||||
|
||||
| Code | HTTP Status | Description |
|
||||
|------|-------------|-------------|
|
||||
| `SCAN_NOT_FOUND` | 404 | Scan ID does not exist |
|
||||
| `DRIFT_NOT_FOUND` | 404 | No drift analysis for this scan |
|
||||
| `GRAPH_NOT_EXTRACTED` | 400 | Call graph not yet extracted |
|
||||
| `LANGUAGE_NOT_SUPPORTED` | 400 | Language not supported for reachability |
|
||||
| `COMPUTATION_IN_PROGRESS` | 409 | Reachability computation already running |
|
||||
| `COMPUTATION_FAILED` | 500 | Reachability computation failed |
|
||||
| `INVALID_SINK_ID` | 400 | Sink node ID not found in graph |
|
||||
|
||||
---
|
||||
|
||||
## 6. Rate Limiting
|
||||
|
||||
| Endpoint | Rate Limit |
|
||||
|----------|------------|
|
||||
| GET endpoints | 100/min |
|
||||
| POST compute | 10/min |
|
||||
|
||||
Rate limit headers:
|
||||
```http
|
||||
X-RateLimit-Limit: 100
|
||||
X-RateLimit-Remaining: 95
|
||||
X-RateLimit-Reset: 1703242800
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Examples
|
||||
|
||||
### 7.1 cURL - Get Drift Results
|
||||
|
||||
```bash
|
||||
curl -X GET \
|
||||
'https://api.stellaops.example/api/scanner/scans/def456/drift?language=dotnet' \
|
||||
-H 'Authorization: Bearer <token>' \
|
||||
-H 'X-Tenant-Id: <tenant_id>'
|
||||
```
|
||||
|
||||
### 7.2 cURL - Compute Reachability
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
'https://api.stellaops.example/api/scanner/scans/def456/compute-reachability' \
|
||||
-H 'Authorization: Bearer <token>' \
|
||||
-H 'X-Tenant-Id: <tenant_id>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"languages": ["dotnet"],
|
||||
"baseScanId": "abc123"
|
||||
}'
|
||||
```
|
||||
|
||||
### 7.3 C# SDK
|
||||
|
||||
```csharp
|
||||
var client = new ScannerClient(options);
|
||||
|
||||
// Get drift results
|
||||
var drift = await client.GetDriftAsync("def456", language: "dotnet");
|
||||
Console.WriteLine($"Newly reachable: {drift.NewlyReachableCount}");
|
||||
|
||||
// Get drifted sinks
|
||||
var sinks = await client.GetDriftedSinksAsync(drift.Id,
|
||||
direction: DriftDirection.BecameReachable);
|
||||
|
||||
foreach (var sink in sinks.Items)
|
||||
{
|
||||
Console.WriteLine($"{sink.Symbol}: {sink.Cause.Description}");
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 TypeScript SDK
|
||||
|
||||
```typescript
|
||||
import { ScannerClient } from '@stellaops/sdk';
|
||||
|
||||
const client = new ScannerClient({ baseUrl, token });
|
||||
|
||||
// Get drift results
|
||||
const drift = await client.getDrift('def456', { language: 'dotnet' });
|
||||
console.log(`Newly reachable: ${drift.newlyReachableCount}`);
|
||||
|
||||
// Explain a sink
|
||||
const explanation = await client.explainReachability('def456', {
|
||||
sinkNodeId: 'MyApp.Services.DbService.ExecuteQuery(string)',
|
||||
includeFullPath: true
|
||||
});
|
||||
|
||||
console.log(explanation.explanation);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Webhooks
|
||||
|
||||
### 8.1 drift.computed
|
||||
|
||||
Fired when drift analysis completes.
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "drift.computed",
|
||||
"timestamp": "2025-12-22T10:30:00Z",
|
||||
"data": {
|
||||
"driftResultId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"scanId": "def456",
|
||||
"baseScanId": "abc123",
|
||||
"newlyReachableCount": 3,
|
||||
"newlyUnreachableCount": 1,
|
||||
"hasMaterialDrift": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 drift.kev_reachable
|
||||
|
||||
Fired when a KEV becomes reachable.
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "drift.kev_reachable",
|
||||
"timestamp": "2025-12-22T10:30:00Z",
|
||||
"severity": "critical",
|
||||
"data": {
|
||||
"driftResultId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"scanId": "def456",
|
||||
"kevCveId": "CVE-2024-12345",
|
||||
"sinkNodeId": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. References
|
||||
|
||||
- **Architecture:** `docs/modules/scanner/reachability-drift.md`
|
||||
- **Operations:** `docs/operations/reachability-drift-guide.md`
|
||||
- **Source:** `src/Scanner/StellaOps.Scanner.WebService/Endpoints/ReachabilityDriftEndpoints.cs`
|
||||
Reference in New Issue
Block a user