Skip to content

telcharr/capdiff

Repository files navigation

capdiff

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.

Install

cargo install cargo-capdiff

Requires Rust 1.85 or newer. Works on Linux, macOS, and Windows.

Use it

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

Output formats

--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
        }
      ]
    }
  ]
}

Flags

  • --format {human|json|sarif}
  • --fail-on {none|notable|high} -- exit 1 when a finding reaches the threshold (default high for diff, none for audit)
  • --baseline <file> -- diff against a committed capdiff.lock snapshot
  • --strict -- exit 2 if 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.

What it looks for

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.

How it works

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.

In CI

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.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.

About

A cargo subcommand that flags new network, process, and filesystem access in your dependencies after an update.

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages