save development progress
This commit is contained in:
430
docs/api/findings-scoring.md
Normal file
430
docs/api/findings-scoring.md
Normal file
@@ -0,0 +1,430 @@
|
||||
# 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 <jwt>` | 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 <token>
|
||||
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 <token>
|
||||
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 <token>
|
||||
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 <token>
|
||||
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 <token>
|
||||
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=<hex-encoded-hmac>
|
||||
X-Webhook-Id: <webhook-uuid>
|
||||
X-Webhook-Timestamp: <unix-epoch-seconds>
|
||||
```
|
||||
|
||||
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) |
|
||||
Reference in New Issue
Block a user