Identity
Two fields identify a finding, and they answer different questions.identifier is the advisory ID — CVE-2024-1234, GHSA-xxxx-…. It is
what you search for, what you paste into a ticket, and what tells you whether
two findings on different assets are the same underlying issue. “Who is
affected by CVE-2024-1234” is GET /findings?q=CVE-2024-1234, or the search box
on the Findings page; q also matches part of a package name, so q=log4j
finds log4j-core. From a vulnerability’s page, “Where else” lists every asset
that has the same package, whatever its version.
fingerprint is the finding’s identity within TRUSTIVAN — a versioned
hash, tof1: followed by a SHA-256 digest, derived from the asset identity, the
finding kind, and the fields that make the issue what it is. For a vulnerability
that is the advisory ID, the component (ecosystem and name), and the install
path.
The fingerprint is what survives a rescan. A finding is keyed by
(organisation, asset, fingerprint), so tonight’s scan matches tonight’s
result to the row you triaged last week rather than creating a new one. It is
also why identity excludes everything that changes while the issue does not:
severity, CVSS score, installed version, fixed version, line numbers, and every
engine-internal identifier. Include severity and a re-rated CVE becomes a new
finding with its triage reset; include the installed version and a dependency
bump that is still vulnerable resets every decision on every upgrade.
That last exclusion is the point of the design. Identity is built from facts any
engine reports — a CVE ID, a package name, a file path — never from an engine’s
internal identifiers, which is what makes the detection engine genuinely
replaceable rather than nominally so.
You will rarely need to compute a fingerprint. You will occasionally want to
match on one, and it is stable enough to do so. See Findings and
identity.
Severity is TRUSTIVAN’s scale
critical, high, medium, low, none, unknown.
These are normalised, not passed through. An engine’s enum is an
implementation detail of an adapter, and a customer’s policy must not need
rewriting because an engine renamed a level or changed its mapping.
unknown is a real value and not a gap to be filled in with a guess: it means
the evidence does not establish a severity. A backlog view that silently sorts
unknown as low is making a claim the data does not support.
Severity changes. CVSS scores are revised routinely, which is exactly why it
is not part of identity — and also why ordering a traversal by it is unsafe. See
Enumerating below.
Kind
kind discriminates what the finding is about and which detail it carries:
vulnerability, secret, misconfiguration and license.
Today you will see vulnerability unless you have asked for more. Secret
and misconfiguration detection are off by default and are enabled per target
kind with /api/v1/settings/scanners; their findings appear in the dashboard
beside vulnerabilities, and a finding’s page shows the evidence its kind
carries: the rule that fired, the file and line, the check’s own resolution
where it gives one, and a secret’s match only in redacted form.
license is the exception worth knowing about. Licence data is not read
from findings — a licence is a fact about an asset, not a judgement, and
modelling it as a finding would let it be suppressed. Read the licence
inventory from /api/v1/licenses instead; see the FAQ.
Fixability has three values
Derived from the advisory evidence:fixable— a fixed version is recorded. This is the actionable set.unfixable— the source positively asserts that no fix exists.unknown— the evidence does not say.
unfixable is currently never produced. No source TRUSTIVAN consumes
distinguishes “the advisory records no fix” from “we did not receive that
field”. It is modelled anyway so that a policy written against it is valid the
day a source does, and documented here so nobody writes one expecting it to fire
today. That is a gap in the data, recorded honestly, rather than a guess dressed
as a fact.
Package and versions
For a vulnerability:
The advisory’s
fixed_version lives in the finding’s detail object alongside
the installed version, the CVSS vector and score, and the advisory’s own
publication and modification dates.
The ecosystem is part of a component’s identity for correctness, not tidiness:
crypto exists in npm, RubyGems and PyPI as three unrelated packages, and
without the namespace a vulnerability in one would share identity with the
others.
The remediation you want is usually component_version → fixed_version for
component_name. That is the whole of what a fix instruction is, and it is
why fixable is the filter to start a triage session from.
Time, and what it measures
first_seen_at is when TRUSTIVAN first observed this finding — not when the
advisory was published. It survives resolution and reopening, so a finding
that was fixed and regressed keeps its original age.
That is deliberate: “this critical has been outstanding for 40 days” is a
statement about your remediation, not about the CVE’s history, and resetting it
on a regression would flatter the number every time a fix failed to hold.
last_seen_at and last_seen_scan_id are the most recent scan that
still found it. Resolution works by absence — a finding a scan should have found
and did not is resolved — so these are what that decision is made from.
Status
Five states:open, triaged, suppressed, resolved, reopened.
open— seen, undecided.triaged— a human has looked at it and it remains in the queue.suppressed— risk accepted, with a reason and usually an expiry. It stops appearing in the backlog and stops counting towards severity totals.resolved— a scan that would have found it did not. Evidence, not opinion: you cannot set this yourself, andPATCHwithstatus: "resolved"is refused. The way to close something you fixed is to rescan; the way to close something you decided not to fix is to suppress it.reopened— it was resolved and came back. Distinct fromopenbecause a regression is a different fact from a first sighting.
The timeline is evidence
GET /findings/{id} returns the finding and the first 50 events of its
timeline inline, newest first. For most findings that is the whole history and
one request is enough.
When there are more, page through them:
The absence is the record. And no event can claim a person did what a pipeline
did: a machine principal has no user ID to write, so those columns are
NULL by
construction and the key’s identity goes in its own column.
Audit coverage extends past findings and verdicts: sign-ins and sign-outs,
policy authoring, webhook endpoints, credentials, API keys, workloads, asset
creation and deletion, identity providers, domain verification, SCIM tokens,
and membership and role changes are recorded too, each with the client address
and request ID it came from. Those entries live in their own table and are read
through GET /api/v1/identity/audit or Settings → Audit log. That list is a
narrower claim than “audit logging”, and the accurate one.
Enumerating safely
Use
order=recent for anything that enumerates. severity is the backlog
view — worst first, for a human working down a list — and its ordering keys
change underneath it. For a person browsing, a row that moves is an annoyance.
For a machine enumerating findings to decide whether a release ships, it is a
clean result reported by a gate that never saw the row that mattered.
Every response reports page.stable, so a client can assert the guarantee
rather than discover its absence from a support ticket. An unknown order is
refused rather than defaulted, because falling back would hand a caller who
asked for the exhaustive order the browsable one — and it would traverse happily
while missing rows.
Pagination is by cursor rather than offset. LIMIT 50 OFFSET 50 re-runs the
ordering and counts forward, so rows inserted before the cursor position between
two requests shift everything and the client silently skips rows. Findings
are written constantly.
Cursors are signed and bound to a tenant, an order and a filter set, and expire
after 24 hours. Do not construct one, do not edit one, and do not resume a
traversal across a filter change — the API will refuse it rather than return
neither the old set nor the new one. See
Pagination.
Filtering
status, severity and kind accept comma-separated lists; asset_id takes
one asset.
page.limit reports
the effective value.
Where to go next
- The finding lifecycle — the five states and the legal transitions.
- Suppression — accepting risk, and why it is its own scope.
- Bulk triage — moving many findings at once.

