- Add RateLimitConfig for configuration management with YAML binding support. - Introduce RateLimitDecision to encapsulate the result of rate limit checks. - Implement RateLimitMetrics for OpenTelemetry metrics tracking. - Create RateLimitMiddleware for enforcing rate limits on incoming requests. - Develop RateLimitService to orchestrate instance and environment rate limit checks. - Add RateLimitServiceCollectionExtensions for dependency injection registration.
16 KiB
Scanner WebService API — Score Proofs & Reachability Extensions
Version: 2.0
Base URL: /api/v1/scanner
Authentication: Bearer token (OpTok with DPoP/mTLS)
Sprint: SPRINT_3500_0002_0003, SPRINT_3500_0003_0003
Overview
This document specifies API extensions to Scanner.WebService for:
- Scan manifests and deterministic replay
- Proof bundles (score proofs + reachability evidence)
- Call-graph ingestion and reachability analysis
- Unknowns management
Design Principles:
- All endpoints return canonical JSON (deterministic serialization)
- Idempotency via
Content-Digestheaders (SHA-256) - DSSE signatures returned for all proof artifacts
- Offline-first (bundles downloadable for air-gap verification)
Endpoints
1. Create Scan with Manifest
POST /api/v1/scanner/scans
Description: Creates a new scan with deterministic manifest.
Request Body:
{
"artifactDigest": "sha256:abc123...",
"artifactPurl": "pkg:oci/myapp@sha256:abc123...",
"scannerVersion": "1.0.0",
"workerVersion": "1.0.0",
"concelierSnapshotHash": "sha256:feed123...",
"excititorSnapshotHash": "sha256:vex456...",
"latticePolicyHash": "sha256:policy789...",
"deterministic": true,
"seed": "AQIDBA==", // base64-encoded 32 bytes
"knobs": {
"maxDepth": "10",
"indirectCallResolution": "conservative"
}
}
Response (201 Created):
{
"scanId": "550e8400-e29b-41d4-a716-446655440000",
"manifestHash": "sha256:manifest123...",
"createdAt": "2025-12-17T12:00:00Z",
"_links": {
"self": "/api/v1/scanner/scans/550e8400-e29b-41d4-a716-446655440000",
"manifest": "/api/v1/scanner/scans/550e8400-e29b-41d4-a716-446655440000/manifest"
}
}
Headers:
Content-Digest:sha256=<base64-hash>(idempotency key)Location:/api/v1/scanner/scans/{scanId}
Errors:
400 Bad Request— Invalid manifest (missing required fields)409 Conflict— Scan with samemanifestHashalready exists422 Unprocessable Entity— Snapshot hashes not found in Concelier/Excititor
Idempotency: Requests with same Content-Digest return existing scan (no duplicate creation).
2. Retrieve Scan Manifest
GET /api/v1/scanner/scans/{scanId}/manifest
Description: Retrieves the canonical JSON manifest with DSSE signature.
Response (200 OK):
{
"manifest": {
"scanId": "550e8400-e29b-41d4-a716-446655440000",
"createdAtUtc": "2025-12-17T12:00:00Z",
"artifactDigest": "sha256:abc123...",
"artifactPurl": "pkg:oci/myapp@sha256:abc123...",
"scannerVersion": "1.0.0",
"workerVersion": "1.0.0",
"concelierSnapshotHash": "sha256:feed123...",
"excititorSnapshotHash": "sha256:vex456...",
"latticePolicyHash": "sha256:policy789...",
"deterministic": true,
"seed": "AQIDBA==",
"knobs": {
"maxDepth": "10"
}
},
"manifestHash": "sha256:manifest123...",
"dsseEnvelope": {
"payloadType": "application/vnd.stellaops.scan-manifest.v1+json",
"payload": "eyJzY2FuSWQiOiIuLi4ifQ==", // base64 canonical JSON
"signatures": [
{
"keyid": "ecdsa-p256-key-001",
"sig": "MEUCIQDx..."
}
]
}
}
Headers:
Content-Type:application/jsonETag:"<manifestHash>"
Errors:
404 Not Found— Scan ID not found
Caching: ETag supports conditional If-None-Match requests (304 Not Modified).
3. Replay Score Computation
POST /api/v1/scanner/scans/{scanId}/score/replay
Description: Recomputes score proofs from manifest without rescanning binaries. Used when feeds/policies change.
Request Body:
{
"overrides": {
"concelierSnapshotHash": "sha256:newfeed...", // Optional: use different feed
"excititorSnapshotHash": "sha256:newvex...", // Optional: use different VEX
"latticePolicyHash": "sha256:newpolicy..." // Optional: use different policy
}
}
Response (200 OK):
{
"scanId": "550e8400-e29b-41d4-a716-446655440000",
"replayedAt": "2025-12-17T13:00:00Z",
"scoreProof": {
"rootHash": "sha256:proof123...",
"nodes": [
{
"id": "input-1",
"kind": "Input",
"ruleId": "inputs.v1",
"delta": 0.0,
"total": 0.0,
"nodeHash": "sha256:node1..."
},
{
"id": "delta-cvss",
"kind": "Delta",
"ruleId": "score.cvss_base.weighted",
"parentIds": ["input-1"],
"evidenceRefs": ["cvss:9.1"],
"delta": 0.50,
"total": 0.50,
"nodeHash": "sha256:node2..."
}
]
},
"proofBundleUri": "/api/v1/scanner/scans/550e8400-e29b-41d4-a716-446655440000/proofs/sha256:proof123...",
"_links": {
"bundle": "/api/v1/scanner/scans/550e8400-e29b-41d4-a716-446655440000/proofs/sha256:proof123..."
}
}
Errors:
404 Not Found— Scan ID not found422 Unprocessable Entity— Override snapshot not found
Use Case: Nightly rescore job when Concelier publishes new advisory snapshot.
4. Upload Call-Graph
POST /api/v1/scanner/scans/{scanId}/callgraphs
Description: Uploads call-graph extracted by language-specific workers (.NET, Java, etc.).
Request Body (application/json):
{
"schema": "stella.callgraph.v1",
"language": "dotnet",
"artifacts": [
{
"artifactKey": "MyApp.WebApi.dll",
"kind": "assembly",
"sha256": "sha256:artifact123..."
}
],
"nodes": [
{
"nodeId": "sha256:node1...",
"artifactKey": "MyApp.WebApi.dll",
"symbolKey": "MyApp.Controllers.OrdersController::Get(System.Guid)",
"visibility": "public",
"isEntrypointCandidate": true
}
],
"edges": [
{
"from": "sha256:node1...",
"to": "sha256:node2...",
"kind": "static",
"reason": "direct_call",
"weight": 1.0
}
],
"entrypoints": [
{
"nodeId": "sha256:node1...",
"kind": "http",
"route": "/api/orders/{id}",
"framework": "aspnetcore"
}
]
}
Headers:
Content-Digest:sha256=<hash>(idempotency)
Response (202 Accepted):
{
"scanId": "550e8400-e29b-41d4-a716-446655440000",
"callGraphDigest": "sha256:cg123...",
"nodesCount": 1234,
"edgesCount": 5678,
"entrypointsCount": 12,
"status": "accepted",
"_links": {
"reachability": "/api/v1/scanner/scans/550e8400-e29b-41d4-a716-446655440000/reachability/compute"
}
}
Errors:
400 Bad Request— Invalid call-graph schema404 Not Found— Scan ID not found413 Payload Too Large— Call-graph >100MB
Idempotency: Same Content-Digest → returns existing call-graph.
5. Compute Reachability
POST /api/v1/scanner/scans/{scanId}/reachability/compute
Description: Triggers reachability analysis for uploaded call-graph + SBOM + vulnerabilities.
Request Body: Empty (uses existing scan data)
Response (202 Accepted):
{
"scanId": "550e8400-e29b-41d4-a716-446655440000",
"jobId": "reachability-job-001",
"status": "queued",
"estimatedDuration": "30s",
"_links": {
"status": "/api/v1/scanner/jobs/reachability-job-001",
"results": "/api/v1/scanner/scans/550e8400-e29b-41d4-a716-446655440000/reachability/findings"
}
}
Polling: Use GET /api/v1/scanner/jobs/{jobId} to check status.
Errors:
404 Not Found— Scan ID not found422 Unprocessable Entity— Call-graph not uploaded yet
6. Get Reachability Findings
GET /api/v1/scanner/scans/{scanId}/reachability/findings
Description: Retrieves reachability verdicts for all vulnerabilities.
Query Parameters:
status(optional): Filter byREACHABLE,UNREACHABLE,POSSIBLY_REACHABLE,UNKNOWNcveId(optional): Filter by CVE ID
Response (200 OK):
{
"scanId": "550e8400-e29b-41d4-a716-446655440000",
"computedAt": "2025-12-17T12:30:00Z",
"findings": [
{
"cveId": "CVE-2024-1234",
"purl": "pkg:npm/lodash@4.17.20",
"status": "REACHABLE_STATIC",
"confidence": 0.70,
"path": [
{
"nodeId": "sha256:entrypoint...",
"symbolKey": "MyApp.Controllers.OrdersController::Get(System.Guid)"
},
{
"nodeId": "sha256:intermediate...",
"symbolKey": "MyApp.Services.OrderService::Process(Order)"
},
{
"nodeId": "sha256:vuln...",
"symbolKey": "Lodash.merge(Object, Object)"
}
],
"evidence": {
"pathLength": 3,
"staticEdgesOnly": true,
"runtimeConfirmed": false
},
"_links": {
"explain": "/api/v1/scanner/scans/{scanId}/reachability/explain?cve=CVE-2024-1234&purl=pkg:npm/lodash@4.17.20"
}
}
],
"summary": {
"total": 45,
"reachable": 3,
"unreachable": 38,
"possiblyReachable": 4,
"unknown": 0
}
}
Errors:
404 Not Found— Scan ID not found or reachability not computed
7. Explain Reachability
GET /api/v1/scanner/scans/{scanId}/reachability/explain
Description: Provides detailed explanation for a reachability verdict.
Query Parameters:
cve(required): CVE IDpurl(required): Package URL
Response (200 OK):
{
"cveId": "CVE-2024-1234",
"purl": "pkg:npm/lodash@4.17.20",
"status": "REACHABLE_STATIC",
"confidence": 0.70,
"explanation": {
"shortestPath": [
{
"depth": 0,
"nodeId": "sha256:entry...",
"symbolKey": "MyApp.Controllers.OrdersController::Get(System.Guid)",
"entrypointKind": "http",
"route": "/api/orders/{id}"
},
{
"depth": 1,
"nodeId": "sha256:inter...",
"symbolKey": "MyApp.Services.OrderService::Process(Order)",
"edgeKind": "static",
"edgeReason": "direct_call"
},
{
"depth": 2,
"nodeId": "sha256:vuln...",
"symbolKey": "Lodash.merge(Object, Object)",
"edgeKind": "static",
"edgeReason": "direct_call",
"vulnerableFunction": true
}
],
"whyReachable": [
"Static call path exists from HTTP entrypoint /api/orders/{id}",
"All edges are statically proven (no heuristics)",
"Vulnerable function Lodash.merge() is directly invoked"
],
"confidenceFactors": {
"staticPathExists": 0.50,
"noHeuristicEdges": 0.20,
"runtimeConfirmed": 0.00
}
},
"alternativePaths": 2, // Number of other paths found
"_links": {
"callGraph": "/api/v1/scanner/scans/{scanId}/callgraphs/sha256:cg123.../graph.json"
}
}
Errors:
404 Not Found— Scan, CVE, or PURL not found
8. Fetch Proof Bundle
GET /api/v1/scanner/scans/{scanId}/proofs/{rootHash}
Description: Downloads proof bundle zip archive for offline verification.
Path Parameters:
rootHash: Proof root hash (e.g.,sha256:proof123...)
Response (200 OK):
Headers:
Content-Type:application/zipContent-Disposition:attachment; filename="proof-{scanId}-{rootHash}.zip"X-Proof-Root-Hash:{rootHash}X-Manifest-Hash:{manifestHash}
Body: Binary zip archive containing:
manifest.json— Canonical scan manifestmanifest.dsse.json— DSSE signature of manifestscore_proof.json— Proof ledger (array of ProofNodes)proof_root.dsse.json— DSSE signature of proof rootmeta.json— Metadata (created timestamp, etc.)
Errors:
404 Not Found— Scan or proof root hash not found
Use Case: Air-gap verification (stella proof verify --bundle proof.zip).
9. List Unknowns
GET /api/v1/scanner/unknowns
Description: Lists unknowns (missing evidence) ranked by priority.
Query Parameters:
band(optional): Filter byHOT,WARM,COLDlimit(optional): Max results (default: 100, max: 1000)offset(optional): Pagination offset
Response (200 OK):
{
"unknowns": [
{
"unknownId": "unk-001",
"pkgId": "pkg:npm/lodash",
"pkgVersion": "4.17.20",
"digestAnchor": "sha256:...",
"reasons": ["missing_vex", "ambiguous_version"],
"score": 0.72,
"band": "HOT",
"popularity": 0.85,
"potentialExploit": 0.60,
"uncertainty": 0.75,
"evidence": {
"deployments": 42,
"epss": 0.58,
"kev": false
},
"createdAt": "2025-12-15T10:00:00Z",
"_links": {
"escalate": "/api/v1/scanner/unknowns/unk-001/escalate"
}
}
],
"pagination": {
"total": 156,
"limit": 100,
"offset": 0,
"next": "/api/v1/scanner/unknowns?band=HOT&limit=100&offset=100"
}
}
Errors:
400 Bad Request— Invalid band value
10. Escalate Unknown to Rescan
POST /api/v1/scanner/unknowns/{unknownId}/escalate
Description: Escalates an unknown to trigger immediate rescan/re-analysis.
Request Body: Empty
Response (202 Accepted):
{
"unknownId": "unk-001",
"escalatedAt": "2025-12-17T12:00:00Z",
"rescanJobId": "rescan-job-001",
"status": "queued",
"_links": {
"job": "/api/v1/scanner/jobs/rescan-job-001"
}
}
Errors:
404 Not Found— Unknown ID not found409 Conflict— Unknown already escalated (rescan in progress)
Data Models
ScanManifest
See src/__Libraries/StellaOps.Scanner.Core/Models/ScanManifest.cs for full definition.
ProofNode
interface ProofNode {
id: string;
kind: "Input" | "Transform" | "Delta" | "Score";
ruleId: string;
parentIds: string[];
evidenceRefs: string[];
delta: number;
total: number;
actor: string;
tsUtc: string; // ISO 8601
seed: string; // base64
nodeHash: string; // sha256:...
}
DsseEnvelope
interface DsseEnvelope {
payloadType: string;
payload: string; // base64 canonical JSON
signatures: DsseSignature[];
}
interface DsseSignature {
keyid: string;
sig: string; // base64
}
ReachabilityStatus
enum ReachabilityStatus {
UNREACHABLE = "UNREACHABLE",
POSSIBLY_REACHABLE = "POSSIBLY_REACHABLE",
REACHABLE_STATIC = "REACHABLE_STATIC",
REACHABLE_PROVEN = "REACHABLE_PROVEN",
UNKNOWN = "UNKNOWN"
}
Error Responses
All errors follow RFC 7807 (Problem Details):
{
"type": "https://stella-ops.org/errors/scan-not-found",
"title": "Scan Not Found",
"status": 404,
"detail": "Scan ID '550e8400-e29b-41d4-a716-446655440000' does not exist.",
"instance": "/api/v1/scanner/scans/550e8400-e29b-41d4-a716-446655440000",
"traceId": "trace-001"
}
Error Types
| Type | Status | Description |
|---|---|---|
scan-not-found |
404 | Scan ID not found |
invalid-manifest |
400 | Manifest validation failed |
duplicate-scan |
409 | Scan with same manifest hash exists |
snapshot-not-found |
422 | Concelier/Excititor snapshot not found |
callgraph-not-uploaded |
422 | Call-graph required before reachability |
payload-too-large |
413 | Request body exceeds size limit |
proof-not-found |
404 | Proof root hash not found |
unknown-not-found |
404 | Unknown ID not found |
escalation-conflict |
409 | Unknown already escalated |
Rate Limiting
Limits:
POST /scans: 100 requests/hour per tenantPOST /scans/{id}/score/replay: 1000 requests/hour per tenantPOST /callgraphs: 100 requests/hour per tenantPOST /reachability/compute: 100 requests/hour per tenantGETendpoints: 10,000 requests/hour per tenant
Headers:
X-RateLimit-Limit: Maximum requests per windowX-RateLimit-Remaining: Remaining requestsX-RateLimit-Reset: Unix timestamp when limit resets
Error (429 Too Many Requests):
{
"type": "https://stella-ops.org/errors/rate-limit-exceeded",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "Exceeded 100 requests/hour for POST /scans. Retry after 1234567890.",
"retryAfter": 1234567890
}
Webhooks (Future)
Planned for Sprint 3500.0004.0003:
POST /api/v1/scanner/webhooks
Register webhook for events: scan.completed, reachability.computed, unknown.escalated
OpenAPI Specification
File: src/Api/StellaOps.Api.OpenApi/scanner/openapi.yaml
Update with new endpoints (Sprint 3500.0002.0003).
References
SPRINT_3500_0002_0001_score_proofs_foundations.md— Implementation sprintSPRINT_3500_0002_0003_proof_replay_api.md— API implementation sprintSPRINT_3500_0003_0003_graph_attestations_rekor.md— Reachability API sprintdocs/07_HIGH_LEVEL_ARCHITECTURE.md— API contracts sectiondocs/db/schemas/scanner_schema_specification.md— Database schema
Last Updated: 2025-12-17 API Version: 2.0 Next Review: Sprint 3500.0004.0001 (CLI integration)