Files
git.stella-ops.org/docs/api/scanner-drift-api.md
StellaOps Bot 634233dfed 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.
2025-12-22 09:50:12 +02:00

12 KiB

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

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

{
  "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

{
  "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

{
  "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:

{
  "languages": ["dotnet", "node"],
  "baseScanId": "abc123",
  "forceRecompute": false
}

Response: 202 Accepted

{
  "jobId": "880e8400-e29b-41d4-a716-446655440003",
  "status": "queued",
  "estimatedCompletionSeconds": 30
}

Response: 200 OK (cached result)

{
  "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

{
  "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

{
  "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

{
  "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

enum DriftDirection {
  became_reachable = "became_reachable",
  became_unreachable = "became_unreachable"
}

4.2 DriftCauseKind

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

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

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:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1703242800

7. Examples

7.1 cURL - Get Drift Results

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

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

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

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.

{
  "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.

{
  "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