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:
622
docs/api/proofs-openapi.yaml
Normal file
622
docs/api/proofs-openapi.yaml
Normal 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
333
docs/api/proofs.md
Normal 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
|
||||
682
docs/api/scanner-score-proofs-api.md
Normal file
682
docs/api/scanner-score-proofs-api.md
Normal 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)
|
||||
282
docs/api/score-replay-api.md
Normal file
282
docs/api/score-replay-api.md
Normal 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
334
docs/api/unknowns-api.md
Normal 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)
|
||||
Reference in New Issue
Block a user