TL;DR: A vulnerability scanner gives you a yes-or-no answer. rust-tool-base uses cargo-deny, which is a policy gate rather than a scanner: it checks licences, advisories, banned version specs and crate sources against rules you write down. The part worth copying isn’t the gate, it’s the waiver list. Every advisory the project can’t fix today is recorded with the crate, the dependency path that reaches it, the reason, and the milestone that removes it. A waiver isn’t “we stopped caring.” It’s a dated note.

A scanner answers one question

When I had go-tool-base security-audited, part of the routine was running a vulnerability scanner over the dependencies. Go has a good one. It looks at your dependency graph, cross-references known advisories, and tells you whether any of them reach code you actually call.

That is useful and you should do it. But notice the shape of what it gives back: essentially a yes or a no. Either there’s a known vulnerability on a reachable path or there isn’t. It answers one question, on the day you ask it.

Supply-chain risk in a framework is broader than that one question, because a framework drags its entire dependency tree into every tool built on it. rust-tool-base treats the whole tree as something to have a policy about, and the tool for that is cargo-deny.

A gate, not a scan

cargo-deny reads a deny.toml and checks the dependency graph against four kinds of rule.

Licences. There’s an allowlist: MIT, Apache-2.0, the BSD variants, ISC, a handful of others. Every transitive crate’s licence has to be on it. A dependency that pulls in something copyleft, or something with no licence at all, fails the build. You find out the first time it enters the tree, not during a release scramble when someone finally reads the legal implications.

Advisories. It checks the RustSec advisory database, and yanked crates are set to deny, so a dependency that’s been pulled from the registry stops CI.

Bans. Wildcard version requirements (version = "*") are denied outright, because a dependency that floats to whatever’s newest is a supply-chain hole by construction. Duplicate versions of the same crate are surfaced too.

Sources. Crates may only come from the official registry. An unknown registry or a stray git dependency is denied. Nothing sneaks in from a URL.

That’s a gate. It encodes, as rules in a file, what the project will and won’t accept into its dependency tree, and it enforces them on every build instead of every audit.

The honest part is the waiver list

Here’s the thing every real project runs into. Sooner or later there’s an advisory you genuinely cannot fix this week. It’s against a crate three levels down your tree. The fix needs an upstream release that hasn’t happened. The crate is scheduled to be reworked two milestones from now anyway. The gate is going to fail, and the work to satisfy it honestly isn’t available to you yet.

The lazy response is a blanket ignore: silence the advisory, move on, forget. Now your gate has a hole in it that nobody remembers opening.

rust-tool-base’s deny.toml does something better. Every waiver in the ignore list is a documented record. Each one carries a comment that names the crate, traces the exact dependency path that reaches it, gives the reason, and names the condition that lifts it:

ignore = [
    # `instant` — reached via async-openai -> backoff -> rtb-ai (v0.3).
    "RUSTSEC-2024-0384",
    # `paste` — reached via ratatui -> rtb-docs (v0.2) / rtb-tui (v0.4).
    "RUSTSEC-2024-0436",
    # ...
]

The file states the policy out loud: “Every waiver points at a deferred stub crate that will be reworked before its ship milestone. Lift each waiver when the owning crate lands its v0.1.”

Some waivers go further and carry a structured reason field, so the why travels with the entry rather than living only in a comment above it:

{ id = "RUSTSEC-2025-0140",
  reason = "gix-date via gix is a stub dependency; rtb-vcs v0.5 will upgrade" },

Read that list and you don’t see a project that stopped caring about seven advisories. You see seven advisories the project knows about, can trace, and has tied to a specific milestone. The waiver has an expiry condition. When rtb-vcs reaches v0.5, that gix entry is meant to come out, and the comment is the reminder that it should.

Why this is the bit to copy

A gate that can’t be relaxed is a gate people route around. They’ll find the broadest possible ignore and use it, because the alternative is being blocked on someone else’s release. The pressure to do that is real and it’s not unreasonable.

So the design that actually holds up isn’t a stricter gate. It’s a gate with an honest, structured escape hatch: you can waive an advisory, but a waiver costs you a documented record with a dependency path and an expiry condition. That price is small enough that nobody routes around it, and high enough that waivers don’t accumulate silently. The ignore list stays readable, and every line in it is something you could defend.

Supply-chain hygiene framed this way isn’t an audit you survive once a year. It’s bookkeeping: a ledger of what you accepted, why, and when each exception is due to close.

Where this leaves us

A vulnerability scanner answers one question on one day. cargo-deny is a standing policy gate: licences against an allowlist, advisories and yanked crates denied, wildcard versions banned, sources restricted to the official registry, enforced on every build.

The part of rust-tool-base’s setup worth copying is the waiver list. Every advisory that can’t be fixed yet is recorded with its crate, its dependency path, its reason and the milestone that removes it. A waiver is a dated note, not a shrug, and that’s what keeps the gate honest enough that nobody wants to bypass it.