A Cargo subcommand that tells you when a dependency gains a sensitive capability across a version bump. A crate that did no network I/O suddenly making network calls in its build script, for example.
capdiff does static analysis with
syn. It is evadable by macros, FFI, and obfuscation, and it is not a sandbox. It raises the bar against common and lazy attacks; it does not stop a determined one. Use it alongside cargo-audit and human review, not instead of them.
cargo install cargo-capdiff
Requires Rust 1.85 or newer. Works on Linux, macOS, and Windows.
After cargo update, see what changed:
cargo capdiff
This diffs your working-tree Cargo.lock against the one at git HEAD and reports
the capabilities each dependency gained. When a crate gains network access inside
its build script, you get:
[High] badnet 0.0.0 -> 0.0.0 gained: Net
Net at .../badnet/build.rs:2 [buildscript] // std::net::TcpStream::connect
Exit code is 1 when a finding reaches the --fail-on threshold (default high
for the diff), so it drops straight into CI.
Audit the whole tree with no baseline:
cargo capdiff audit
Diff against a reviewed snapshot instead of git HEAD:
cargo capdiff --baseline capdiff.lock
--format human (default), --format json, or --format sarif. JSON conforms to
the committed schemas in schema/; SARIF is 2.1.0 and uploads to GitHub
code-scanning. JSON audit output:
{
"version": 4,
"crate_count": 1,
"skipped_count": 0,
"findings": [
{
"crate": "badnet",
"version": "0.0.0",
"capabilities": ["Net", "BuildScript"],
"severity": "High",
"evidence": [
{
"capability": "Net",
"file": ".../badnet/build.rs",
"line": 2,
"snippet": "std::net::TcpStream::connect",
"in_build_script": true
}
]
}
]
}--format {human|json|sarif}--fail-on {none|notable|high}-- exit1when a finding reaches the threshold (defaulthighfor diff,nonefor audit)--baseline <file>-- diff against a committedcapdiff.locksnapshot--strict-- exit2if any source file could not be parsed--no-cache-- do not persist the fetch/extract cache
Exit codes: 0 clean, 1 findings at or above --fail-on, 2 usage, IO, or strict failure.
Each dependency version fingerprints to a small capability set:
| Capability | Meaning |
|---|---|
Net |
network I/O (std::net, reqwest, hyper, ureq, ...) |
Process |
spawning processes (Command::new, ...) |
FsRead |
filesystem reads |
FsSensitive |
filesystem access near a credential store (.ssh, .aws/credentials, ...) |
Env |
environment variable reads |
Ffi |
extern blocks, #[link], an analysis boundary |
BuildScript |
the crate has a build.rs |
ProcMacro |
the crate is a proc-macro |
ObfuscatedBlob |
large base64/hex string literals |
Transmute |
a mem::transmute call |
FnPtrTransmute |
a transmute whose target is a function pointer |
Severity is the combination, applied to the gained set:
- Network, process, env, or credential-path access inside a build script -> High.
- Network plus env or credential access in the same crate (the exfiltration shape) -> High.
- An obfuscated blob plus a process spawn or function-pointer transmute -> High.
- A proc-macro doing network or process work at compile time -> High.
Build scripts get the stricter rules because they run at build time with full permissions.
Cargo.lock resolves to a list of (name, version, checksum). Each .crate is
fetched from static.crates.io, sha256-verified against the index, and extracted
into a temp dir (symlinks and path-traversal entries are rejected; nothing is ever
executed). The extracted source is walked with syn, matching call paths against
curated identifier sets. Results are cached immutably by (name, version, checksum),
so a given crate version is analyzed once, ever.
Build-script noise is suppressed by an intraprocedural dataflow pass: when a
Command::new(rustc) argument is bound from env::var("RUSTC"), capdiff resolves
the binding and recognizes routine rustc-version probing rather than flagging it.
Matching is lexical, not name-resolved. A crate that aliases an import behind one indirection is caught; macros that expand to hidden calls are not. That is the fundamental limit of static analysis and the reason a precise, compiler-backed tier is future work.
Copy .github/templates/capdiff-action.yml
into your workflows. It runs capdiff on every pull request, diffs against the merge
base, and uploads SARIF to the code-scanning dashboard, so a dependency that gains
a high-severity capability shows up as a PR annotation.
Licensed under either of Apache License, Version 2.0 or MIT license at your option.