# Evidence-Weighted Score (EWS) API Last updated: 2025-12-25 (Sprint 8200.0012.0004) ## Status - v1.0-beta (API endpoints complete; QA pending) ## Scope RESTful API for calculating, retrieving, and managing Evidence-Weighted Scores for vulnerability findings. Enables prioritization by aggregating multiple evidence signals into a single 0-100 score with action bucket classification. ## Related Documentation - [API Overview](overview.md) - Shared conventions for all StellaOps APIs - [Signals API](signals.md) - Signal collection and correlation - [OpenAPI Spec](../modules/findings-ledger/openapi/findings-ledger.v1.yaml) - Full OpenAPI 3.0 specification ## Authentication & Authorization ### Required Scopes | Scope | Description | Endpoints | |-------|-------------|-----------| | `read:scores` | Read score data | GET endpoints | | `write:scores` | Calculate scores | POST endpoints | | `admin:scoring` | Webhook management | All webhook endpoints | ### Required Headers | Header | Required | Description | |--------|----------|-------------| | `Authorization: Bearer ` | Yes | RS256/ES256 token with appropriate scopes | | `X-Stella-Tenant` | Yes | Tenant slug/UUID | | `Content-Type` | Yes (POST/PUT) | `application/json` | | `traceparent` | Recommended | W3C trace context | ## Endpoints Summary | Method | Path | Rate Limit | Description | |--------|------|------------|-------------| | `POST` | `/api/v1/findings/{findingId}/score` | 100/min | Calculate score for finding | | `GET` | `/api/v1/findings/{findingId}/score` | 1000/min | Get cached score | | `POST` | `/api/v1/findings/scores` | 10/min | Batch calculate (max 100) | | `GET` | `/api/v1/findings/{findingId}/score-history` | 100/min | Get score history | | `GET` | `/api/v1/scoring/policy` | 100/min | Get active policy | | `GET` | `/api/v1/scoring/policy/{version}` | 100/min | Get policy version | | `POST` | `/api/v1/scoring/webhooks` | 10/min | Register webhook | | `GET` | `/api/v1/scoring/webhooks` | 10/min | List webhooks | | `GET` | `/api/v1/scoring/webhooks/{id}` | 10/min | Get webhook | | `PUT` | `/api/v1/scoring/webhooks/{id}` | 10/min | Update webhook | | `DELETE` | `/api/v1/scoring/webhooks/{id}` | 10/min | Delete webhook | ## Score Calculation ### Calculate Single Score **Request:** ```http POST /api/v1/findings/{findingId}/score Authorization: Bearer X-Stella-Tenant: acme-corp Content-Type: application/json { "forceRecalculate": false, "includeBreakdown": true, "policyVersion": null } ``` **Parameters:** - `findingId` (path, required): Finding identifier in format `CVE-ID@pkg:PURL` - `forceRecalculate` (body, optional): Bypass cache, default `false` - `includeBreakdown` (body, optional): Include detailed inputs/weights, default `true` - `policyVersion` (body, optional): Specific policy version, `null` for latest **Response (200 OK):** ```json { "findingId": "CVE-2024-1234@pkg:deb/debian/curl@7.64.0-4", "score": 78, "bucket": "ScheduleNext", "inputs": { "rch": 0.85, "rts": 0.40, "bkp": 0.00, "xpl": 0.70, "src": 0.80, "mit": 0.10 }, "weights": { "rch": 0.30, "rts": 0.25, "bkp": 0.15, "xpl": 0.15, "src": 0.10, "mit": 0.10 }, "flags": ["live-signal", "proven-path"], "explanations": [ "Static reachability: path to vulnerable sink (confidence: 85%)", "Runtime: 3 observations in last 24 hours", "EPSS: 0.8% probability (High band)", "Source: Distro VEX signed (trust: 80%)", "Mitigations: seccomp profile active" ], "caps": { "speculativeCap": false, "notAffectedCap": false, "runtimeFloor": false }, "policyDigest": "sha256:abc123def456...", "calculatedAt": "2026-01-15T14:30:00Z", "cachedUntil": "2026-01-15T15:30:00Z", "fromCache": false } ``` ### Score Buckets | Bucket | Score Range | Action | |--------|-------------|--------| | `ActNow` | 90-100 | Immediate remediation required | | `ScheduleNext` | 70-89 | Schedule for upcoming sprint | | `Investigate` | 40-69 | Needs further analysis | | `Watchlist` | 0-39 | Monitor, low priority | ### Evidence Inputs | Code | Full Name | Description | |------|-----------|-------------| | `rch` | Reachability | Static/dynamic code path analysis (0-1) | | `rts` | Runtime Signal | Production observation frequency (0-1) | | `bkp` | Backport | Vendor backport availability (0-1) | | `xpl` | Exploit | EPSS/KEV exploit likelihood (0-1) | | `src` | Source Trust | Advisory source trust level (0-1) | | `mit` | Mitigation | Applied mitigations effectiveness (0-1) | ### Guardrails (Caps & Floors) | Guardrail | Effect | Condition | |-----------|--------|-----------| | `speculativeCap` | Max 45 | No runtime evidence | | `notAffectedCap` | Max 15 | VEX status = not_affected | | `runtimeFloor` | Min 60 | Observed in production | ## Batch Calculation **Request:** ```http POST /api/v1/findings/scores Authorization: Bearer X-Stella-Tenant: acme-corp Content-Type: application/json { "findingIds": [ "CVE-2024-1234@pkg:deb/debian/curl@7.64.0-4", "CVE-2024-5678@pkg:npm/lodash@4.17.20", "GHSA-abc123@pkg:pypi/requests@2.25.0" ], "forceRecalculate": false, "includeBreakdown": true } ``` **Response (200 OK):** ```json { "results": [ { "findingId": "...", "score": 78, "bucket": "ScheduleNext", ... }, { "findingId": "...", "score": 45, "bucket": "Investigate", ... }, { "findingId": "...", "score": 92, "bucket": "ActNow", ... } ], "summary": { "total": 3, "succeeded": 3, "failed": 0, "byBucket": { "actNow": 1, "scheduleNext": 1, "investigate": 1, "watchlist": 0 }, "averageScore": 71.7, "calculationTimeMs": 45 }, "errors": [], "policyDigest": "sha256:abc123...", "calculatedAt": "2026-01-15T14:30:00Z" } ``` **Limits:** - Maximum batch size: 100 findings - Rate limit: 10 requests/minute ## Score History **Request:** ```http GET /api/v1/findings/{findingId}/score-history?from=2026-01-01&to=2026-01-15&limit=50 Authorization: Bearer X-Stella-Tenant: acme-corp ``` **Parameters:** - `from` (query, optional): Start date (ISO-8601) - `to` (query, optional): End date (ISO-8601) - `limit` (query, optional): Max entries (1-100, default 50) - `cursor` (query, optional): Pagination cursor **Response (200 OK):** ```json { "findingId": "CVE-2024-1234@pkg:deb/debian/curl@7.64.0-4", "history": [ { "score": 78, "bucket": "ScheduleNext", "policyDigest": "sha256:abc123...", "calculatedAt": "2026-01-15T14:30:00Z", "trigger": "evidence_update", "changedFactors": ["rts", "xpl"] }, { "score": 65, "bucket": "Investigate", "policyDigest": "sha256:abc123...", "calculatedAt": "2026-01-10T09:15:00Z", "trigger": "scheduled", "changedFactors": [] } ], "pagination": { "hasMore": true, "nextCursor": "eyJvZmZzZXQiOjUwfQ==" } } ``` **Trigger Types:** - `evidence_update` - New evidence received - `policy_change` - Policy weights changed - `scheduled` - Periodic recalculation - `manual` - User-initiated recalculation ## Scoring Policy ### Get Active Policy **Request:** ```http GET /api/v1/scoring/policy Authorization: Bearer X-Stella-Tenant: acme-corp ``` **Response (200 OK):** ```json { "version": "ews.v1.2", "digest": "sha256:abc123...", "activeSince": "2026-01-01T00:00:00Z", "environment": "production", "weights": { "rch": 0.30, "rts": 0.25, "bkp": 0.15, "xpl": 0.15, "src": 0.10, "mit": 0.10 }, "guardrails": { "notAffectedCap": { "enabled": true, "maxScore": 15 }, "runtimeFloor": { "enabled": true, "minScore": 60 }, "speculativeCap": { "enabled": true, "maxScore": 45 } }, "buckets": { "actNowMin": 90, "scheduleNextMin": 70, "investigateMin": 40 } } ``` ## Webhooks ### Register Webhook **Request:** ```http POST /api/v1/scoring/webhooks Authorization: Bearer X-Stella-Tenant: acme-corp Content-Type: application/json { "url": "https://example.com/webhook/scores", "secret": "webhook-secret-key", "findingPatterns": ["CVE-*", "GHSA-*"], "minScoreChange": 10, "triggerOnBucketChange": true } ``` **Response (201 Created):** ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://example.com/webhook/scores", "hasSecret": true, "findingPatterns": ["CVE-*", "GHSA-*"], "minScoreChange": 10, "triggerOnBucketChange": true, "createdAt": "2026-01-15T14:30:00Z" } ``` ### Webhook Payload When a score change triggers a webhook: ```json { "event_type": "score.changed", "finding_id": "CVE-2024-1234@pkg:deb/debian/curl@7.64.0-4", "previous_score": 65, "current_score": 78, "previous_bucket": "Investigate", "current_bucket": "ScheduleNext", "score_change": 13, "bucket_changed": true, "policy_digest": "sha256:abc123...", "timestamp": "2026-01-15T14:30:00Z" } ``` ### Webhook Signature If a secret is configured, payloads include an HMAC-SHA256 signature: ``` X-Webhook-Signature: sha256= X-Webhook-Id: X-Webhook-Timestamp: ``` Verify by computing: `HMAC-SHA256(secret, payload_body)` ### Webhook Delivery - Retry policy: 4 attempts with exponential backoff (100ms, 500ms, 2s, 5s) - Timeout: 30 seconds per attempt - Success: HTTP 2xx response - Fire-and-forget: Does not block score calculation ## Error Responses All errors follow the standard envelope: ```json { "code": "SCORING_FINDING_NOT_FOUND", "message": "Finding 'CVE-2024-1234@...' not found or no evidence available", "traceId": "01HXYZABCD1234567890" } ``` ### Error Codes | Code | HTTP Status | Description | |------|-------------|-------------| | `SCORING_FINDING_NOT_FOUND` | 404 | Finding doesn't exist | | `SCORING_EVIDENCE_NOT_AVAILABLE` | 404 | No evidence for scoring | | `SCORING_POLICY_NOT_FOUND` | 404 | Policy version doesn't exist | | `SCORING_CALCULATION_FAILED` | 400 | Score calculation error | | `SCORING_BATCH_TOO_LARGE` | 400 | Batch exceeds 100 findings | | `SCORING_RATE_LIMIT_EXCEEDED` | 429 | Rate limit hit | | `SCORING_INVALID_REQUEST` | 400 | Malformed request | ## Observability ### Metrics | Metric | Type | Description | |--------|------|-------------| | `ews_calculations_total` | Counter | Total calculations by bucket/result | | `ews_calculation_duration_seconds` | Histogram | Calculation latency | | `ews_batch_calculations_total` | Counter | Batch operations | | `ews_cache_hits_total` | Counter | Cache hits | | `ews_cache_misses_total` | Counter | Cache misses | | `ews_webhooks_delivered_total` | Counter | Webhook deliveries | | `ews_bucket_distribution_*` | Gauge | Findings per bucket | ### Tracing All operations emit OpenTelemetry spans: - `EWS.Calculate` - Single score calculation - `EWS.CalculateBatch` - Batch calculation - `EWS.WebhookDelivery` - Webhook delivery Span attributes include: `finding_id`, `score`, `bucket`, `policy_digest`, `duration_ms`, `from_cache` ## Examples ### CLI Usage ```bash # Calculate score curl -X POST "https://api.stellaops.local/api/v1/findings/CVE-2024-1234@pkg:npm/lodash@4.17.20/score" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Stella-Tenant: acme-corp" \ -H "Content-Type: application/json" \ -d '{"includeBreakdown": true}' # Get cached score curl "https://api.stellaops.local/api/v1/findings/CVE-2024-1234@pkg:npm/lodash@4.17.20/score" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Stella-Tenant: acme-corp" # Batch calculation curl -X POST "https://api.stellaops.local/api/v1/findings/scores" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Stella-Tenant: acme-corp" \ -H "Content-Type: application/json" \ -d '{"findingIds": ["CVE-2024-1234@...", "CVE-2024-5678@..."]}' ``` ## Changelog | Version | Date | Changes | |---------|------|---------| | 1.0-beta | 2025-12-25 | Initial API release (Sprint 8200.0012.0004) |