Policy rules reference
A version is held while any rule fails. All rules are off by default; an empty policy holds nothing. Unless marked otherwise, a rule failure can be waived for a single version by manual promotion approvals.
On publish registries, five rules can also reject at publish
instead of holding: their remedy never needs the version to exist, so
rejection keeps the version number free for a fixed re-publish. The
vcs, repository_change, and license toggles default to reject
(the two VCS strengths share one toggle); new_publisher defaults to
hold. Mirrors never reject — there is no publish to refuse, imported
versions always hold at read time — so the toggles do not exist on a
mirror’s policy.
| Rule | Checks | Waivable | Rejects at publish |
|---|---|---|---|
quarantine |
Minimum age since publish (or first seen) | yes | never |
sbom |
An SBOM is attached | yes | never |
attestations |
Enough distinct identities attested | yes | never |
license |
Declared license satisfies the SPDX allowlist | yes | default on |
vcs |
Tarball claims a clean VCS commit | yes | default on |
vcs_downgrade |
Provenance once claimed keeps being claimed | yes, or trust | default on |
provenance |
The claimed commit is verified in the declared repo | yes | never (the check is async) |
repository_change |
The declared repository stays what it was | yes, or trust | default on |
new_publisher |
Publisher is trusted for this crate | yes, or a vouch | default off |
advisories |
No advisory at a blocking level | no, allow the advisory | never |
blocked |
No owner has manually pulled the version | yes, or the blocker withdraws | never |
pinned |
Version matches the registry pin for its package | no, edit the pin | never |
custom:<fact> |
A recorded custom fact satisfies the owner’s rule | yes | only on publish-time haven: facts |
This table is written for cargo registries. A
static file registry
evaluates the narrowed subset a files version can carry — quarantine,
license, blocked, pinned, and custom:<fact> rules over declared
facts — plus two per-file gates of its own, a size cap and a
required upload checksum, both of
which refuse the upload outright. The other rows read facts only a
cargo publish carries, and a files policy naming them is refused at
save. pinned has one extra condition on files: the pinned package
must declare the semver
version scheme, because a
version requirement has nothing to match against a date or a counter.
provenance is the one rule whose answer arrives after the publish,
because verifying a commit means reaching the repository over the
network. It therefore never rejects, and a version holds with an
explicit awaiting analysis reason until the check lands. That reason
is a statement about Haven, not about the crate.
The three trust rules take a per-version waiver like any other, but their intended exit is the standing trust described in each section below: it lifts every version the same change holds, not one.
advisories and pinned are the two rules with no per-version exit
at all. Owners can allow an advisory by id, which stops it holding
anything in the registry, but there is no way to ship one version
despite an advisory that still applies to the others. Out-voting a CVE
on a single version is not a thing Haven lets you do. A pin works the
same way: the pin itself is the standing decision, and editing or
removing it is the only release.
quarantine
Section titled “quarantine”Holds a version until it has aged past the configured delay. Stored in seconds, edited in days in the UI. On publish registries the clock starts at publish time; on mirrors it starts when the mirror first sees the version (see Mirrors). Versions older than the policy pass immediately; enabling quarantine never re-holds history.
Use it to let the ecosystem be your canary: most malicious releases are caught within days of appearing.
Holds versions that have no SBOM attached. Haven generates a CycloneDX SBOM for publishes automatically, and one can be uploaded per version; see SBOM & attestations.
attestations
Section titled “attestations”Holds a version until at least the configured number of attestations from distinct identities are attached. Use it to require sign-off from N release engineers or build systems before a version ships.
By default every distinct identity counts. To pin down whose sign-off
satisfies the rule, set trusted_attesters: a list of identities that
count toward the minimum. An entry matches exactly, or as a prefix when
it ends with *, so
https://token.actions.githubusercontent.com#repo:acme/* covers every
workflow in the acme GitHub org. Anyone can still attest either way;
unlisted identities are recorded as evidence but do not count. Without a
list, any holder of an account on the instance (or any identity from a
trusted OIDC issuer) can satisfy the threshold, so configure one
whenever the rule is doing security work rather than workflow work.
license
Section titled “license”An SPDX allowlist, for example MIT, Apache-2.0, BSD-3-Clause.
The version’s declared license expression must be satisfiable using
only allowed identifiers (MIT OR GPL-3.0 passes an MIT allowlist;
MIT AND GPL-3.0 does not).
The rule fails closed, with distinct hold reasons for each case: the declaration is missing, the declaration does not parse as SPDX, or the expression cannot be satisfied from the allowlist.
The declaration is frozen into the tarball, so a failing version can never pass this rule later. By default a failing publish is therefore rejected, keeping the version number free: fix the declaration (or widen the allowlist first) and publish the same version again. With the reject toggle off, the version is accepted and permanently held; only an owner waiver can ship it.
Requires the tarball’s packaging metadata to claim a clean VCS state:
a known commit with no dirty files. Publishes made with
--allow-dirty, or without version control provenance, are held.
This state is frozen into the tarball forever: a version held by
vcs can never pass on its own. By default a failing publish is
therefore rejected, keeping the version number free: publish the
same version again from a clean checkout. With the reject toggle off,
the version is accepted and permanently held; only an owner waiver can
ship it.
vcs_downgrade
Section titled “vcs_downgrade”The lighter VCS strength, shown as Hold dropped provenance in the
policy form (one three-way choice with vcs, which subsumes it).
Crates that never claimed provenance pass; a version fails when an
earlier version of the crate claimed VCS provenance and this one
claims none. A release suddenly built outside the crate’s usual git
flow is the tampered-tarball shape and worth a human look. Dirty
claims pass: the rule watches for provenance disappearing, not for
its quality.
Unlike vcs, the hold is not permanent: owners can accept the crate’s
new provenance-less state with Trust new state on the Held tab.
Like publisher vouches, the trust is standing and takes the promotion
threshold of distinct owners to grant, one owner to revoke — and it is
scoped to the current epoch: if the crate resumes claiming provenance
and then drops it again, the drop holds again until owners re-affirm.
The rejection default and mechanics follow the shared VCS toggle: a failing publish is rejected with the version number kept free (republish from the usual checkout, or get the state trusted and the same version publishes again), and rejected attempts are listed on the Held tab with the same Trust action. Inert on mirrors: cached versions carry no provenance facts, so nothing ever counts as prior.
provenance
Section titled “provenance”Requires the version’s provenance to be positively verified, not
merely claimed. Where vcs judges what the tarball says about itself,
this rule checks that claim against the repository it names. Two
strengths:
- The repository carries the commit’s tag — the declared
repositoryresolves over the network and carries a tag pointing at the commit.cargo_vcs_info.jsonclaims. - The packaged files match that commit — the stronger answer: the
files in the tarball are byte-for-byte the files in that commit’s
tree, allowing for what cargo itself changes at package time (it
rewrites
Cargo.toml, may addCargo.lock, adds.cargo_vcs_info.json, and appliesinclude/exclude).
The check runs after the publish, not during it, because it needs the
network. So this rule never rejects a publish and has no reject
toggle: until the check lands, the version is held with an
awaiting analysis reason that says plainly that nothing is known yet.
The hold clears itself the moment a passing result arrives — no action
needed.
A verdict says one of four things, and they are deliberately not interchangeable:
| verdict | meaning |
|---|---|
| verified | the claim held, at the tier shown |
| unclaimed | the tarball claims no commit, or declares no repository, so there is nothing to check |
| unverifiable | a claim was made and could not be checked: the host does not resolve, the repository is private or gone, the fetch timed out |
| refuted | the packaged files provably differ from that commit’s tree |
Only refuted is an accusation. unverifiable is Haven saying it
does not know, and it is shown that way — a gap in what Haven can see
has never been treated as evidence against a crate. Note what is
absent: there is no “that commit does not exist” verdict. Git servers
differ in whether they will serve a commit that is not a branch or tag
tip, and a refusal to serve one cannot be told apart from its absence,
so Haven does not guess.
All four non-passing states hold, so a registry whose repositories are private will hold everything: the escape is a promotion approval, the same per-version waiver every other rule uses.
On a mirror this holds the whole catalogue until each version has been checked, and checking a mirrored corpus takes as long as it takes. That is the rule working as intended — a version nobody has inspected is not one you have evidence about — but turn it on deliberately.
repository_change
Section titled “repository_change”The same change-is-the-signal shape as vcs_downgrade, over a value:
the manifest’s declared repository URL. Crates that never declared
one pass, and so does the first declaration — but once declared, a
version declaring a different repository (or dropping the
declaration) fails. A crate quietly pointing consumers at a new
repository is the handover shape and worth a human look.
The hold is not permanent: owners can accept the new value with Trust new repository on the Held tab. The trust names the exact new URL, takes the promotion threshold of distinct owners to grant, one to revoke — and the rule ratchets onto the crate’s latest declaration, so switching back to a previous repository is itself a change needing its own trust.
By default a failing publish is rejected with the version number kept free: revert the field, or get the new value trusted and the same version publishes again. Rejected attempts are listed on the Held tab with the attempted URL and the Trust action.
On crates.io mirrors the rule works from sampled observations: the dump carries only the crate-level repository (the value as of dump time), so each refresh records it, and a change between two refreshes holds the versions published in that window until owners trust the new URL. The usual dump caveats apply: history before the first observation is trusted, versions newer than the latest dump pass until the next refresh, and several changes inside one refresh window read as one. Mirrors of other registries publish no dump and carry no repository facts, so the rule never holds anything there.
advisories
Section titled “advisories”Holds versions affected by a security advisory whose level is in the
policy’s blocking set. Levels: vulnerability, unsound,
unmaintained, notice. With no levels selected (the default),
advisories are recorded and shown but never hold anything.
For crates.io mirrors, advisories arrive automatically from the
RUSTSEC database; owners can raise advisories manually anywhere
(manual raises file as vulnerability). Withdrawn advisories release
their holds automatically; reclassified ones change level.
Not waivable. The only bypass is per advisory id: adding an id
such as RUSTSEC-2024-0003 to the policy’s ignored list makes that
one advisory never hold, whatever its level. Changing levels or the
ignored list takes effect immediately, no re-sync needed.
An owner can add an id straight from a held version, without going via the policy form: the Held tab’s Trust menu offers an Allow action for each advisory holding that version. It is a policy edit in a per-advisory shape, so one owner suffices and the blast radius is the whole registry, not the one version: the advisory stops holding everything it touches. This is the practical relief valve on a large mirror, where an advisory on a popular crate can hold thousands of versions that no one will ever review one by one. Removing an id again is a Policy tab edit.
new_publisher
Section titled “new_publisher”A trust model for maintainer changes, aimed at the supply chain attack where a well-known crate quietly gains a new releaser. When enabled, the policy records a cutoff instant:
- Publishers whose first release of a crate predates the cutoff are trusted for that crate. Enabling the rule trusts all history, so it turns on without holding anything retroactively.
- A version released by a publisher new to that crate after the cutoff is held until enough distinct owners vouch for the publisher (the same threshold as promotion approvals).
Vouching happens from the Held tab (Trust publisher) and applies to all of that publisher’s versions of the crate, past and future. Any single owner can revoke trust. Where publisher facts come from depends on the registry kind: publish registries record them first-hand, crates.io mirrors take them from the catalog dump. Mirrors of other registries have no publisher facts, so the rule never holds there.
On publish registries the rule can instead reject an untrusted publisher’s release outright (off by default). Trust is in the person, not the version, so nothing is lost: once enough owners vouch, the same version publishes again. Because a rejection leaves no held row, the Held tab lists recent rejected attempts with the same Trust action. Note that a read token minted with “serve held versions” can serve a held version, but a rejected publish was never stored, so there is nothing to serve.
blocked
Section titled “blocked”Not a policy setting: a manual, per-version hold any single owner can place from the crate page’s Security tab, with a required reason. Use it to pull a version out of circulation the moment something looks wrong — a suspected compromise, a botched release, a vulnerability that has no advisory id yet — without waiting for a rule to exist for it. Blocks work on every registry kind, mirrors included, and hold even when the rest of the policy is empty.
A blocked version stops serving exactly like any other hold: cargo reads it as nonexistent, and the version lands in the Held tab’s review queue carrying the blocker’s reason.
The asymmetry is deliberate, and mirrors the standing trusts in the opposite direction: making a version unavailable is the safe act, so one owner suffices; making it available again is the dangerous one, so it takes review. A block clears two ways:
- The blocking owner withdraws it, from the crate page or the Held tab’s Trust menu — the mistake path. Owners can only ever remove their own block; another owner’s block is never deletable.
- Promotion approvals out-vote it: the usual per-version waiver at the registry threshold, from the Held tab’s Promote action. The block stays recorded; the waiver ships the version anyway.
Several owners can block the same version independently. One waiver at the threshold covers them all, while withdrawing is per-owner.
pinned
Section titled “pinned”A registry-wide version pin: the policy names a package and a cargo version requirement, and every version of that package outside the requirement is held — including versions that do not exist yet. A pin is a standing freeze that needs no maintenance as upstream publishes: whatever appears next is held on sight, before any advisory exists.
Where a Cargo.lock pins one repository and drifts on cargo update,
a registry pin is enforced at resolution for every project, CI job,
and fresh checkout consuming the registry. Pins work on every registry
kind, and mirrors are the headline: freeze a crates.io dependency to
the versions you have audited, org-wide, with one policy row.
Mechanics:
- One pin per package. A requirement can still AND-compose
(
>=1.2, <1.5); there is no OR in cargo requirements. - On cargo registries, names match the crates.io identity:
case-insensitive,
-and_interchangeable. On files registries they match exactly, becausefoo-barandfoo_barare two distinct packages there. - On files registries the pinned package must be on the
semverversion scheme. Saving a pin against any other package is refused, and a package cannot leave the semver scheme while a pin names it: without that rule a requirement would silently hold every version the package has and every version it goes on to publish. - A package none of whose versions match its pin reads as nonexistent, exactly like any fully-held package.
- On crates.io mirrors, versions newer than the latest catalog dump are judged from the mirror’s first-seen clock, so a fresh upstream release is held immediately, not at the next refresh.
Not waivable. Promotions cannot out-vote a pin and the Held tab offers no Promote action for it; edit or remove the pin on the Policy tab to release. Held rows can still be accepted to leave the review queue, like any reviewable hold.
custom:<fact>
Section titled “custom:<fact>”The owner-defined rules: declare a named fact on the Policy tab (a
number, bool, or string), have your pipeline record one per version
through the API, and write rules over it. Each failing rule holds the
version under the custom:<fact> kind. Full guide, including the
comparators, the built-in haven: facts, and the ingestion API:
Custom facts & rules.
Two properties to know when reviewing their holds:
- Missing values fail closed. A version with no recorded value for
a rule’s metric is held, exactly like
require_clean_vcsholds a tarball that claims no provenance. Recording a passing value clears the hold on the next read; the Policy tab’s dry run shows what a rule would hold before you save it. - One kind per fact. Several rules on the same fact share the
custom:<fact>kind, so a promotion waiver on that kind waives all of them for that version.
Rules over the publish-time haven: facts (haven:license,
haven:rust_version, haven:tarball_bytes, haven:vcs_sha,
haven:vcs_dirty, haven:repository, haven:description,
haven:homepage, haven:documentation, haven:links,
haven:edition, and the tarball-pass set — haven:has_build_script,
haven:is_proc_macro, haven:tarball_parses, haven:binary_entries,
haven:hostile_paths, haven:symlink_entries,
haven:duplicate_entries, haven:high_entropy_entries,
haven:decompression_ratio) can also reject at publish;
rules over recorded values never reject, since values arrive after the
publish is accepted. On crates.io mirrors, custom rules apply to
versions the analyzer has inspected; a version it has not reached yet
holds as awaiting analysis and clears when the value lands.
Reviewing a hold
Section titled “Reviewing a hold”A hold is a question put to owners, and most of the Held tab’s work is deciding which questions are still open. The tab therefore opens on Needs review: the versions held by a rule no owner has looked at yet. Accept hold answers one — it records that an owner saw these failures and is leaving the version held, so the row drops out of the review queue.
Accepting releases nothing. The version stays held, invisible to consumers, exactly as before; promotion, a vouch, or a state trust are what actually ship a version. Because nothing is released, a single owner suffices, where a waiver takes the full threshold.
An acceptance names the rule kinds failing when it was made, and is judged against the version’s current failures. If a new rule starts holding the version later, the acceptance no longer covers it and the version returns to the queue: what an owner accepted was those failures, not any future ones.
Quarantine is never reviewable. A version held only by its cooldown clears on its own, so it never enters the queue and cannot be accepted.
Promotion threshold
Section titled “Promotion threshold”promotion approvals is not a hold rule but the policy’s waiver
threshold: how many distinct owner identities must approve to waive a
rule failure for one version, and how many vouches lift a
new_publisher hold. Default 2 (four eyes), minimum 1.