RiskEngine

Deterministic, explainable risk scoring and exploit-maturity assessment for Stella Ops findings.

Status: active — consolidated into the Findings domain (Sprint 207).

The RiskEngine source lives under src/Findings/ (StellaOps.RiskEngine.Core, StellaOps.RiskEngine.WebService, StellaOps.RiskEngine.Worker, and __Libraries/StellaOps.RiskEngine.Infrastructure). It computes deterministic, explainable risk scores by running registered scoring providers over EPSS, CVSS, KEV, VEX, and fix-exposure signals, and persists job results in its own PostgreSQL riskengine schema (in-memory storage is test-only).

Service surface

The StellaOps.RiskEngine.WebService host (Program.cs) exposes a Stella-Router-fronted minimal API. All business endpoints require an Authority bearer token; health, OpenAPI, and build-info endpoints are anonymous.

Method & routeScope (policy)Notes
GET /exploit-maturity/{cveId}risk-engine:read (RiskEngine.Read)Full ExploitMaturityResult. Returns 501 risk_sources_unavailable when sources are not wired.
GET /exploit-maturity/{cveId}/levelrisk-engine:read{ cveId, level }; 404 when undetermined.
GET /exploit-maturity/{cveId}/historyrisk-engine:read{ cveId, entries }.
POST /exploit-maturity/batchrisk-engine:operate (RiskEngine.Operate)Body { cveIds: [...] }; returns { results, errors }.
GET /risk-scores/providersrisk-engine:readSorted list of registered provider names.
POST /risk-scores/jobsrisk-engine:operateEnqueues, runs synchronously, returns 202 with { jobId, result }. Audited (create_score_job).
GET /risk-scores/jobs/{jobId}risk-engine:readStored RiskScoreResult; 404 if absent.
POST /risk-scores/simulationsrisk-engine:operateEvaluates a request collection without persisting; returns { results }. Audited (create_simulation).
POST /risk-scores/simulations/summaryrisk-engine:operateAs above plus aggregate { summary, results } (avg/min/max + top-3 movers). Audited (create_simulation).
GET /healthz, GET /readyz, GET /buildinfo.json, GET /api/v1/buildinfoanonymousLiveness, readiness, and build provenance.

Scope constants are defined in StellaOpsScopes.RiskEngineRead/RiskEngineOperate; the named policies (RiskEngine.Read, RiskEngine.Operate) are declared in RiskEnginePolicies.

Scoring providers

Providers implement IRiskScoreProvider and are registered in RiskScoreProviderRegistry. The registered set is: default-transforms, cvss-kev, epss, cvss-kev-epss, vex-gate, and fix-exposure. ScoreRequest (Provider, Subject, Signals) is scored into a RiskScoreResult (JobId, Provider, Subject, Score, Success, Error, Signals, CompletedAtUtc).

Exploit-maturity assessment is independent of the scoring providers and uses the ExploitMaturityLevel taxonomy (Unknown=0, Theoretical=1, ProofOfConcept=2, Active=3, Weaponized=4) with per-source MaturitySignals (Epss, Kev, InTheWild, ExploitDb, ScannerTemplate, Override).

Persistence

Migrations auto-apply on startup via AddStartupMigrations("riskengine", "RiskEngine", ...). Migration 001_initial_schema.sql creates schema riskengine and table riskengine.risk_score_results (job_id PK, provider, subject, score, success, error, signals JSONB, completed_at) plus an index on completed_at DESC. The storage driver is selected by Storage:Driver / RiskEngine:Storage:Driver (postgres default; inmemory rejected outside the Testing environment); connection string resolves from RiskEngine:Storage:Postgres:ConnectionString, Storage:Postgres:ConnectionString, or ConnectionStrings:Default. EPSS sources are loaded from RiskEngine:Sources:Epss:BundlePath / :DirectoryPath / :SnapshotPath.

Note (corrected 2026-07-12): StellaOps.RiskEngine.Worker is not a placeholder loop — it is a snapshot exporter. Worker.ExecuteAsync exports once on startup and then on a PeriodicTimer (Worker:ExportIntervalSeconds, default 3600 s, floored at 60 s): it reads vuln.advisory_cvss and vuln.kev_flags from Concelier’s Postgres (Worker:ConcelerConnectionString / ConnectionStrings:Concelier) and writes cvss-snapshot.gz + kev-snapshot.gz into Worker:BundleOutputDirectory. When either the connection string or the output directory is unset it logs at Debug and skips — so an unconfigured host is a no-op, not a failure.

What remains true: the worker does not process risk-score jobs. Those are still handled synchronously inside the WebService request handler (RiskScoreWorker.ProcessNextAsync).

docs/modules/riskengine/ contains this README, the auto-generated api-reference.md, and the openapi/ spec snapshot it is generated from. For architecture, persistence, and operations narrative, see: