A Rust tool for auditing and analyzing the capabilities of Rust crates based on the symbols they link to.
Say a the popular (and fictitious) crate super_nice_xml_parser
gets compromised by some evil actor and now mines bitcoins and sends them to some server.
Anyone depending on that crate (directly or indirectly) would be vulnerable.
But running cargo-caps check
you get an error (on CI or locally),
and in the output you see that the super_nice_xml_parser
crate suddenly has new capabilities of thread
and net
.
In short: cargo-caps
is a useful tool for auditing 3rd party crates.
- Only tested on macOS
- Some of the code was AI generated and has not yet been vetted by human eyes
- Half-finished
- Not ready for production
- cap-std - a capability-based library for things like filesystem and network access
- cargo-audit-build - tool for auditing
build.rs
files - cargo-audit - scans dependency tree for RUSTSEC advisories
- cargo-geiger - find usage of
unsafe
in crate tree - cargo-supply-chain - who are you trusting?
- cargo-vet - can be used to mark crates as "vetted" (audited)
- Fail safe: if
cargo-caps
can't understand it then assume the worst - Minimize noise: if we bother the user too much, they'll get desensitized
cargo install cargo-caps --locked
Make sure you're in the root of a cargo project, then run:
cargo-caps init
This creates a cargo-caps.eon
file where you can specify what crates are allowed what capabilities.
Next run:
cargo-caps check
This will build your local project, and while doing so, print the capabilities of each crate it depends on, directly or indirectly.
Any package manager like cargo
is vulnerable to supply chain attacks.
You want to add that nice 3rd party crate, but to do so you must trust it, and all the other transitive dependencies it pulls in.
And you must also trust future update whenever you run cargo update
.
You can read their source code, but that's a LOT of work, and so you don't.
Enter cargo-deny
.
cargo-deny
will verify what a crate is capable and incapble of.
Worried a create will read your files, or mess with your filesystem? It can't without the fs
capability.
Worried it will communicate with nefarious actors over the inernet? Better check whether or not it has the net
capability!
You can configure exactly what each dependency is allowed in a cargo-deny.eon
config file in your repository.
You can then run cargo-deny check
to check if any dependency does more than it is allowed to.
cargo-caps check
will run cargo build
and then analyze the linker symbols of each built library.
Based on these symbols cargo-caps
will then infer capabilities of each library.
For instance: if a crate links with symbols in std::net::
then cargo-deny
infers that the crate has the capability to communicate over network.
See default_rules.ron
for how different symbols are categorized.
Any unknown symbol will lead to the crate being assigned the capability of any
(fail-safe).
A lot of crates have build.rs
files that have the possibility to do anything.
But build.rs
files gets compiled to binaries, making analyzing their symbols a lot harder (they just pull in a lot more by default).
Therefore cargo-caps
will instead parse build.rs
files (using syn
).
The source analyzer works only for simple build.rs
files (which are most of them).
For more complex things, you the user will have to manually audit (or trust).
To help, cargo-caps
will print the path to the source code so you can more easily find and read the code.
- Do source code analysis to find and flag all
unsafe
- (with
unsafe
a crate can potentially do any evil sys-call)
- (with
cargo-caps
currently can distinguish between the following capabilities:
alloc
- allocate memory (applied to everything instd
)panic
- can cause apanic!
(applied to everything instd
)time
- measuring time and telling the current timesysinfo
- reading environment variables, process info, …stdio
- read/write stdin/stdout/stderrthread
- spawn threadsnet
- communicate over the networkfs
- filesystem access (read and/or write)any
- can do anything
Things that will get a crate put in the any
bucket includes calling into an opaque library, or starting another process.
Using any symbol not yet categorized in default_rules.ron
will also put you in the any
bucket.
cargo-deny
can tell whether or not a library can use the network, but not to where it connects.
cargo-deny
can only tell that a crate is using the file system, but not which files are being read.
If you want this sort of fine-grained capabilities, I suggest you take a look at cap-std.
cargo-deny
also works on a per-crate basis.
Perhaps only a single function in a crate uses the file system, and you aren't calling that.
The crate will still be labeles as using the fs
capability.
Macros produce can make cargo-deny
point the finger at the wrong crate.
For instance, if a simple_log!
macro actually spawns a thread and starts a network connection, then the net
capability will show up for the crate calling simple_log!
, as opposed to the crate defining
it.
In theory we should be able to expand all macros at the call site and analyze the resulting code, but that's not something cargo-caps currently does.
cargo-deny
is not perfect, but it can still be a useful layer of defence.
fwrite/fread
can be used on any FILE, including network sockets, BUT fwrite/fread
is NOT considered a high capability.
Only opening a FILE requires a specific capability (e.g. net
to open a socket, of fs
to open a file).
To be able to write to a FILE you first need to open it, OR another crate must hand you that FILE handle, thus handing you the capability.
Similarly with dynamic calls: a crate that calls some callback is not responsible for the capabilities of that callback.
For some crates (hopefully most!), the capabilities is just the union of the capabilities of all its dependent crates. This means that if we have accurate capabilities for all the crates it depend on, we can confidently assign capabilities to the crate without having to audit it.
However, there will be some edge crates where that won't work, if it uses some 3rd party thing that is opaque to cargo-caps (e.g. dynamically linking with a C library).
In these cases, we must conservatively assign them the any
capability set (the union of everything).
However, this will infect all dependent crates, so that the whole crate eco system gets labels with any
.
If a curl
crate interacts with libcurl
, we want some way to say "The capability of this crate is actually just net
".
For this we need signing. A trusted person verifies that the crate has a certain set of capabilities, and then signs a specific release and/or commit hash of that crate.
Another example: ffmpeg-sidecar
is a crate that starts and controls an ffmpeg
process.
If you trust ffmpeg not to do any shenanigans, then you could sign ffmpeg-sidecar
to be excluded from net
and fs
capbilties.
Currently a Rust developer ha to verify all dependencies it pulls in (even transitive ones!), but with cargo-caps you only need to trust these (hopefully few) edge crates.
Run cargo-caps on self:
cargo run --release -- check