feat(rate-limiting): Implement core rate limiting functionality with configuration, decision-making, metrics, middleware, and service registration

- 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.
This commit is contained in:
master
2025-12-17 18:02:37 +02:00
parent 394b57f6bf
commit 8bbfe4d2d2
211 changed files with 47179 additions and 1590 deletions

View File

@@ -0,0 +1,622 @@
openapi: 3.1.0
info:
title: StellaOps Proof Chain API
version: 1.0.0
description: |
API for proof chain operations including proof spine creation, verification receipts,
VEX attestations, and trust anchor management.
The proof chain provides cryptographic evidence linking SBOM entries to vulnerability
assessments through attestable DSSE envelopes.
license:
name: AGPL-3.0-or-later
url: https://www.gnu.org/licenses/agpl-3.0.html
servers:
- url: https://api.stellaops.dev/v1
description: Production API
- url: http://localhost:5000/v1
description: Local development
tags:
- name: Proofs
description: Proof spine and receipt operations
- name: Anchors
description: Trust anchor management
- name: Verify
description: Proof verification endpoints
paths:
/proofs/{entry}/spine:
post:
operationId: createProofSpine
summary: Create proof spine for SBOM entry
description: |
Assembles a merkle-rooted proof spine from evidence, reasoning, and VEX verdict
for an SBOM entry. Returns a content-addressed proof bundle ID.
tags: [Proofs]
security:
- bearerAuth: []
- mtls: []
parameters:
- name: entry
in: path
required: true
schema:
type: string
pattern: '^sha256:[a-f0-9]{64}:pkg:.+'
description: SBOMEntryID in format sha256:<hash>:pkg:<purl>
example: "sha256:abc123...def:pkg:npm/lodash@4.17.21"
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateSpineRequest'
responses:
'201':
description: Proof spine created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/CreateSpineResponse'
'400':
$ref: '#/components/responses/BadRequest'
'404':
$ref: '#/components/responses/NotFound'
'422':
$ref: '#/components/responses/ValidationError'
get:
operationId: getProofSpine
summary: Get proof spine for SBOM entry
description: Retrieves the existing proof spine for an SBOM entry.
tags: [Proofs]
security:
- bearerAuth: []
parameters:
- name: entry
in: path
required: true
schema:
type: string
pattern: '^sha256:[a-f0-9]{64}:pkg:.+'
description: SBOMEntryID
responses:
'200':
description: Proof spine retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/ProofSpineDto'
'404':
$ref: '#/components/responses/NotFound'
/proofs/{entry}/receipt:
get:
operationId: getProofReceipt
summary: Get verification receipt
description: |
Retrieves a verification receipt for the SBOM entry's proof spine.
The receipt includes merkle proof paths and signature verification status.
tags: [Proofs]
security:
- bearerAuth: []
parameters:
- name: entry
in: path
required: true
schema:
type: string
pattern: '^sha256:[a-f0-9]{64}:pkg:.+'
description: SBOMEntryID
responses:
'200':
description: Verification receipt
content:
application/json:
schema:
$ref: '#/components/schemas/VerificationReceiptDto'
'404':
$ref: '#/components/responses/NotFound'
/proofs/{entry}/vex:
get:
operationId: getProofVex
summary: Get VEX attestation for entry
description: Retrieves the VEX verdict attestation for the SBOM entry.
tags: [Proofs]
security:
- bearerAuth: []
parameters:
- name: entry
in: path
required: true
schema:
type: string
pattern: '^sha256:[a-f0-9]{64}:pkg:.+'
description: SBOMEntryID
responses:
'200':
description: VEX attestation
content:
application/json:
schema:
$ref: '#/components/schemas/VexAttestationDto'
'404':
$ref: '#/components/responses/NotFound'
/anchors:
get:
operationId: listAnchors
summary: List trust anchors
description: Lists all configured trust anchors with their status.
tags: [Anchors]
security:
- bearerAuth: []
responses:
'200':
description: List of trust anchors
content:
application/json:
schema:
type: object
properties:
anchors:
type: array
items:
$ref: '#/components/schemas/TrustAnchorDto'
post:
operationId: createAnchor
summary: Create trust anchor
description: Creates a new trust anchor with the specified public key.
tags: [Anchors]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateAnchorRequest'
responses:
'201':
description: Trust anchor created
content:
application/json:
schema:
$ref: '#/components/schemas/TrustAnchorDto'
'400':
$ref: '#/components/responses/BadRequest'
'409':
description: Anchor already exists
/anchors/{anchorId}:
get:
operationId: getAnchor
summary: Get trust anchor
description: Retrieves a specific trust anchor by ID.
tags: [Anchors]
security:
- bearerAuth: []
parameters:
- name: anchorId
in: path
required: true
schema:
type: string
description: Trust anchor ID
responses:
'200':
description: Trust anchor details
content:
application/json:
schema:
$ref: '#/components/schemas/TrustAnchorDto'
'404':
$ref: '#/components/responses/NotFound'
delete:
operationId: deleteAnchor
summary: Delete trust anchor
description: Deletes a trust anchor (soft delete, marks as revoked).
tags: [Anchors]
security:
- bearerAuth: []
parameters:
- name: anchorId
in: path
required: true
schema:
type: string
description: Trust anchor ID
responses:
'204':
description: Anchor deleted
'404':
$ref: '#/components/responses/NotFound'
/verify:
post:
operationId: verifyProofBundle
summary: Verify proof bundle
description: |
Performs full verification of a proof bundle including:
- DSSE signature verification
- Content-addressed ID recomputation
- Merkle path verification
- Optional Rekor inclusion proof verification
tags: [Verify]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/VerifyRequest'
responses:
'200':
description: Verification result
content:
application/json:
schema:
$ref: '#/components/schemas/VerificationResultDto'
'400':
$ref: '#/components/responses/BadRequest'
/verify/batch:
post:
operationId: verifyBatch
summary: Verify multiple proof bundles
description: Performs batch verification of multiple proof bundles.
tags: [Verify]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- bundles
properties:
bundles:
type: array
items:
$ref: '#/components/schemas/VerifyRequest'
maxItems: 100
responses:
'200':
description: Batch verification results
content:
application/json:
schema:
type: object
properties:
results:
type: array
items:
$ref: '#/components/schemas/VerificationResultDto'
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: Authority-issued OpToken
mtls:
type: mutualTLS
description: Mutual TLS with client certificate
schemas:
CreateSpineRequest:
type: object
required:
- evidenceIds
- reasoningId
- vexVerdictId
- policyVersion
properties:
evidenceIds:
type: array
description: Content-addressed IDs of evidence statements
items:
type: string
pattern: '^sha256:[a-f0-9]{64}$'
minItems: 1
example: ["sha256:e7f8a9b0c1d2..."]
reasoningId:
type: string
pattern: '^sha256:[a-f0-9]{64}$'
description: Content-addressed ID of reasoning statement
example: "sha256:f0e1d2c3b4a5..."
vexVerdictId:
type: string
pattern: '^sha256:[a-f0-9]{64}$'
description: Content-addressed ID of VEX verdict statement
example: "sha256:d4c5b6a7e8f9..."
policyVersion:
type: string
pattern: '^v[0-9]+\.[0-9]+\.[0-9]+$'
description: Version of the policy used
example: "v1.2.3"
CreateSpineResponse:
type: object
required:
- proofBundleId
properties:
proofBundleId:
type: string
pattern: '^sha256:[a-f0-9]{64}$'
description: Content-addressed ID of the created proof bundle (merkle root)
example: "sha256:1a2b3c4d5e6f..."
receiptUrl:
type: string
format: uri
description: URL to retrieve the verification receipt
example: "/proofs/sha256:abc:pkg:npm/lodash@4.17.21/receipt"
ProofSpineDto:
type: object
required:
- sbomEntryId
- proofBundleId
- evidenceIds
- reasoningId
- vexVerdictId
- policyVersion
- createdAt
properties:
sbomEntryId:
type: string
description: The SBOM entry this spine covers
proofBundleId:
type: string
description: Merkle root hash of the proof bundle
evidenceIds:
type: array
items:
type: string
description: Sorted list of evidence IDs
reasoningId:
type: string
description: Reasoning statement ID
vexVerdictId:
type: string
description: VEX verdict statement ID
policyVersion:
type: string
description: Policy version used
createdAt:
type: string
format: date-time
description: Creation timestamp (UTC ISO-8601)
VerificationReceiptDto:
type: object
required:
- graphRevisionId
- findingKey
- decision
- createdAt
- verified
properties:
graphRevisionId:
type: string
description: Graph revision ID this receipt was computed from
findingKey:
type: object
properties:
sbomEntryId:
type: string
vulnerabilityId:
type: string
rule:
type: object
properties:
id:
type: string
version:
type: string
decision:
type: object
properties:
verdict:
type: string
enum: [pass, fail, warn, skip]
severity:
type: string
reasoning:
type: string
createdAt:
type: string
format: date-time
verified:
type: boolean
description: Whether the receipt signature verified correctly
VexAttestationDto:
type: object
required:
- sbomEntryId
- vulnerabilityId
- status
- vexVerdictId
properties:
sbomEntryId:
type: string
vulnerabilityId:
type: string
status:
type: string
enum: [not_affected, affected, fixed, under_investigation]
justification:
type: string
policyVersion:
type: string
reasoningId:
type: string
vexVerdictId:
type: string
TrustAnchorDto:
type: object
required:
- id
- keyId
- algorithm
- status
- createdAt
properties:
id:
type: string
description: Unique anchor identifier
keyId:
type: string
description: Key identifier (fingerprint)
algorithm:
type: string
enum: [ECDSA-P256, Ed25519, RSA-2048, RSA-4096]
description: Signing algorithm
publicKey:
type: string
description: PEM-encoded public key
status:
type: string
enum: [active, revoked, expired]
createdAt:
type: string
format: date-time
revokedAt:
type: string
format: date-time
CreateAnchorRequest:
type: object
required:
- keyId
- algorithm
- publicKey
properties:
keyId:
type: string
description: Key identifier
algorithm:
type: string
enum: [ECDSA-P256, Ed25519, RSA-2048, RSA-4096]
publicKey:
type: string
description: PEM-encoded public key
VerifyRequest:
type: object
required:
- proofBundleId
properties:
proofBundleId:
type: string
pattern: '^sha256:[a-f0-9]{64}$'
description: The proof bundle ID to verify
checkRekor:
type: boolean
default: true
description: Whether to verify Rekor inclusion proofs
anchorIds:
type: array
items:
type: string
description: Specific trust anchors to use for verification
VerificationResultDto:
type: object
required:
- proofBundleId
- verified
- checks
properties:
proofBundleId:
type: string
verified:
type: boolean
description: Overall verification result
checks:
type: object
properties:
signatureValid:
type: boolean
description: DSSE signature verification passed
idRecomputed:
type: boolean
description: Content-addressed IDs recomputed correctly
merklePathValid:
type: boolean
description: Merkle path verification passed
rekorInclusionValid:
type: boolean
description: Rekor inclusion proof verified (if checked)
errors:
type: array
items:
type: string
description: Error messages if verification failed
verifiedAt:
type: string
format: date-time
responses:
BadRequest:
description: Invalid request
content:
application/problem+json:
schema:
type: object
properties:
title:
type: string
detail:
type: string
status:
type: integer
example: 400
NotFound:
description: Resource not found
content:
application/problem+json:
schema:
type: object
properties:
title:
type: string
detail:
type: string
status:
type: integer
example: 404
ValidationError:
description: Validation error
content:
application/problem+json:
schema:
type: object
properties:
title:
type: string
detail:
type: string
status:
type: integer
example: 422
errors:
type: object
additionalProperties:
type: array
items:
type: string

333
docs/api/proofs.md Normal file
View File

@@ -0,0 +1,333 @@
# Proof Chain API Reference
> **Version**: 1.0.0
> **OpenAPI Spec**: [`proofs-openapi.yaml`](./proofs-openapi.yaml)
The Proof Chain API provides endpoints for creating and verifying cryptographic proof bundles that link SBOM entries to vulnerability assessments through attestable DSSE envelopes.
---
## Overview
The proof chain creates an auditable, cryptographically-verifiable trail from vulnerability evidence through policy reasoning to VEX verdicts. Each component is signed with DSSE envelopes and aggregated into a merkle-rooted proof spine.
### Proof Chain Components
| Component | Predicate Type | Purpose |
|-----------|----------------|---------|
| **Evidence** | `evidence.stella/v1` | Raw findings from scanners/feeds |
| **Reasoning** | `reasoning.stella/v1` | Policy evaluation trace |
| **VEX Verdict** | `cdx-vex.stella/v1` | Final VEX status determination |
| **Proof Spine** | `proofspine.stella/v1` | Merkle aggregation of all components |
| **Verdict Receipt** | `verdict.stella/v1` | Human-readable verification receipt |
### Content-Addressed IDs
All proof chain components use content-addressed identifiers:
```
Format: sha256:<64-hex-chars>
Example: sha256:e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6...
```
IDs are computed by:
1. Canonicalizing the JSON payload (RFC 8785/JCS)
2. Computing SHA-256 hash
3. Prefixing with `sha256:`
---
## Authentication
All endpoints require authentication via:
- **Bearer Token**: Authority-issued OpToken with appropriate scopes
- **mTLS**: Mutual TLS with client certificate (service-to-service)
Required scopes:
- `proofs.read` - Read proof bundles and receipts
- `proofs.write` - Create proof spines
- `anchors.manage` - Manage trust anchors
- `proofs.verify` - Perform verification
---
## Endpoints
### Proofs
#### POST /proofs/{entry}/spine
Create a proof spine for an SBOM entry.
**Parameters:**
- `entry` (path, required): SBOMEntryID in format `sha256:<hash>:pkg:<purl>`
**Request Body:**
```json
{
"evidenceIds": ["sha256:e7f8a9b0..."],
"reasoningId": "sha256:f0e1d2c3...",
"vexVerdictId": "sha256:d4c5b6a7...",
"policyVersion": "v1.2.3"
}
```
**Response (201 Created):**
```json
{
"proofBundleId": "sha256:1a2b3c4d...",
"receiptUrl": "/proofs/sha256:abc:pkg:npm/lodash@4.17.21/receipt"
}
```
**Errors:**
- `400 Bad Request`: Invalid SBOM entry ID format
- `404 Not Found`: Evidence, reasoning, or VEX verdict not found
- `422 Unprocessable Entity`: Validation error
---
#### GET /proofs/{entry}/spine
Get the proof spine for an SBOM entry.
**Parameters:**
- `entry` (path, required): SBOMEntryID
**Response (200 OK):**
```json
{
"sbomEntryId": "sha256:abc123:pkg:npm/lodash@4.17.21",
"proofBundleId": "sha256:1a2b3c4d...",
"evidenceIds": ["sha256:e7f8a9b0..."],
"reasoningId": "sha256:f0e1d2c3...",
"vexVerdictId": "sha256:d4c5b6a7...",
"policyVersion": "v1.2.3",
"createdAt": "2025-12-17T10:00:00Z"
}
```
---
#### GET /proofs/{entry}/receipt
Get the verification receipt for an SBOM entry's proof spine.
**Response (200 OK):**
```json
{
"graphRevisionId": "grv_sha256:9f8e7d6c...",
"findingKey": {
"sbomEntryId": "sha256:abc123:pkg:npm/lodash@4.17.21",
"vulnerabilityId": "CVE-2025-1234"
},
"rule": {
"id": "critical-vuln-block",
"version": "v1.0.0"
},
"decision": {
"verdict": "pass",
"severity": "none",
"reasoning": "Not affected - vulnerable code not present"
},
"createdAt": "2025-12-17T10:00:00Z",
"verified": true
}
```
---
#### GET /proofs/{entry}/vex
Get the VEX attestation for an SBOM entry.
**Response (200 OK):**
```json
{
"sbomEntryId": "sha256:abc123:pkg:npm/lodash@4.17.21",
"vulnerabilityId": "CVE-2025-1234",
"status": "not_affected",
"justification": "vulnerable_code_not_present",
"policyVersion": "v1.2.3",
"reasoningId": "sha256:f0e1d2c3...",
"vexVerdictId": "sha256:d4c5b6a7..."
}
```
---
### Trust Anchors
#### GET /anchors
List all configured trust anchors.
**Response (200 OK):**
```json
{
"anchors": [
{
"id": "anchor-001",
"keyId": "sha256:abc123...",
"algorithm": "ECDSA-P256",
"status": "active",
"createdAt": "2025-01-01T00:00:00Z"
}
]
}
```
---
#### POST /anchors
Create a new trust anchor.
**Request Body:**
```json
{
"keyId": "sha256:abc123...",
"algorithm": "ECDSA-P256",
"publicKey": "-----BEGIN PUBLIC KEY-----\n..."
}
```
**Response (201 Created):**
```json
{
"id": "anchor-002",
"keyId": "sha256:abc123...",
"algorithm": "ECDSA-P256",
"status": "active",
"createdAt": "2025-12-17T10:00:00Z"
}
```
---
#### DELETE /anchors/{anchorId}
Delete (revoke) a trust anchor.
**Response:** `204 No Content`
---
### Verification
#### POST /verify
Perform full verification of a proof bundle.
**Request Body:**
```json
{
"proofBundleId": "sha256:1a2b3c4d...",
"checkRekor": true,
"anchorIds": ["anchor-001"]
}
```
**Response (200 OK):**
```json
{
"proofBundleId": "sha256:1a2b3c4d...",
"verified": true,
"checks": {
"signatureValid": true,
"idRecomputed": true,
"merklePathValid": true,
"rekorInclusionValid": true
},
"errors": [],
"verifiedAt": "2025-12-17T10:00:00Z"
}
```
**Verification Steps:**
1. **Signature Verification**: Verify DSSE envelope signatures against trust anchors
2. **ID Recomputation**: Recompute content-addressed IDs and compare
3. **Merkle Path Verification**: Verify proof bundle merkle tree construction
4. **Rekor Inclusion**: Verify transparency log inclusion proof (if enabled)
---
#### POST /verify/batch
Verify multiple proof bundles in a single request.
**Request Body:**
```json
{
"bundles": [
{ "proofBundleId": "sha256:1a2b3c4d...", "checkRekor": true },
{ "proofBundleId": "sha256:5e6f7g8h...", "checkRekor": false }
]
}
```
**Response (200 OK):**
```json
{
"results": [
{ "proofBundleId": "sha256:1a2b3c4d...", "verified": true, "checks": {...} },
{ "proofBundleId": "sha256:5e6f7g8h...", "verified": false, "errors": ["..."] }
]
}
```
---
## Error Handling
All errors follow RFC 7807 Problem Details format:
```json
{
"title": "Validation Error",
"detail": "Evidence ID sha256:abc... not found",
"status": 422,
"errors": {
"evidenceIds[0]": ["Evidence not found"]
}
}
```
### Common Error Codes
| Status | Meaning |
|--------|---------|
| 400 | Invalid request format or parameters |
| 401 | Authentication required |
| 403 | Insufficient permissions |
| 404 | Resource not found |
| 409 | Conflict (e.g., anchor already exists) |
| 422 | Validation error |
| 500 | Internal server error |
---
## Offline Verification
For air-gapped environments, verification can be performed without Rekor:
```json
{
"proofBundleId": "sha256:1a2b3c4d...",
"checkRekor": false
}
```
This skips Rekor inclusion proof verification but still performs:
- DSSE signature verification
- Content-addressed ID recomputation
- Merkle path verification
---
## Related Documentation
- [Proof Chain Predicates](../modules/attestor/architecture.md#predicate-types) - DSSE predicate type specifications
- [Content-Addressed IDs](../modules/attestor/architecture.md#content-addressed-identifier-formats) - ID generation rules
- [Attestor Architecture](../modules/attestor/architecture.md) - Full attestor module documentation

View File

@@ -0,0 +1,682 @@
# 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:
1. Scan manifests and deterministic replay
2. Proof bundles (score proofs + reachability evidence)
3. Call-graph ingestion and reachability analysis
4. Unknowns management
**Design Principles**:
- All endpoints return canonical JSON (deterministic serialization)
- Idempotency via `Content-Digest` headers (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**:
```json
{
"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):
```json
{
"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 same `manifestHash` already exists
- `422 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):
```json
{
"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/json`
- `ETag`: `"<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**:
```json
{
"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):
```json
{
"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 found
- `422 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`):
```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):
```json
{
"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 schema
- `404 Not Found` — Scan ID not found
- `413 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):
```json
{
"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 found
- `422 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 by `REACHABLE`, `UNREACHABLE`, `POSSIBLY_REACHABLE`, `UNKNOWN`
- `cveId` (optional): Filter by CVE ID
**Response** (200 OK):
```json
{
"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 ID
- `purl` (required): Package URL
**Response** (200 OK):
```json
{
"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/zip`
- `Content-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 manifest
- `manifest.dsse.json` — DSSE signature of manifest
- `score_proof.json` — Proof ledger (array of ProofNodes)
- `proof_root.dsse.json` — DSSE signature of proof root
- `meta.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 by `HOT`, `WARM`, `COLD`
- `limit` (optional): Max results (default: 100, max: 1000)
- `offset` (optional): Pagination offset
**Response** (200 OK):
```json
{
"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):
```json
{
"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 found
- `409 Conflict` — Unknown already escalated (rescan in progress)
---
## Data Models
### ScanManifest
See `src/__Libraries/StellaOps.Scanner.Core/Models/ScanManifest.cs` for full definition.
### ProofNode
```typescript
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
```typescript
interface DsseEnvelope {
payloadType: string;
payload: string; // base64 canonical JSON
signatures: DsseSignature[];
}
interface DsseSignature {
keyid: string;
sig: string; // base64
}
```
### ReachabilityStatus
```typescript
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):
```json
{
"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 tenant
- `POST /scans/{id}/score/replay`: 1000 requests/hour per tenant
- `POST /callgraphs`: 100 requests/hour per tenant
- `POST /reachability/compute`: 100 requests/hour per tenant
- `GET` endpoints: 10,000 requests/hour per tenant
**Headers**:
- `X-RateLimit-Limit`: Maximum requests per window
- `X-RateLimit-Remaining`: Remaining requests
- `X-RateLimit-Reset`: Unix timestamp when limit resets
**Error** (429 Too Many Requests):
```json
{
"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 sprint
- `SPRINT_3500_0002_0003_proof_replay_api.md` — API implementation sprint
- `SPRINT_3500_0003_0003_graph_attestations_rekor.md` — Reachability API sprint
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md` — API contracts section
- `docs/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)

View File

@@ -0,0 +1,282 @@
# Score Replay API Reference
**Sprint:** SPRINT_3401_0002_0001
**Task:** SCORE-REPLAY-014 - Update scanner API docs with replay endpoint
## Overview
The Score Replay API enables deterministic re-scoring of scans using historical manifests. This is essential for auditing, compliance verification, and investigating how scores change with updated advisory feeds.
## Base URL
```
/api/v1/score
```
## Authentication
All endpoints require Bearer token authentication:
```http
Authorization: Bearer <token>
```
Required scope: `scanner:replay:read` for GET, `scanner:replay:write` for POST
## Endpoints
### Replay Score
```http
POST /api/v1/score/replay
```
Re-scores a scan using the original manifest with an optionally different feed snapshot.
#### Request Body
```json
{
"scanId": "scan-12345678-abcd",
"feedSnapshotHash": "sha256:abc123...",
"policyVersion": "1.0.0",
"dryRun": false
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `scanId` | string | Yes | Original scan ID to replay |
| `feedSnapshotHash` | string | No | Feed snapshot to use (defaults to current) |
| `policyVersion` | string | No | Policy version (defaults to original) |
| `dryRun` | boolean | No | If true, calculates but doesn't persist |
#### Response
```json
{
"replayId": "replay-87654321-dcba",
"originalScanId": "scan-12345678-abcd",
"status": "completed",
"feedSnapshotHash": "sha256:abc123...",
"policyVersion": "1.0.0",
"originalManifestHash": "sha256:def456...",
"replayedManifestHash": "sha256:ghi789...",
"scoreDelta": {
"originalScore": 7.5,
"replayedScore": 6.8,
"delta": -0.7
},
"findingsDelta": {
"added": 2,
"removed": 5,
"rescored": 12,
"unchanged": 45
},
"proofBundleRef": "proofs/replays/replay-87654321/bundle.zip",
"duration": {
"ms": 1250
},
"createdAt": "2025-01-15T10:30:00Z"
}
```
#### Example
```bash
# Replay with latest feed
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"scanId": "scan-12345678-abcd"}' \
"https://scanner.example.com/api/v1/score/replay"
# Replay with specific feed snapshot
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"scanId": "scan-12345678-abcd",
"feedSnapshotHash": "sha256:abc123..."
}' \
"https://scanner.example.com/api/v1/score/replay"
# Dry run (preview only)
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"scanId": "scan-12345678-abcd",
"dryRun": true
}' \
"https://scanner.example.com/api/v1/score/replay"
```
### Get Replay History
```http
GET /api/v1/score/replays
```
Returns history of score replays.
#### Query Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `scanId` | string | - | Filter by original scan |
| `page` | int | 1 | Page number |
| `pageSize` | int | 50 | Items per page |
#### Response
```json
{
"items": [
{
"replayId": "replay-87654321-dcba",
"originalScanId": "scan-12345678-abcd",
"triggerType": "manual",
"scoreDelta": -0.7,
"findingsAdded": 2,
"findingsRemoved": 5,
"createdAt": "2025-01-15T10:30:00Z"
}
],
"pagination": {
"page": 1,
"pageSize": 50,
"totalItems": 12,
"totalPages": 1
}
}
```
### Get Replay Details
```http
GET /api/v1/score/replays/{replayId}
```
Returns detailed information about a specific replay.
### Get Scan Manifest
```http
GET /api/v1/scans/{scanId}/manifest
```
Returns the scan manifest containing all input hashes.
#### Response
```json
{
"manifestId": "manifest-12345678",
"scanId": "scan-12345678-abcd",
"manifestHash": "sha256:def456...",
"sbomHash": "sha256:aaa111...",
"rulesHash": "sha256:bbb222...",
"feedHash": "sha256:ccc333...",
"policyHash": "sha256:ddd444...",
"scannerVersion": "1.0.0",
"createdAt": "2025-01-15T10:00:00Z"
}
```
### Get Proof Bundle
```http
GET /api/v1/scans/{scanId}/proof-bundle
```
Downloads the proof bundle (ZIP archive) for a scan.
#### Response
Returns `application/zip` with the proof bundle containing:
- `manifest.json` - Signed scan manifest
- `ledger.json` - Proof ledger nodes
- `sbom.json` - Input SBOM (hash-verified)
- `findings.json` - Scored findings
- `signature.dsse` - DSSE envelope
## Scheduled Replay
Scans can be automatically replayed when feed snapshots change.
### Configuration
```yaml
# config/scanner.yaml
score_replay:
enabled: true
schedule: "0 4 * * *" # Daily at 4 AM UTC
max_age_days: 30 # Only replay scans from last 30 days
notify_on_delta: true # Send notification if scores change
delta_threshold: 0.5 # Only notify if delta > threshold
```
### Trigger Types
| Type | Description |
|------|-------------|
| `manual` | User-initiated via API |
| `feed_update` | Triggered by new feed snapshot |
| `policy_change` | Triggered by policy version change |
| `scheduled` | Triggered by scheduled job |
## Determinism Guarantees
Score replay guarantees deterministic results when:
1. **Same manifest hash** - All inputs are identical
2. **Same scanner version** - Scoring algorithm unchanged
3. **Same policy version** - Policy rules unchanged
### Manifest Contents
The manifest captures:
- SBOM content hash
- Rules snapshot hash
- Advisory feed snapshot hash
- Policy configuration hash
- Scanner version
### Verification
```bash
# Verify replay determinism
curl -H "Authorization: Bearer $TOKEN" \
"https://scanner.example.com/api/v1/scans/{scanId}/manifest" \
| jq '.manifestHash'
# Compare with replay
curl -H "Authorization: Bearer $TOKEN" \
"https://scanner.example.com/api/v1/score/replays/{replayId}" \
| jq '.replayedManifestHash'
```
## Error Responses
| Status | Code | Description |
|--------|------|-------------|
| 400 | `INVALID_SCAN_ID` | Scan ID not found |
| 400 | `INVALID_FEED_SNAPSHOT` | Feed snapshot not found |
| 400 | `MANIFEST_NOT_FOUND` | Scan manifest missing |
| 401 | `UNAUTHORIZED` | Invalid token |
| 403 | `FORBIDDEN` | Insufficient permissions |
| 409 | `REPLAY_IN_PROGRESS` | Replay already running for scan |
| 429 | `RATE_LIMITED` | Too many requests |
## Rate Limits
- POST replay: 10 requests/minute
- GET replays: 100 requests/minute
- GET manifest: 100 requests/minute
## Related Documentation
- [Proof Bundle Format](./proof-bundle-format.md)
- [Scanner Architecture](../modules/scanner/architecture.md)
- [Determinism Requirements](../product-advisories/14-Dec-2025%20-%20Determinism%20and%20Reproducibility%20Technical%20Reference.md)

334
docs/api/unknowns-api.md Normal file
View File

@@ -0,0 +1,334 @@
# Unknowns API Reference
**Sprint:** SPRINT_3600_0002_0001
**Task:** UNK-RANK-011 - Update unknowns API documentation
## Overview
The Unknowns API provides access to items that could not be fully classified due to missing evidence, ambiguous data, or incomplete intelligence. Unknowns are ranked by blast radius, exploit pressure, and containment signals.
## Base URL
```
/api/v1/unknowns
```
## Authentication
All endpoints require Bearer token authentication:
```http
Authorization: Bearer <token>
```
Required scope: `scanner:unknowns:read`
## Endpoints
### List Unknowns
```http
GET /api/v1/unknowns
```
Returns paginated list of unknowns, optionally sorted by score.
#### Query Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `sort` | string | `score` | Sort field: `score`, `created_at`, `blast_dependents` |
| `order` | string | `desc` | Sort order: `asc`, `desc` |
| `page` | int | 1 | Page number (1-indexed) |
| `pageSize` | int | 50 | Items per page (max 200) |
| `artifact` | string | - | Filter by artifact digest |
| `reason` | string | - | Filter by reason code |
| `minScore` | float | - | Minimum score threshold (0-1) |
| `maxScore` | float | - | Maximum score threshold (0-1) |
| `kev` | bool | - | Filter by KEV status |
| `seccomp` | string | - | Filter by seccomp state: `enforced`, `permissive`, `unknown` |
#### Response
```json
{
"items": [
{
"id": "unk-12345678-abcd-1234-5678-abcdef123456",
"artifactDigest": "sha256:abc123...",
"artifactPurl": "pkg:oci/myapp@sha256:abc123",
"reasons": ["missing_vex", "ambiguous_indirect_call"],
"blastRadius": {
"dependents": 15,
"netFacing": true,
"privilege": "user"
},
"evidenceScarcity": 0.7,
"exploitPressure": {
"epss": 0.45,
"kev": false
},
"containment": {
"seccomp": "enforced",
"fs": "ro"
},
"score": 0.62,
"proofRef": "proofs/unknowns/unk-12345678/tree.json",
"createdAt": "2025-01-15T10:30:00Z",
"updatedAt": "2025-01-15T10:30:00Z"
}
],
"pagination": {
"page": 1,
"pageSize": 50,
"totalItems": 142,
"totalPages": 3
}
}
```
#### Example
```bash
# Get top 10 highest-scored unknowns
curl -H "Authorization: Bearer $TOKEN" \
"https://scanner.example.com/api/v1/unknowns?sort=score&order=desc&pageSize=10"
# Filter by KEV and minimum score
curl -H "Authorization: Bearer $TOKEN" \
"https://scanner.example.com/api/v1/unknowns?kev=true&minScore=0.5"
# Filter by artifact
curl -H "Authorization: Bearer $TOKEN" \
"https://scanner.example.com/api/v1/unknowns?artifact=sha256:abc123"
```
### Get Unknown by ID
```http
GET /api/v1/unknowns/{id}
```
Returns detailed information about a specific unknown.
#### Response
```json
{
"id": "unk-12345678-abcd-1234-5678-abcdef123456",
"artifactDigest": "sha256:abc123...",
"artifactPurl": "pkg:oci/myapp@sha256:abc123",
"reasons": ["missing_vex", "ambiguous_indirect_call"],
"reasonDetails": [
{
"code": "missing_vex",
"message": "No VEX statement found for CVE-2024-1234",
"component": "pkg:npm/lodash@4.17.20"
},
{
"code": "ambiguous_indirect_call",
"message": "Indirect call target could not be resolved",
"location": "src/utils.js:42"
}
],
"blastRadius": {
"dependents": 15,
"netFacing": true,
"privilege": "user"
},
"evidenceScarcity": 0.7,
"exploitPressure": {
"epss": 0.45,
"kev": false
},
"containment": {
"seccomp": "enforced",
"fs": "ro"
},
"score": 0.62,
"scoreBreakdown": {
"blastComponent": 0.35,
"scarcityComponent": 0.21,
"pressureComponent": 0.26,
"containmentDeduction": -0.20
},
"proofRef": "proofs/unknowns/unk-12345678/tree.json",
"createdAt": "2025-01-15T10:30:00Z",
"updatedAt": "2025-01-15T10:30:00Z"
}
```
### Get Unknown Proof
```http
GET /api/v1/unknowns/{id}/proof
```
Returns the proof tree explaining the ranking decision.
#### Response
```json
{
"version": "1.0",
"unknownId": "unk-12345678-abcd-1234-5678-abcdef123456",
"nodes": [
{
"kind": "input",
"hash": "sha256:abc...",
"data": {
"reasons": ["missing_vex"],
"evidenceScarcity": 0.7
}
},
{
"kind": "delta",
"hash": "sha256:def...",
"factor": "blast_radius",
"contribution": 0.35
},
{
"kind": "delta",
"hash": "sha256:ghi...",
"factor": "containment_seccomp",
"contribution": -0.10
},
{
"kind": "score",
"hash": "sha256:jkl...",
"finalScore": 0.62
}
],
"rootHash": "sha256:mno..."
}
```
### Batch Get Unknowns
```http
POST /api/v1/unknowns/batch
```
Get multiple unknowns by ID in a single request.
#### Request Body
```json
{
"ids": [
"unk-12345678-abcd-1234-5678-abcdef123456",
"unk-87654321-dcba-4321-8765-654321fedcba"
]
}
```
#### Response
Same format as list response with matching items.
### Get Unknowns Summary
```http
GET /api/v1/unknowns/summary
```
Returns aggregate statistics about unknowns.
#### Query Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `artifact` | string | Filter by artifact digest |
#### Response
```json
{
"totalCount": 142,
"byReason": {
"missing_vex": 45,
"ambiguous_indirect_call": 32,
"incomplete_sbom": 28,
"unknown_platform": 15,
"other": 22
},
"byScoreBucket": {
"critical": 12, // score >= 0.8
"high": 35, // 0.6 <= score < 0.8
"medium": 48, // 0.4 <= score < 0.6
"low": 47 // score < 0.4
},
"byContainment": {
"enforced": 45,
"permissive": 32,
"unknown": 65
},
"kevCount": 8,
"avgScore": 0.52
}
```
## Reason Codes
| Code | Description |
|------|-------------|
| `missing_vex` | No VEX statement for vulnerability |
| `ambiguous_indirect_call` | Indirect call target unresolved |
| `incomplete_sbom` | SBOM missing component data |
| `unknown_platform` | Platform not recognized |
| `missing_advisory` | No advisory data for CVE |
| `conflicting_evidence` | Multiple conflicting data sources |
| `stale_data` | Data exceeds freshness threshold |
## Score Calculation
The unknown score is calculated as:
```
score = 0.60 × blast + 0.30 × scarcity + 0.30 × pressure + containment_deduction
```
Where:
- `blast` = normalized blast radius (0-1)
- `scarcity` = evidence scarcity factor (0-1)
- `pressure` = exploit pressure (EPSS + KEV factor)
- `containment_deduction` = -0.10 for enforced seccomp, -0.10 for read-only FS
### Blast Radius Normalization
```
dependents_normalized = min(dependents / 50, 1.0)
net_factor = 0.5 if net_facing else 0.0
priv_factor = 0.5 if privilege == "root" else 0.0
blast = min((dependents_normalized + net_factor + priv_factor) / 2, 1.0)
```
### Exploit Pressure
```
epss_normalized = epss ?? 0.35 // Default if unknown
kev_factor = 0.30 if kev else 0.0
pressure = min(epss_normalized + kev_factor, 1.0)
```
## Error Responses
| Status | Code | Description |
|--------|------|-------------|
| 400 | `INVALID_PARAMETER` | Invalid query parameter |
| 401 | `UNAUTHORIZED` | Missing or invalid token |
| 403 | `FORBIDDEN` | Insufficient permissions |
| 404 | `NOT_FOUND` | Unknown not found |
| 429 | `RATE_LIMITED` | Too many requests |
## Rate Limits
- List: 100 requests/minute
- Get by ID: 300 requests/minute
- Summary: 60 requests/minute
## Related Documentation
- [Unknowns Ranking Technical Reference](../product-advisories/14-Dec-2025%20-%20Triage%20and%20Unknowns%20Technical%20Reference.md)
- [Scanner Architecture](../modules/scanner/architecture.md)
- [Proof Bundle Format](../api/proof-bundle-format.md)