cargo-capsec
Static capability audit for Rust — find out what your code can do to the outside world.
cargo-capsec scans Rust source code and reports every function that exercises ambient authority: filesystem access, network connections, environment variable reads, process spawning, and FFI calls. No annotations or code changes required.
Installation
Commands
cargo capsec init — Bootstrap for existing codebases
Runs a full audit, generates a .capsec.toml with allow rules for all existing findings, saves a baseline, and optionally sets up CI. Adopt in 30 seconds — then catch regressions.
cargo capsec audit — Scan for ambient authority
# Basic scan (workspace crates only)
# Cross-crate propagation (workspace + dependencies)
# Full dependency tree analysis
# MIR-based deep analysis (requires nightly + capsec-driver)
# Supply-chain view (only dependency findings)
# Output formats
# Filtering
# CI integration
# Baselines
Output example
my-app v0.1.0
─────────────
FS src/config.rs:8:5 fs::read_to_string load_config()
NET src/api.rs:15:9 reqwest::get fetch_data()
↳ Cross-crate: reqwest::get() → TcpStream::connect [NET]
FFI src/db.rs:31:9 rusqlite::execute query()
↳ Cross-crate: rusqlite::execute() → sqlite3_exec [FFI]
PROC src/deploy.rs:42:17 Command::new run_migration()
VIA src/main.rs:5:9 load_config() main()
Summary
───────
Crates with findings: 1
Total findings: 5
Categories: FS: 1 NET: 1 ENV: 0 PROC: 1 FFI: 1
2 critical-risk findings
Analysis modes
| Mode | Flag | What it scans | Speed |
|---|---|---|---|
| Workspace only | (default) | Your code | Fast |
| Cross-crate | --include-deps |
Your code + dependency source (syntactic) | Medium |
| Deep | --deep --include-deps |
Everything via MIR (sees through macros, FFI wrappers) | Slow (nightly) |
cargo capsec diff — Compare crate versions
Shows what ambient authority was added or removed between two versions of a crate. Useful for reviewing Dependabot PRs or evaluating upgrades.
serde_json 1.0.130 → 1.0.133
─────────────────────────────
+ NET src/de.rs:142:9 TcpStream::connect fetch_schema()
- FS src/io.rs:88:5 fs::read old_loader()
Summary: 1 added, 1 removed, 1 unchanged
cargo capsec compare — Compare different crates
Side-by-side capability profiles for making informed dependency choices.
ureq v2.12.1 reqwest v0.12.12
────────── ────────────────
FS: 0 FS: 3
NET: 4 NET: 18
ENV: 1 ENV: 4
PROC: 0 PROC: 0
FFI: 0 FFI: 12
Total: 5 Total: 37
cargo capsec check-deny — Verify #[capsec::deny] annotations
Checks that functions annotated with #[capsec::deny(fs)] or #[capsec::deny(all)] don't contain ambient authority calls. Any violation is promoted to critical risk.
cargo capsec badge — Generate shields.io badge
Configuration (.capsec.toml)
# Exclude directories from scanning
[]
= ["tests/**", "benches/**", "examples/**"]
# Crate-level deny — all ambient authority is a violation
[]
= ["all"]
# Custom authority patterns for project-specific I/O
[[]]
= ["my_crate", "secrets", "fetch"]
= "net"
= "critical"
= "Fetches secrets from vault"
# Suppress known-good findings
[[]]
= "tracing"
= "Logging framework, reviewed"
[[]]
= "my-app"
= "load_config"
= "Known FS access, reviewed"
# Classify crates as pure (no I/O) or resource (has I/O)
[[]]
= "my-parser"
= "pure"
Deep analysis (--deep)
The --deep flag uses a custom Rust compiler driver (capsec-driver) that walks MIR after macro expansion and type resolution. This catches:
- FFI calls hidden behind macros (e.g.,
git2'stry_call!()→libgit2_sys) - Authority exercised through trait dispatch
- Generic instantiations that resolve to I/O functions
Requires nightly:
&&
See crates/capsec-deep/README.md for architecture details.
Cross-crate propagation
With --include-deps, capsec builds an export map for each dependency: which functions exercise ambient authority. When your workspace code calls those functions, the finding propagates transitively:
your_code::handler() → reqwest::get() → TcpStream::connect [NET]
This works across:
- Registry dependencies (crates.io)
- Workspace member dependencies (topological ordering)
- FFI boundaries (extern function declarations)
- Multiple hops (
A → B → C → std::fs::read)
Limitations
- Dynamic dispatch (
dyn Trait) — cannot statically resolve which implementation runs - C/C++ internals — sees FFI call boundaries but not what foreign code does inside
- Inline assembly —
asm!()blocks are opaque - Runtime-loaded code —
dlopen/libloadingis invisible to static analysis
Output formats
| Format | Flag | Use case |
|---|---|---|
| Text | --format text |
Terminal, human review |
| JSON | --format json |
Scripts, dashboards, CI pipelines |
| SARIF | --format sarif |
GitHub Code Scanning, VS Code SARIF Viewer |
License
Apache-2.0