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.

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.csScoringReadPolicy/ScoringWritePolicy/ScoringAdminPolicy bind to StellaOpsScopes.VulnOperate.)

PolicyDescriptionEndpoints
scoring.readRead score dataGET endpoints
scoring.writeCalculate scoresPOST score endpoints
scoring.adminWebhook managementAll webhook endpoints

Required Headers

HeaderRequiredDescription
Authorization: Bearer <jwt>YesRS256/ES256 token with appropriate scopes
X-StellaOps-TenantIdYesTenant slug/UUID
Content-TypeYes (POST/PUT)application/json
traceparentRecommendedW3C trace context

Endpoints Summary

MethodPathRate LimitDescription
POST/api/v1/findings/{findingId}/score100/minCalculate score for finding
GET/api/v1/findings/{findingId}/score1000/minGet cached score
POST/api/v1/findings/scores10/minBatch calculate (max 100)
GET/api/v1/findings/{findingId}/score-history100/minGet score history
GET/api/v1/scoring/policy100/minGet active policy
GET/api/v1/scoring/policy/{version}100/minGet policy version
POST/api/v1/scoring/webhooks10/minRegister webhook
GET/api/v1/scoring/webhooks10/minList webhooks
GET/api/v1/scoring/webhooks/{id}10/minGet webhook
PUT/api/v1/scoring/webhooks/{id}10/minUpdate webhook
DELETE/api/v1/scoring/webhooks/{id}10/minDelete 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:

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

FieldTypeDescription
reductionProfileobjectReduction profile configuration (when enabled)
reductionProfile.enabledbooleanWhether attested-reduction is active
reductionProfile.modestring"aggressive" or "conservative"
reductionProfile.profileIdstringProfile identifier for audit trail
reductionProfile.maxReductionPercentintegerMaximum score reduction allowed (0-100)
reductionProfile.requireVexAnchoringbooleanWhether VEX must be anchored to qualify
reductionProfile.requireRekorVerificationbooleanWhether Rekor verification is required
hardFailbooleantrue if anchored evidence confirms active exploitation
shortCircuitReasonstringReason for short-circuit (if score was short-circuited)
anchorobjectPrimary evidence anchor metadata (if available)

Short-Circuit Reasons

ReasonScore EffectCondition
anchored_vex_not_affectedScore = 0Verified VEX not_affected/fixed attestation
anchored_affected_runtime_confirmedScore = 100 (hard fail)Anchored VEX affected + anchored runtime confirms vulnerability

Evidence Anchor Fields

FieldTypeDescription
anchor.anchoredbooleanWhether evidence has cryptographic attestation
anchor.envelopeDigeststringDSSE envelope digest (sha256 hex)
anchor.predicateTypestringAttestation predicate type URL
anchor.rekorLogIndexintegerSigstore Rekor transparency log index
anchor.rekorEntryIdstringRekor entry UUID
anchor.scopestringAttestation scope (finding, package, image)
anchor.verifiedbooleanWhether attestation signature was verified
anchor.attestedAtstringISO-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

BucketScore RangeAction
ActNow90-100Immediate remediation required
ScheduleNext70-89Schedule for upcoming sprint
Investigate40-69Needs further analysis
Watchlist0-39Monitor, low priority

Evidence Inputs

CodeFull NameDescription
rchReachabilityStatic/dynamic code path analysis (0-1)
rtsRuntime SignalProduction observation frequency (0-1)
bkpBackportVendor backport availability (0-1)
xplExploitEPSS/KEV exploit likelihood (0-1)
srcSource TrustAdvisory source trust level (0-1)
mitMitigationApplied mitigations effectiveness (0-1)

Guardrails (Caps & Floors)

GuardrailEffectCondition
speculativeCapMax 45No runtime evidence
notAffectedCapMax 15VEX status = not_affected
runtimeFloorMin 60Observed 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:

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:

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:

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:

Short-Circuit Rules:

  1. Anchored VEX not_affected/fixed → Score = 0 (immediate watchlist)
  2. 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):

FlagDescription
attested-reductionAttested-reduction scoring path was used
anchored-vexAnchored VEX evidence triggered precedence
anchored-backportAnchored backport evidence applied reduction
anchored-reachabilityAnchored reachability evidence applied bonus
anchored-runtimeAnchored runtime evidence affected score
hard-failHard-fail triggered (affected + runtime confirmed)
epss-reducedEPSS 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

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

CodeHTTP StatusDescription
SCORING_FINDING_NOT_FOUND404Finding doesn’t exist
SCORING_EVIDENCE_NOT_AVAILABLE404No evidence for scoring
SCORING_POLICY_NOT_FOUND404Policy version doesn’t exist
SCORING_CALCULATION_FAILED400Score calculation error
SCORING_BATCH_TOO_LARGE400Batch exceeds 100 findings
SCORING_RATE_LIMIT_EXCEEDED429Rate limit hit
SCORING_INVALID_REQUEST400Malformed request

Observability

Metrics

MetricTypeDescription
ews_calculations_totalCounterTotal calculations by bucket/result
ews_calculation_duration_secondsHistogramCalculation latency
ews_batch_calculations_totalCounterBatch operations
ews_cache_hits_totalCounterCache hits
ews_cache_misses_totalCounterCache misses
ews_webhooks_delivered_totalCounterWebhook deliveries
ews_bucket_distribution_*GaugeFindings per bucket

Tracing

All operations emit OpenTelemetry spans:

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

VersionDateChanges
1.0-beta2025-12-25Initial API release (Sprint 8200.0012.0004)