Evidence-Weighted Score (EWS) API
The Evidence-Weighted Score (EWS) API calculates, retrieves, and manages prioritization scores for vulnerability findings in StellaOps. It aggregates multiple evidence signals — reachability, runtime observation, exploit likelihood, source trust, and applied mitigations — into a single 0–100 score with an action-bucket classification, so teams remediate by real-world risk rather than raw CVSS.
Audience: platform engineers and integrators wiring findings prioritization into release gates, dashboards, and automation.
Status: v1.0-beta. API endpoints are complete; the score-calculation contract is stable. See the Changelog for version history.
Related documentation
- API Overview — shared conventions for all StellaOps APIs.
- Signals API — signal collection and correlation.
- OpenAPI specification — full OpenAPI 3.0 contract for the Findings Ledger service.
Authentication & Authorization
Required Scopes
All scoring endpoints (read, write, and webhook/admin) are gated by the same configurable scope set. By default this is vuln:operate; it can be overridden via the findings:ledger:Authority:RequiredScopes configuration key. (Source: src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs — ScoringReadPolicy/ScoringWritePolicy/ScoringAdminPolicy bind to StellaOpsScopes.VulnOperate.)
| Policy | Description | Endpoints |
|---|---|---|
scoring.read | Read score data | GET endpoints |
scoring.write | Calculate scores | POST score endpoints |
scoring.admin | Webhook management | All webhook endpoints |
Required Headers
| Header | Required | Description |
|---|---|---|
Authorization: Bearer <jwt> | Yes | RS256/ES256 token with appropriate scopes |
X-StellaOps-TenantId | 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:
POST /api/v1/findings/{findingId}/score
Authorization: Bearer <token>
X-StellaOps-TenantId: acme-corp
Content-Type: application/json
{
"forceRecalculate": false,
"includeBreakdown": true,
"policyVersion": null
}
Parameters:
findingId(path, required): Finding identifier in formatCVE-ID@pkg:PURLforceRecalculate(body, optional): Bypass cache, defaultfalseincludeBreakdown(body, optional): Include detailed inputs/weights, defaulttruepolicyVersion(body, optional): Specific policy version,nullfor latest
Response (200 OK):
{
"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
}
Attested-Reduction Mode (v1.1)
When attested-reduction scoring is enabled on the policy, the response includes additional fields for cryptographic attestation metadata and reduction profile information.
Extended Response (200 OK) with Reduction Mode:
{
"findingId": "CVE-2024-1234@pkg:deb/debian/curl@7.64.0-4",
"score": 0,
"bucket": "Watchlist",
"inputs": { "rch": 0.00, "rts": 0.00, "bkp": 1.00, "xpl": 0.30, "src": 0.90, "mit": 1.00 },
"weights": { "rch": 0.30, "rts": 0.25, "bkp": 0.15, "xpl": 0.15, "src": 0.10, "mit": 0.10 },
"flags": ["anchored-vex", "vendor-na", "attested-reduction"],
"explanations": [
"Anchored VEX statement: not_affected - score reduced to 0"
],
"caps": { "speculativeCap": false, "notAffectedCap": false, "runtimeFloor": false },
"policyDigest": "sha256:reduction123...",
"calculatedAt": "2026-01-15T14:30:00Z",
"cachedUntil": "2026-01-15T15:30:00Z",
"fromCache": false,
"reductionProfile": {
"enabled": true,
"mode": "aggressive",
"profileId": "attested-verified",
"maxReductionPercent": 100,
"requireVexAnchoring": true,
"requireRekorVerification": true
},
"hardFail": false,
"shortCircuitReason": "anchored_vex_not_affected",
"anchor": {
"anchored": true,
"envelopeDigest": "sha256:abc123def456...",
"predicateType": "https://stellaops.io/attestation/vex/v1",
"rekorLogIndex": 12345678,
"rekorEntryId": "24296fb24b8ad77a7e...",
"scope": "finding",
"verified": true,
"attestedAt": "2026-01-14T10:00:00Z"
}
}
Attested-Reduction Fields
| Field | Type | Description |
|---|---|---|
reductionProfile | object | Reduction profile configuration (when enabled) |
reductionProfile.enabled | boolean | Whether attested-reduction is active |
reductionProfile.mode | string | "aggressive" or "conservative" |
reductionProfile.profileId | string | Profile identifier for audit trail |
reductionProfile.maxReductionPercent | integer | Maximum score reduction allowed (0-100) |
reductionProfile.requireVexAnchoring | boolean | Whether VEX must be anchored to qualify |
reductionProfile.requireRekorVerification | boolean | Whether Rekor verification is required |
hardFail | boolean | true if anchored evidence confirms active exploitation |
shortCircuitReason | string | Reason for short-circuit (if score was short-circuited) |
anchor | object | Primary evidence anchor metadata (if available) |
Short-Circuit Reasons
| Reason | Score Effect | Condition |
|---|---|---|
anchored_vex_not_affected | Score = 0 | Verified VEX not_affected/fixed attestation |
anchored_affected_runtime_confirmed | Score = 100 (hard fail) | Anchored VEX affected + anchored runtime confirms vulnerability |
Evidence Anchor Fields
| Field | Type | Description |
|---|---|---|
anchor.anchored | boolean | Whether evidence has cryptographic attestation |
anchor.envelopeDigest | string | DSSE envelope digest (sha256 hex) |
anchor.predicateType | string | Attestation predicate type URL |
anchor.rekorLogIndex | integer | Sigstore Rekor transparency log index |
anchor.rekorEntryId | string | Rekor entry UUID |
anchor.scope | string | Attestation scope (finding, package, image) |
anchor.verified | boolean | Whether attestation signature was verified |
anchor.attestedAt | string | ISO-8601 attestation timestamp |
Hard-Fail Response Example
When anchored evidence confirms active exploitation:
{
"findingId": "CVE-2024-9999@pkg:npm/critical@1.0.0",
"score": 100,
"bucket": "ActNow",
"flags": ["anchored-vex", "anchored-runtime", "hard-fail", "attested-reduction"],
"explanations": [
"Anchored VEX affected + runtime confirmed vulnerable path - hard fail"
],
"hardFail": true,
"shortCircuitReason": "anchored_affected_runtime_confirmed",
"reductionProfile": {
"enabled": true,
"mode": "aggressive",
"profileId": "attested-verified",
"maxReductionPercent": 100,
"requireVexAnchoring": true,
"requireRekorVerification": true
}
}
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:
POST /api/v1/findings/scores
Authorization: Bearer <token>
X-StellaOps-TenantId: 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):
{
"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:
GET /api/v1/findings/{findingId}/score-history?from=2026-01-01&to=2026-01-15&limit=50
Authorization: Bearer <token>
X-StellaOps-TenantId: 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):
{
"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 receivedpolicy_change- Policy weights changedscheduled- Periodic recalculationmanual- User-initiated recalculation
Scoring Policy
Get Active Policy
Request:
GET /api/v1/scoring/policy
Authorization: Bearer <token>
X-StellaOps-TenantId: acme-corp
Response (200 OK):
{
"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
}
}
Attested-Reduction Scoring Profile
Sprint: SPRINT_20260112_004_LB_attested_reduction_scoring
When enabled, the attested-reduction profile applies precedence-based scoring using cryptographically anchored evidence:
Formula: score = clamp(base_epss * (1 + R + T) - P, 0, 1)
Where:
base_epss- EPSS score (exploit likelihood)R- Reachability bonus (applied when anchored not-reachable evidence exists)T- Telemetry bonus (applied when anchored no-observation evidence exists)P- Patch proof reduction (applied when anchored backport/fix evidence exists)
Short-Circuit Rules:
- Anchored VEX not_affected/fixed → Score = 0 (immediate watchlist)
- Anchored VEX affected + runtime confirmed → Hard fail (Score = 100, ActNow bucket)
Configuration in Policy:
{
"version": "ews.v1.3",
"weights": { ... },
"guardrails": { ... },
"buckets": { ... },
"attestedReduction": {
"enabled": true,
"precedenceList": [
"vex.not_affected",
"vex.fixed",
"backport.signed_proof",
"backport.vendor_vex",
"reachability.not_reachable",
"runtime.not_observed"
],
"reachabilityBonus": 0.3,
"telemetryBonus": 0.2,
"patchProofReduction": 0.5,
"clampMin": 0.0,
"clampMax": 1.0,
"hardFailOnAffectedWithRuntime": true,
"hardFailScore": 1.0,
"skipEpssWhenAnchored": true,
"requiredVerificationStatus": "Verified"
}
}
Anchor Metadata:
Evidence inputs can include anchor metadata for cryptographic attestation:
{
"findingId": "CVE-2024-1234@pkg:test/lib@1.0.0",
"xpl": 0.5,
"vexStatus": "not_affected",
"vexAnchor": {
"isAnchored": true,
"dsseEnvelopeDigest": "sha256:abc123...",
"predicateType": "https://stellaops.io/attestation/vex-override/v1",
"rekorLogIndex": 12345678,
"rekorEntryId": "24296fb24b8ad77a...",
"verificationStatus": "Verified",
"attestationTimestamp": "2026-01-14T10:30:00Z"
},
"backportDetails": {
"evidenceTier": "SignedProof",
"status": "Fixed",
"confidence": 0.95,
"anchor": {
"isAnchored": true,
"dsseEnvelopeDigest": "sha256:def456...",
"predicateType": "https://stellaops.io/attestation/backport/v1",
"verificationStatus": "Verified"
}
}
}
Response Flags (when attested-reduction is active):
| Flag | Description |
|---|---|
attested-reduction | Attested-reduction scoring path was used |
anchored-vex | Anchored VEX evidence triggered precedence |
anchored-backport | Anchored backport evidence applied reduction |
anchored-reachability | Anchored reachability evidence applied bonus |
anchored-runtime | Anchored runtime evidence affected score |
hard-fail | Hard-fail triggered (affected + runtime confirmed) |
epss-reduced | EPSS influence reduced due to anchored evidence |
Webhooks
Register Webhook
Request:
POST /api/v1/scoring/webhooks
Authorization: Bearer <token>
X-StellaOps-TenantId: 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):
{
"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:
{
"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:
{
"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 calculationEWS.CalculateBatch- Batch calculationEWS.WebhookDelivery- Webhook delivery
Span attributes include: finding_id, score, bucket, policy_digest, duration_ms, from_cache
Examples
CLI Usage
# 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-StellaOps-TenantId: 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-StellaOps-TenantId: acme-corp"
# Batch calculation
curl -X POST "https://api.stellaops.local/api/v1/findings/scores" \
-H "Authorization: Bearer $TOKEN" \
-H "X-StellaOps-TenantId: 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) |
