The risk score
Every finding carries a risk score from 0 to 100 and the complete breakdown behind it. The dashboard shows the breakdown as Why this priority on the finding page; the API returns it asrisk on every finding (model: "risk-v2").
There is no learned weight and no hidden input: you can recompute any score
from its breakdown.
Worked examples: an unexploited critical (CVSS 9.8, EPSS 2%) with a fix scores
45; a high (CVSS 7.5) in the KEV catalogue scores 65; Log4Shell running
on a critical asset scores 100; that same high, if it only reaches the
software through a development dependency, scores 23 instead of 38 — still
visible, ranked below anything on a real runtime path. An exploited high
outranks an unexploited critical, which is the point.
Each factor records its source and the date of its data (the KEV
catalogue version, the EPSS model and score date, when the runtime edge was
last observed). Internet exposure is shown as “not recorded”: no collector
records it yet, so it is not an input.
Unknown is not no. With no KEV catalogue loaded the known-exploited factor
reads “unknown” and
risk.known_exploited is null, never false.
Filtering by threat
GET /api/v1/findings takes known_exploited=true|false (three states — the
absent one does not filter) and min_epss=<0-1>. Both read the STORED
assessment; neither triggers a live KEV or EPSS lookup. A finding never
scored, or scored where EPSS is off, has no value and never matches
min_epss — absence of a score is not evidence it is low.
Reachability: dependency path
Severity and threat describe the advisory. They say nothing about whether your build actually ships the vulnerable code. TrustIvan adds the one reachability signal that generalises across ecosystems without adding a new collector: whether the scanner’s own manifest parse marks the package a development or test dependency (devDependency in npm, and the equivalent
concept where the ecosystem has one). A vulnerability in jest scores lower
than the identical CVE in express, because only one of them ships.
This is deliberately narrower than full call-graph reachability (“is the
vulnerable symbol imported”), which no scanner here performs across
ecosystems. Direct-vs-transitive dependency depth is recorded in the
component graph (GET /assets/{id}/dependents) but is not yet a scoring
input — see Roadmap for what is tracked and not yet built.
Suppression and resolution
The score describes the finding in its context and does not change when you triage it. Suppressing or resolving a finding takes it out of the risk order instead:risk_priority — the key order=risk sorts by — is the score
for an open, reopened or triaged finding, and 0 for a suppressed or resolved
one. The accepted assessment stays readable, and a policy can still see it.
When scores update
The risk pass re-scores an asset’s findings after every scan of it, and every finding after each feed refresh (daily by default). Changing an asset’s criticality re-scores its findings immediately. A newruns edge is picked up
by the next pass. risk_scored_at is when the assessment last changed.
SLA clocks
An organization can set a remediation target, in days, per severity:PUT /api/v1/settings/sla/{severity} (critical, high, medium or low).
No severity has a target until you set one — TrustIvan does not invent an
industry-default SLA and grade you against a policy you never wrote. Manage
the policy from Policies in the dashboard, or the API directly.
Once a target is set, every finding of that severity carries:
All three fields are absent — not
false, not zero — on a finding whose
severity has no configured target. An unconfigured SLA is not the same fact
as “on time”.
The two feeds
A vulnerability matches a feed by its CVE ID or by any of its aliases, so a
finding reported under a GitHub advisory ID still matches the CVE it names.
How the feeds stay current
The worker fetches each feed on a schedule (daily by default,RISK_FEED_REFRESH_HOURS), stores it, and re-applies it to findings already
stored — you do not need to rescan an asset for a newly catalogued
vulnerability to be flagged on it. When a finding’s known-exploited status
changes this way, the change is written to the finding’s timeline with the
catalogue version it came from.
When a feed cannot be fetched
A failed fetch, an error page served with a200, or a document with nothing
in it never replaces the stored feed. Replacing it with an empty one would
mark the whole estate not-exploited with the authority of a loaded catalogue.
Instead:
- the previous data stays in force, and the error is recorded;
- the fetch is retried within the hour;
- once no refresh has succeeded for 72 hours, or once the document itself is older than 14 days (KEV) or 72 hours (EPSS), the data is flagged stale. Stale data is still used — a week-old exploitation catalogue is far better evidence than none — but it is never presented as fresh.
GET /api/v1/system/status reports each feed under exploits (KEV) and
epss: its source, version, publication date, when it last loaded, whether it
is stale, and the last error.
A feed that has never loaded is reported as not configured, and its signal
as unknown — never as “not exploited”.
Air-gapped deployments
SetKEV_SOURCE=file and EPSS_SOURCE=file with KEV_PATH / EPSS_PATH
pointing at documents you place on the worker’s filesystem. The worker re-reads
them on every refresh, so replacing the file is how an offline site updates,
and an old file turns stale on the same rules as a failed fetch. See
Environment variables.
