A finding is the current state of one issue on one asset. Not one row per observation: one row per issue, updated across every scan that sees it. Everything below is what that row carries and how to read it.

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.
The third is load-bearing. A fixed version being absent means the evidence does not say — never “no fix exists”. Collapsing “no fix recorded” into “unfixable” would make a policy of “block only on things we can fix” silently stop blocking on exactly the findings nobody has looked at. 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, and PATCH with status: "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 from open because a regression is a different fact from a first sighting.
Not every transition is legal, and the transition table is the authority. See The finding lifecycle and Suppression.

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 event log is append-only, enforced by a database trigger rather than by a comment. An audit trail the application can silently rewrite is not evidence, which is every use the table exists for. Each event names its actor structurally, in its own column rather than in a shared string: 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

Two orders exist and they offer different guarantees: 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.
Filters are part of what the cursor is bound to, so decide them before you start paging. Page size is capped at 200 and clamped rather than refused; page.limit reports the effective value.

Where to go next