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:
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)
|
||||
Reference in New Issue
Block a user