consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
Scope: expose Orchestrator read + operator control surfaces through the Web gateway (tenant-scoped, deterministic pagination, cache headers) to unblock Console control-plane views.
|
||||
|
||||
This is an interim contract until the gateway is aligned to the Orchestrator OpenAPI (`/openapi/orchestrator.json` in the Orchestrator service).
|
||||
This is an interim contract until the gateway is aligned to the Orchestrator OpenAPI (`/openapi/jobengine.json` in the Orchestrator service).
|
||||
|
||||
## Security / headers
|
||||
- `Authorization: Bearer <token>` (or `DPoP` where configured)
|
||||
@@ -15,26 +15,26 @@ This is an interim contract until the gateway is aligned to the Orchestrator Ope
|
||||
- `X-Stella-Operator-Ticket: <ticket-id>` (optional but recommended)
|
||||
|
||||
## Endpoints
|
||||
- `GET /orchestrator/sources` — list registered job sources (tenant-scoped).
|
||||
- `GET /jobengine/sources` — list registered job sources (tenant-scoped).
|
||||
- Query params: `sourceType`, `enabled`, `limit`, `continuationToken`
|
||||
- `GET /orchestrator/sources/{sourceId}` — source detail.
|
||||
- `GET /orchestrator/quotas` — list quotas (scope: `orch:quota`).
|
||||
- `GET /jobengine/sources/{sourceId}` — source detail.
|
||||
- `GET /jobengine/quotas` — list quotas (scope: `orch:quota`).
|
||||
- Query params: `jobType`, `paused`, `limit`, `continuationToken`
|
||||
- `GET /orchestrator/quotas/{quotaId}` — quota detail (scope: `orch:quota`).
|
||||
- `POST /orchestrator/quotas` — create quota (scope: `orch:quota`).
|
||||
- `PUT /orchestrator/quotas/{quotaId}` — update quota (scope: `orch:quota`).
|
||||
- `DELETE /orchestrator/quotas/{quotaId}` — delete quota (scope: `orch:quota`).
|
||||
- `POST /orchestrator/quotas/{quotaId}/pause` — pause quota (scope: `orch:quota`).
|
||||
- `POST /orchestrator/quotas/{quotaId}/resume` — resume quota (scope: `orch:quota`).
|
||||
- `GET /orchestrator/quotas/summary` — quota/backpressure metrics summary (scope: `orch:quota`).
|
||||
- `GET /orchestrator/jobs/summary` — job summary counts (scope: `orch:read`).
|
||||
- `GET /orchestrator/deadletter/stats` — deadletter stats and top error clustering (scope: `orch:operate`).
|
||||
- `GET /orchestrator/deadletter/summary` — grouped deadletter summary (scope: `orch:operate`).
|
||||
- `POST /orchestrator/deadletter/{entryId}/replay` — replay a deadletter entry (scope: `orch:backfill`).
|
||||
- `POST /orchestrator/deadletter/replay/batch` — replay a set of entry IDs (scope: `orch:backfill`).
|
||||
- `POST /orchestrator/deadletter/replay/pending` — replay pending entries by filter (scope: `orch:backfill`).
|
||||
- `POST /orchestrator/pack-runs/{packRunId}/cancel` — cancel a pack run (scope: `orch:operate`).
|
||||
- `POST /orchestrator/pack-runs/{packRunId}/retry` — retry a pack run (scope: `orch:backfill`).
|
||||
- `GET /jobengine/quotas/{quotaId}` — quota detail (scope: `orch:quota`).
|
||||
- `POST /jobengine/quotas` — create quota (scope: `orch:quota`).
|
||||
- `PUT /jobengine/quotas/{quotaId}` — update quota (scope: `orch:quota`).
|
||||
- `DELETE /jobengine/quotas/{quotaId}` — delete quota (scope: `orch:quota`).
|
||||
- `POST /jobengine/quotas/{quotaId}/pause` — pause quota (scope: `orch:quota`).
|
||||
- `POST /jobengine/quotas/{quotaId}/resume` — resume quota (scope: `orch:quota`).
|
||||
- `GET /jobengine/quotas/summary` — quota/backpressure metrics summary (scope: `orch:quota`).
|
||||
- `GET /jobengine/jobs/summary` — job summary counts (scope: `orch:read`).
|
||||
- `GET /jobengine/deadletter/stats` — deadletter stats and top error clustering (scope: `orch:operate`).
|
||||
- `GET /jobengine/deadletter/summary` — grouped deadletter summary (scope: `orch:operate`).
|
||||
- `POST /jobengine/deadletter/{entryId}/replay` — replay a deadletter entry (scope: `orch:backfill`).
|
||||
- `POST /jobengine/deadletter/replay/batch` — replay a set of entry IDs (scope: `orch:backfill`).
|
||||
- `POST /jobengine/deadletter/replay/pending` — replay pending entries by filter (scope: `orch:backfill`).
|
||||
- `POST /jobengine/pack-runs/{packRunId}/cancel` — cancel a pack run (scope: `orch:operate`).
|
||||
- `POST /jobengine/pack-runs/{packRunId}/retry` — retry a pack run (scope: `orch:backfill`).
|
||||
|
||||
## Caching & pagination
|
||||
- `limit` max: `200`.
|
||||
@@ -4,7 +4,7 @@ Provides a fast “first meaningful signal” for a run (TTFS), with caching and
|
||||
|
||||
## Endpoint
|
||||
|
||||
`GET /api/v1/orchestrator/runs/{runId}/first-signal`
|
||||
`GET /api/v1/jobengine/runs/{runId}/first-signal`
|
||||
|
||||
### Required headers
|
||||
- `X-Tenant-Id`: tenant identifier (string)
|
||||
@@ -58,7 +58,7 @@ Missing/invalid tenant header or invalid parameters.
|
||||
## Streaming (SSE)
|
||||
The run stream emits `first_signal` events when the signal changes:
|
||||
|
||||
`GET /api/v1/orchestrator/stream/runs/{runId}`
|
||||
`GET /api/v1/jobengine/stream/runs/{runId}`
|
||||
|
||||
Event type:
|
||||
- `first_signal`
|
||||
@@ -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