consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -1,282 +1,126 @@
|
||||
# Score Replay API Reference
|
||||
# Score API Reference (Platform)
|
||||
|
||||
**Sprint:** SPRINT_3401_0002_0001
|
||||
**Task:** SCORE-REPLAY-014 - Update scanner API docs with replay endpoint
|
||||
**Module:** Platform WebService
|
||||
**Base route:** `/api/v1/score`
|
||||
|
||||
> Scope note: this page documents the Platform score API.
|
||||
> Scanner score replay endpoints are implemented separately at:
|
||||
> - primary: `/api/v1/scans/{scanId}/score/replay|bundle|verify|history`
|
||||
> - compatibility aliases: `/api/v1/score/{scanId}/replay|bundle|verify|history`
|
||||
> See `src/Scanner/StellaOps.Scanner.WebService/Endpoints/ScoreReplayEndpoints.cs` and `docs/modules/scanner/architecture.md`.
|
||||
|
||||
## 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.
|
||||
The score API exposes deterministic score computation, replay verification, and explanation payloads.
|
||||
All responses are tenant-scoped and wrapped in the standard Platform envelope.
|
||||
|
||||
## Base URL
|
||||
## Authentication and tenant context
|
||||
|
||||
- Bearer token authentication is required.
|
||||
- Required policies:
|
||||
`platform.score.read`, `platform.score.evaluate`
|
||||
- Tenant context is resolved from authenticated context/middleware and must be present.
|
||||
|
||||
## Response envelope
|
||||
|
||||
Single-item responses return:
|
||||
|
||||
```json
|
||||
{
|
||||
"tenantId": "tenant-a",
|
||||
"actorId": "user-1",
|
||||
"dataAsOf": "2026-02-26T12:00:00Z",
|
||||
"cached": true,
|
||||
"cacheTtlSeconds": 300,
|
||||
"item": { }
|
||||
}
|
||||
```
|
||||
/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
|
||||
### `POST /api/v1/score/evaluate`
|
||||
|
||||
```http
|
||||
POST /api/v1/score/replay
|
||||
```
|
||||
Computes unified score from provided signal inputs.
|
||||
|
||||
Re-scores a scan using the original manifest with an optionally different feed snapshot.
|
||||
Response highlights:
|
||||
- `unknowns`: deterministic list of missing signal dimensions when snapshot data is available.
|
||||
- `proof_ref`: deterministic proof locator (`proof://score/<normalized-digest>`).
|
||||
|
||||
#### Request Body
|
||||
### `GET /api/v1/score/history?cve_id=<id>&purl=<optional>&limit=<optional>`
|
||||
|
||||
Returns historical score records for the requested CVE and optional PURL.
|
||||
|
||||
### `GET /api/v1/score/{scoreId}`
|
||||
|
||||
Returns persisted score by score identifier.
|
||||
|
||||
### `GET /api/v1/score/{scoreId}/replay`
|
||||
|
||||
Returns replay payload for deterministic verification.
|
||||
|
||||
### `POST /api/v1/score/verify`
|
||||
|
||||
Verifies replay payload and returns deterministic verification status fields.
|
||||
|
||||
Verification details:
|
||||
- `verified` is computed from deterministic comparison checks (`score_matches`, `digest_matches`) and available signature/Rekor checks.
|
||||
- `differences` includes field-level mismatch reasons (for example `final_score`, `ews_digest`, `signed_replay_log_dsse`).
|
||||
- malformed replay envelopes return a deterministic `differences` entry rather than synthetic success.
|
||||
|
||||
### `GET /api/v1/score/explain/{digest}`
|
||||
|
||||
Returns canonical score explanation contract for a persisted replay digest.
|
||||
|
||||
Success payload (`item`) schema:
|
||||
|
||||
```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": [
|
||||
"contractVersion": "score.explain.v1",
|
||||
"digest": "sha256:...",
|
||||
"scoreId": "score_...",
|
||||
"finalScore": 62,
|
||||
"bucket": "Investigate",
|
||||
"computedAt": "2026-02-26T12:00:00Z",
|
||||
"deterministicInputHash": "sha256:...",
|
||||
"replayLink": "/api/v1/score/score_x/replay",
|
||||
"factors": [
|
||||
{
|
||||
"replayId": "replay-87654321-dcba",
|
||||
"originalScanId": "scan-12345678-abcd",
|
||||
"triggerType": "manual",
|
||||
"scoreDelta": -0.7,
|
||||
"findingsAdded": 2,
|
||||
"findingsRemoved": 5,
|
||||
"createdAt": "2025-01-15T10:30:00Z"
|
||||
"name": "reachability",
|
||||
"weight": 0.25,
|
||||
"value": 1.0,
|
||||
"contribution": 0.25
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"pageSize": 50,
|
||||
"totalItems": 12,
|
||||
"totalPages": 1
|
||||
}
|
||||
"sources": [
|
||||
{
|
||||
"sourceType": "score_history",
|
||||
"sourceRef": "score-history:score_x",
|
||||
"sourceDigest": "sha256:..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Get Replay Details
|
||||
## Deterministic error schema (`/explain/{digest}`)
|
||||
|
||||
```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
|
||||
Error payload:
|
||||
|
||||
```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"
|
||||
"code": "not_found | invalid_input | backend_unavailable",
|
||||
"message": "deterministic human-readable message",
|
||||
"digest": "sha256:..."
|
||||
}
|
||||
```
|
||||
|
||||
### Get Proof Bundle
|
||||
Status mapping:
|
||||
|
||||
```http
|
||||
GET /api/v1/scans/{scanId}/proof-bundle
|
||||
```
|
||||
- `400` -> `invalid_input`
|
||||
- `404` -> `not_found`
|
||||
- `503` -> `backend_unavailable`
|
||||
|
||||
Downloads the proof bundle (ZIP archive) for a scan.
|
||||
## Client integration notes
|
||||
|
||||
#### 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)
|
||||
- CLI and Web clients must treat `score.explain.v1` as the current canonical contract.
|
||||
- Clients must not synthesize explanation factors when `404` or `503` is returned.
|
||||
- `digest` values are normalized to lowercase with explicit algorithm prefix (`sha256:`).
|
||||
|
||||
Reference in New Issue
Block a user