The problem identity solves
Run any scanner twice and you get two reports. Nothing in either one connects a row in the second to a row in the first, so a decision recorded against last night’s report has nowhere to live tonight. A user who marks CVE-2024-1234 in openssl onservice-a as “accepted risk,
expires 2027-01-01” must still see that decision after tonight’s scan, next
month’s scan, and after TRUSTIVAN changes its detection engine. The fingerprint
is what supplies that link, and therefore what turns a scanner into a product.
Fingerprints are derived from content
A fingerprint is a versioned hash —tof1: followed by a SHA-256 digest — over
the asset identity, the finding kind, and the fields that make the issue what
it is.
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.
Fields are length-prefixed before hashing rather than joined with a separator.
With a separator, ("ab", "c") and ("a", "bc") produce the same input, so a
package named foo:bar could collide with a package foo and a rule bar.
Length prefixing makes a collision require a SHA-256 collision.
What is deliberately excluded
Severity, CVSS score, scanner ID, scanner version, database version, scan timestamps, line and column numbers, the installed version and the fixed version are not part of identity. Every one of them changes while the underlying issue does not:
That last one is the point of the whole 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. It is what makes the engine genuinely
replaceable rather than nominally so.
The version prefix exists because the algorithm will eventually need to change.
A
tof2 can coexist with tof1 in one table, and the migration can be written
as “recompute and carry the decisions across” rather than “hope nothing
breaks”. Every field that feeds a fingerprint is also stored as its own column,
so that recomputation is an UPDATE rather than a re-scan of every asset in
every tenant.
The tenant is excluded too
The same CVE, in the same package, on the same asset is the same security issue regardless of who owns the asset. Isolation does not depend on the fingerprint and must not — it is enforced byorg_id and scoped queries, as described in
Tenancy and isolation. A finding is keyed by
(org_id, asset_id, fingerprint); the fingerprint alone is never a lookup key.
Mixing the tenant into identity would provide no isolation that scoping does
not already provide, while destroying portability.
What a finding carries
Beyond identity and status, a finding carries the fields a backlog is worked from: severity on TRUSTIVAN’s own scale, the advisory identifier, the component and its versions, the location, when it was first seen, and which scan last saw it. Severities are TRUSTIVAN’s, not an engine’s —critical, high, medium,
low, none, unknown. 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.
first_seen_at is when TRUSTIVAN first observed the finding, and it survives
resolution and reopening. It is what age is measured from, because “this has
been outstanding for 40 days” is a statement about remediation, not about when
the advisory was published.
Fixability has three values
fixable— the advisory names a fixed version.unfixable— the source positively asserts that no fix exists.unknown— TRUSTIVAN cannot tell.
unknown. No source TRUSTIVAN consumes today distinguishes the two, so
unfixable is modelled but never produced — a policy written against it is
valid the day a source does, and will not fire before then.
Collapsing “no fix recorded” into “unfixable” would make a rule of “block only
on things we can fix” silently stop blocking on exactly the findings nobody has
looked at.
Listing findings
order=recent for anything that enumerates. It orders by immutable keys,
so the traversal is exhaustive: every finding present when you started and not
deleted is visited exactly once. order=severity is the backlog view and sorts
by a value that changes, so a re-rated finding can move between pages and be
seen twice or missed. Each response reports page.stable so a client can
assert this rather than assume it.
Findings can be filtered by status, severity, kind and asset_id.
The timeline is evidence
Every change to a finding writes a row to its event log, readable atGET /api/v1/findings/{id}/events.
The table is append-only, and that is 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. Stated honestly, the
guarantee has edges:
- It stops the application from rewriting history, including by accident — an
UPDATEwith a missingWHERE, or a well-meaning migration that “fixes” an event type. - It does not stop a database superuser, who can drop the trigger. Nothing inside the database can.
DELETEis permitted, because deleting a tenant must remain possible and the cascade reaches these rows. Erasing a customer is a deliberate act on a whole tenant; rewriting one event is not.
Who did it is structural
There are four kinds of actor, and each is recorded in its own column rather than in a shared “actor” string:
The absence is the record. A suppression that expired is a fact nobody is
accountable for, and a column asserting otherwise would be a small permanent
lie. Crucially, no audit row ever claims a person did what a pipeline did:
a machine principal has no user ID to write, so those columns are
NULL and
the key’s identity goes in its own column.
The audit coverage that exists today
Stated plainly, because a security product being vague about its own audit coverage is worse than having narrow coverage:
The vocabulary those events are written in is shared, so the second audited
resource will read against the same fields rather than inventing its own shape.
But the table for it does not exist yet, and neither do the entries.
Where to go next
- The finding lifecycle — the five states and what may move between them.
- Suppression — the one action whose misuse is invisible by design.
- Assets and scans — where findings come from.

