Changelog

What shipped, and when

Grouped by release. Anything that changes behaviour for an existing deployment is called out rather than buried.

2026.09

Private registries, retention, a second factor, and a database that enforces the tenant boundary

  • added
  • security
  • fixed
  • changed

Added

Private registry scanning. Store a credential against a registry host — either a username with a password or personal access token, or a bearer token — and scans of images from that host present it. Available on Pro and Enterprise; the private_registries entitlement is checked when the credential is created.

There is deliberately no endpoint that reads a stored secret back, at any role. A secret that can be fetched is one that ends up in a screenshot; if you lose it, rotate it. The secret is encrypted under its own derived subkey, so a ciphertext from this column can never be decrypted as a webhook signing secret, and the scan carries a reference rather than the secret — the plaintext exists only in the few lines that hand it to a registry client, never in a job payload, a log line or an error message.

A scan whose credential cannot be resolved fails rather than falling back to an anonymous pull. The fallback risks pulling a public image that happens to share the name and reporting its findings as yours, which is the one failure a vulnerability scanner must never have.

Two-factor authentication. TOTP with single-use recovery codes. A password that is correct but unaccompanied produces a challenge and no session at all — not a partial one — and a step already spent on an account is refused even while it is still inside its time window. Enrolment revokes every other live session. Removing the factor requires proof of holding it, because a session alone being enough is exactly what turns a stolen session back into a stolen account.

Consumer OAuth logins are challenged too, since every provider callback funnels through one handoff. Enterprise SSO deliberately defers to your own identity provider. Two-factor is per-user and opt-in; an organization cannot yet require it of its members.

Retention windows are enforced. A sweep now expires each tenant’s history against the window their plan grants — 30 days on Free, a year on Pro, two years on Enterprise. These numbers were in the plan tables from the beginning and nothing read them.

One rule governs the whole thing: retention expires history, never current state. A finding that is still open is never deleted at any age — an unfixed vulnerability does not stop existing because it is old. A suppressed finding is never deleted either, because a suppression is a decision somebody made and deleting the finding would let the next scan re-create it as new, silently reversing an accepted risk. What expires is resolved findings, superseded timeline entries, and metering history. The identity audit trail is append-only and is not swept on any plan.

Security

The database now enforces the tenant boundary too. Every statement on the request path runs inside a transaction carrying the tenant, and PostgreSQL row-level security holds the same boundary the application’s scope type already did. The two fail differently, which is the reason for having both: the scope cannot catch a query that holds one and forgets to use it in the SQL text, and row-level security does not care what the query says.

The worker connects as a second role with a narrow, per-transaction cross-tenant policy rather than BYPASSRLS. BYPASSRLS is not scoped to a table or a statement — it would also strip the boundary from the worker’s ordinary per-tenant writes, so one mistake in one query would become a cross-tenant write with nothing behind it. Neither role holds it.

This is opt-in per deployment: set JOB_DATABASE_URL to a distinct job-plane role and the second layer is armed, and the process says which configuration it is in at startup. Unset, both planes share one role and isolation rests on the application boundary alone, exactly as before.

Registry credential validation is dialled through the SSRF guard. Testing a credential connects to a host the customer named and presents a secret to it, which is the same shape as a webhook destination. It now uses the guarded transport that inspects every resolved address at connect time and ignores an inherited HTTP_PROXY, so an administrator cannot have TRUSTIVAN authenticate against a cloud metadata endpoint. A deployment that has set SCAN_ALLOW_PRIVATE_REGISTRIES still validates against its own internal registry — that setting is what the guard is bound to.

Two API-key crashes closed, and a bug that made recovery codes unreadable after an encryption-key rotation.

Fixed

Four scan reliability defects. A scan whose lease expired reached a terminal failure without ever emitting scan.completed, so a pipeline waiting on that event waited forever for a scan the API already showed as failed. A worker shutting down mid-scan consumed one of the scan’s retry attempts, so three rolling restarts hitting the same scan could exhaust its budget and fail it permanently having never once actually run it to completion. Finding lifecycle accounting and the scan idempotency fingerprint were also corrected.

Failures that cannot succeed no longer retry. A reference that does not resolve, a credential the registry rejected, or a request refused before any network call now fails immediately instead of spending three attempts and several minutes on an answer that was knowable on the first try. Everything unrecognised keeps retrying exactly as before.

Two-factor no longer locks you out of the dashboard. A user with a factor enrolled was answered with a challenge and no session, which the dashboard read as a successful sign-in — landing on a page with no session and bouncing back to the login screen indefinitely. The callback now presents the verification step, and two-factor can be set up, inspected and turned off from Settings → Security rather than only through the API.

Changed

Scans have never been cancellable, and the documentation said otherwise: a canceled status was listed as one a scan can reach. Nothing has ever written it. The status tables now say so.

2026.08

Policy engine, verdicts and webhooks

  • added
  • security

Added

The policy engine. A policy is a set of declarative rules — conditions on severity, finding kind, lifecycle status, fixability, advisory identifier, age and match count — each carrying an effect of fail, warn or ignore. There is no expression language and no customer-supplied code. Evaluation is a pure function of the policy, the findings and the time, so a historical evaluation reproduces its historical answer.

Verdicts. POST /scans/{id}/verdict grades a completed scan against the enabled policy and records the outcome. Verdicts are append-only and never recomputed: asking again replays the recorded decision. The result — pass, warn or fail — is in the response body rather than the HTTP status, so a pipeline can distinguish a policy failure from an unreachable service.

Immutable policy versions. Editing a version is refused by the database itself, not by application code, so the policy a historical verdict was graded against can always be produced.

Webhooks. Three events — scan.completed, verdict.recorded and finding.state_changed — published from inside the transaction that causes them. Payloads are HMAC-signed with a rotatable secret, carry a timestamp against replay, and are retried with exponential backoff for about a day. Destinations are checked against private and loopback ranges before they are accepted.

Security

Suppression does not produce a green build by default. A policy’s suppression mode defaults to warn, so an accepted risk remains visible in every verdict rather than disappearing from it. ignore and enforce are available when a team wants different behaviour.

Policy management is not grantable to a machine credential. A build cannot rewrite the policy it is graded against.

2026.07

Finding lifecycle, scheduled rescans and the machine API

  • added
  • changed

Added

The finding lifecycle. Findings now move through five states — open, triaged, suppressed, resolved and reopened — governed by a single transition table. A human cannot mark a finding resolved: that state means a scan that would have found it did not, which is a statement about evidence.

Suppression with accountability. Accepting a risk requires a written reason and records who accepted it. Expiry is optional but bounded at two years, and an expiry that lapses is recorded as its own event, distinct from someone actively changing their mind.

Audit timelines. Every transition is written to an append-only record that names the kind of actor responsible — a person, a scan, a system task, or an API key. No audit entry attributes to a person something a pipeline did.

Scheduled rescans. An asset can carry one schedule at hourly, daily or weekly frequency. Occurrences missed while the platform was down are collapsed into a single run and surfaced as a count, rather than replayed as a backlog.

The machine API. Scoped, expiring API keys; cursor pagination that states its traversal guarantee; durable idempotency on scan requests and bulk operations; and bulk triage of up to five hundred findings in one transaction.

Changed

Enumeration defaults to a stable order. Listing by recency uses immutable keys and is exhaustive. Listing by severity offers no such guarantee, and every response now reports which it gave you.