Skip to main content

Your Rust Dependency Audit: A Busy Dev’s Practical 5-Step Checklist

Every Rust project starts with a clean Cargo.toml. Six months later, you have 47 direct dependencies and 214 transitive ones. Which ones are still maintained? Which pulled in an unexpected license? Which added unsafe blocks behind a feature flag you don't need? This checklist is for the developer who wants a repeatable, time-boxed dependency audit — not a theoretical treatise on supply-chain security. We wrote this for teams shipping Rust to production, whether that's a CLI tool, a WebAssembly module, or a backend service. The five steps below take about 45 minutes once you're familiar with the tools. You'll leave with a concrete list of actions: update, pin, replace, or remove. 1. Why Your Dependency Tree Deserves a Regular Audit Rust's strong type system and memory safety guarantees can create a false sense of security. The compiler won't warn you that chrono 0.4.

Every Rust project starts with a clean Cargo.toml. Six months later, you have 47 direct dependencies and 214 transitive ones. Which ones are still maintained? Which pulled in an unexpected license? Which added unsafe blocks behind a feature flag you don't need? This checklist is for the developer who wants a repeatable, time-boxed dependency audit — not a theoretical treatise on supply-chain security.

We wrote this for teams shipping Rust to production, whether that's a CLI tool, a WebAssembly module, or a backend service. The five steps below take about 45 minutes once you're familiar with the tools. You'll leave with a concrete list of actions: update, pin, replace, or remove.

1. Why Your Dependency Tree Deserves a Regular Audit

Rust's strong type system and memory safety guarantees can create a false sense of security. The compiler won't warn you that chrono 0.4.19 has an unmaintained timezone database, or that a patch-level bump in hyper quietly dropped support for HTTP/1.0 keep-alive. Dependencies are trust relationships, and trust decays when upstream maintainers lose interest, change direction, or merge a malicious pull request.

The 2023 cargo-audit database shows roughly 1 in 20 Rust crates have had at least one advisory filed. That number understates the problem because many vulnerabilities are never reported. More common than CVEs are dependency rot: crates that compile but pull in abandoned transitive deps, forcing your build to download packages nobody has touched in three years. Over time, this increases CI time, binary size, and the surface area for supply-chain attacks.

A regular audit shifts your mindset from 'it compiles, ship it' to 'I know exactly what each dependency does and why it's here.' It also surfaces license conflicts before legal review becomes a blocking step. For teams using cargo-deny or cargo-audit in CI, the manual audit complements automated checks by catching things that tools can't: unnecessary feature flags, abandoned forks, and undocumented unsafe usage.

Who Should Run This Audit

Any project with more than five direct dependencies benefits from a structured review. Solo developers often skip audits because they 'know the codebase,' but that familiarity can blind them to changes in upstream crates. Teams shipping to embedded or regulated environments should run this checklist before every minor release. Open-source maintainers can use it to prepare for a major version bump.

2. The Five-Step Audit Checklist

We've designed this checklist to be run in order. Each step builds on the previous one, and you can stop after any step if you find a blocker that needs immediate attention. The total time is roughly 45 minutes for a project with 20–30 direct dependencies.

Step 1: Inventory and Categorize

Start by listing every direct dependency with its version, purpose, and last update date. Use cargo tree to see the full tree and identify duplicates or multiple versions of the same crate. Group them into three categories: core (essential to your domain logic), utility (logging, serialization, error handling), and incidental (one-off helpers you could inline).

This step alone often reveals surprises: a crate you thought was a dev-dependency is actually in the release build, or two crates pull in different minor versions of the same library. Record which dependencies are behind by more than six months — those are candidates for update or replacement.

Step 2: Check Semver Compliance and Changelogs

For each direct dependency, read the changelog between your current version and the latest compatible release (respecting semver). Tools like cargo-outdated show available updates, but they don't tell you what changed. Focus on major version bumps first, then minor. Look for breaking changes that affect your usage, deprecation notices, and security fixes.

A common mistake is assuming that a patch bump is always safe. In practice, some crates have shipped behavior changes under patch versions, especially in the 0.x range where semver rules are looser. For crates with major.minor.patch versions below 1.0, treat every update as potentially breaking and test thoroughly.

Step 3: Scan for Unsafe Code and Soundness Issues

Use cargo-geiger to list which dependencies contain unsafe blocks. Not all unsafe code is dangerous, but each block is a point where the compiler's guarantees are suspended. Review the unsafe blocks in crates that handle network I/O, parsing, or FFI — these are the most likely sources of soundness bugs.

For each unsafe dependency, check whether there's a safe alternative that meets your needs. For example, if you use memchr for byte searching, consider whether the standard library's memchr (stabilized in Rust 1.77) suffices. Document your rationale for keeping each unsafe dependency in a comment or an audit log.

Step 4: Verify License Compatibility

Run cargo-deny to list all licenses in your dependency tree. Pay attention to copyleft licenses (AGPL, GPL) that may impose obligations on your project's distribution. Also watch for 'non-standard' licenses that haven't been reviewed by the OSI — they might restrict commercial use or require attribution in ways your legal team hasn't approved.

If your project is dual-licensed or uses a permissive license like MIT or Apache 2.0, ensure that all dependencies are compatible. The cargo-deny configuration lets you specify allowed licenses and fail the build on violations. For edge cases where a dependency uses a different license, document the exception and get sign-off from your legal contact.

Step 5: Review Transitive Dependencies and Prune Unused Features

Transitive dependencies are the most common source of bloat and risk. Use cargo-udeps to find unused dependencies — crates that are pulled in but never actually used in your code. Then check cargo tree --duplicates to see if multiple versions of the same crate are being compiled (this increases build time and binary size).

For each direct dependency, examine its default features. Many crates enable features you don't need. For example, serde's default features include derive and std, but if you only use serde_json for serialization without custom derive, you can disable those. Use default-features = false in Cargo.toml and explicitly enable only what you need.

3. How the Audit Works Under the Hood

Each step in the checklist relies on a combination of Cargo's built-in commands and third-party tools. Understanding what these tools check helps you interpret their output correctly.

cargo tree walks the dependency graph starting from your Cargo.toml, resolving semver requirements and showing the full tree with duplicate detection. It uses the same resolver as the build, so what you see is exactly what gets compiled. cargo-outdated queries the crates.io index for newer versions and compares them against your lockfile. It respects the semver range in Cargo.toml but doesn't account for yanked versions — you need to cross-check with cargo audit.

cargo-geiger scans the source code of each dependency for the unsafe keyword and aggregates counts per crate. It doesn't evaluate whether the unsafe usage is correct; that requires manual review. cargo-deny checks licenses against a configurable allowlist and can also run advisory queries. It downloads the full crate source to inspect license files, which means it can catch mismatches between the license field in Cargo.toml and the actual license text.

The most important insight from this layer is that automated tools are necessary but not sufficient. They flag patterns, but only human judgment can decide whether a particular unsafe block is justified or whether a license exception is acceptable. The audit checklist is designed to funnel your attention to the decisions that matter.

4. Walkthrough: Auditing a Realistic Web Service Project

Let's walk through a composite scenario. Imagine a Rust web service using actix-web 4.4, sqlx 0.7 with PostgreSQL, serde 1.0, tokio 1.35, and reqwest 0.11. The project has about 25 direct dependencies and 110 transitive ones. The last audit was six months ago.

In Step 1, cargo tree shows that actix-web pulls in actix-http 3.6, which depends on h2 0.3.24. Meanwhile, reqwest also depends on h2 but at version 0.3.22. The resolver merges them to 0.3.24, but the two crates were tested against different patch versions. This isn't a bug yet, but it's a risk if a future update to one crate requires a newer h2 that breaks the other.

Step 2 reveals that sqlx 0.7.4 is available, which includes a fix for a connection pool deadlock. The changelog mentions that the runtime-tokio feature now requires tokio 1.38. Upgrading sqlx means also bumping tokio, which affects the entire async stack. We decide to upgrade both and run the test suite.

Step 3 with cargo-geiger shows that actix-http has 47 unsafe blocks, mostly in the HTTP/2 frame parser. That's expected for a performance-critical network crate, but we note it in the audit log. sqlx has 12 unsafe blocks in its PostgreSQL protocol implementation. We check the commit history and see that recent versions have reduced unsafe usage — a good sign of ongoing maintenance.

Step 4: cargo-deny flags a transitive dependency on ring 0.17, which uses a custom license (ISC with a patent clause). Our legal team has pre-approved this license, so we add an exception. It also shows that openssl-sys is pulled in via reqwest's native-tls feature. Since we use rustls everywhere else, we switch reqwest to rustls-tls and remove the OpenSSL dependency entirely.

Step 5: cargo-udeps finds that chrono is unused — we were using it for timestamp formatting but switched to time in a previous refactor and forgot to remove the dependency. That saves one compile-time dependency. We also disable serde's derive feature because we use a custom serializer, cutting 0.3 seconds from incremental compilation.

The whole audit takes 50 minutes. The output is a short document: three crates to update, one feature flag to change, one unused dependency to remove, and one license exception to document. The team merges these changes in a single PR and runs CI.

5. Edge Cases and Common Pitfalls

Not every dependency audit goes smoothly. Here are situations where the checklist needs adjustment.

When a Dependency Is Unmaintained but Critical

You find that rust-crypto (hypothetical) hasn't been updated in two years, but it's the only crate that implements a specific algorithm your project requires. The safe path is to fork the crate, apply minimal fixes, and vendor it. Alternatively, you can wrap the unsafe calls in a safe API and pin the version with a comment explaining the risk. The audit should document the decision and set a calendar reminder to revisit every quarter.

When Multiple Crates Conflict on Semver Ranges

If two direct dependencies require incompatible versions of the same crate (e.g., foo needs bar 1.x and baz needs bar 2.x), Cargo will compile both versions. This doubles compile time and binary size. The fix is usually to update one of the dependents to a newer version that uses the same bar range. If that's not possible, consider replacing one of the dependents with an alternative.

When a Dependency Uses a Non-Standard Build Script

Some crates use build.rs to download native libraries or generate code. These build scripts can fail in restricted environments (e.g., air-gapped CI) or introduce non-determinism. Audit the build script's behavior: does it fetch from the internet? Does it write to OUT_DIR in a way that breaks caching? If the build script is complex, consider replacing the crate with a simpler alternative that doesn't require build-time code generation.

When the Audit Reveals a License Conflict

If a dependency uses AGPL-3.0 and your project is MIT-licensed, you cannot distribute them together unless you also license your project under AGPL. The options are: replace the dependency, negotiate an exception with the maintainer, or relicense your project (if you own all the code). The audit should flag this early so legal can weigh in before you're deep into development.

6. Limits of the Audit and When to Go Deeper

This five-step checklist is a practical starting point, but it has blind spots. It doesn't detect malicious code injection through compromised maintainer accounts — that requires additional tooling like cargo-crev or a private registry with code review. It also doesn't measure runtime performance impact of dependencies; a crate that adds 10ms to startup time might be fine for a CLI tool but unacceptable for a latency-sensitive service.

The audit assumes that the dependency tree is stable between audits. In practice, transitive dependencies can change when you run cargo update without changing your Cargo.toml. A patch-level bump in a deep transitive dep could introduce a breaking change or a vulnerability. To mitigate this, pin your lockfile and review cargo update outputs carefully, or use a tool like cargo-lock to diff the lockfile between commits.

Another limitation is that the audit doesn't evaluate the quality of a crate's documentation or test coverage. A crate with 100% test coverage and thorough docs is easier to maintain than one with sparse tests, even if both pass the audit. For long-lived projects, consider adding a qualitative assessment: how responsive are the maintainers? Is the crate's API stable? Does it have a clear migration path for breaking changes?

Finally, the audit is a snapshot. Dependencies evolve, and new vulnerabilities are discovered. Integrate the manual audit with automated daily or weekly checks using cargo-audit in CI. Set up notifications for new advisories. And at least once per quarter, run the full five-step checklist to catch issues that automation misses.

The goal isn't zero dependencies — that's impractical for most projects. The goal is intentionality: knowing exactly what you depend on, why, and at what cost. A regular audit turns dependency management from a source of anxiety into a routine maintenance task that you can complete before lunch.

Share this article:

Comments (0)

No comments yet. Be the first to comment!