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,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)