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 & route | Scope (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}/level | risk-engine:read | { cveId, level }; 404 when undetermined. |
GET /exploit-maturity/{cveId}/history | risk-engine:read | { cveId, entries }. |
POST /exploit-maturity/batch | risk-engine:operate (RiskEngine.Operate) | Body { cveIds: [...] }; returns { results, errors }. |
GET /risk-scores/providers | risk-engine:read | Sorted list of registered provider names. |
POST /risk-scores/jobs | risk-engine:operate | Enqueues, runs synchronously, returns 202 with { jobId, result }. Audited (create_score_job). |
GET /risk-scores/jobs/{jobId} | risk-engine:read | Stored RiskScoreResult; 404 if absent. |
POST /risk-scores/simulations | risk-engine:operate | Evaluates a request collection without persisting; returns { results }. Audited (create_simulation). |
POST /risk-scores/simulations/summary | risk-engine:operate | As above plus aggregate { summary, results } (avg/min/max + top-3 movers). Audited (create_simulation). |
GET /healthz, GET /readyz, GET /buildinfo.json, GET /api/v1/buildinfo | anonymous | Liveness, 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.Workeris not a placeholder loop — it is a snapshot exporter.Worker.ExecuteAsyncexports once on startup and then on aPeriodicTimer(Worker:ExportIntervalSeconds, default 3600 s, floored at 60 s): it readsvuln.advisory_cvssandvuln.kev_flagsfrom Concelier’s Postgres (Worker:ConcelerConnectionString/ConnectionStrings:Concelier) and writescvss-snapshot.gz+kev-snapshot.gzintoWorker: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:
- Findings Ledger module dossier — documents the Sprint 207 consolidation and the RiskEngine runtime
- Historical (pre-consolidation) docs:
docs-archive/modules/risk-engine/
