- 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.
6.5 KiB
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:
Authorization: Bearer <token>
Required scope: scanner:replay:read for GET, scanner:replay:write for POST
Endpoints
Replay Score
POST /api/v1/score/replay
Re-scores a scan using the original manifest with an optionally different feed snapshot.
Request Body
{
"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
{
"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
# 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
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
{
"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
GET /api/v1/score/replays/{replayId}
Returns detailed information about a specific replay.
Get Scan Manifest
GET /api/v1/scans/{scanId}/manifest
Returns the scan manifest containing all input hashes.
Response
{
"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
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 manifestledger.json- Proof ledger nodessbom.json- Input SBOM (hash-verified)findings.json- Scored findingssignature.dsse- DSSE envelope
Scheduled Replay
Scans can be automatically replayed when feed snapshots change.
Configuration
# 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:
- Same manifest hash - All inputs are identical
- Same scanner version - Scoring algorithm unchanged
- 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
# 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