save development progress

This commit is contained in:
StellaOps Bot
2025-12-25 23:09:58 +02:00
parent d71853ad7e
commit aa70af062e
351 changed files with 37683 additions and 150156 deletions

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