cargo-auditable-0.7.2/.cargo_vcs_info.json0000644000000001550000000000100140660ustar { "git": { "sha1": "0c4c6591a355c0fec883f70a4e9280974f15e02c" }, "path_in_vcs": "cargo-auditable" }cargo-auditable-0.7.2/CHANGELOG.md000064400000000000000000000126221046102023000144710ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.7.2] - 2025-11-09 ### Changed - Upgraded `cargo_metadata` and `object` dependencies to newer semver-incompatible versions - Publish prebuilt binaries for aarch64-unknown-linux-gnu and aarch64-pc-windows-msvc platforms ## [0.7.1] - 2025-10-19 ### Changed - Turned a hard error into a warning when `rustc` is called in a compilation command without `--crate-name` or `--out-dir`, for compatibility with exotic proc macros such as `crabtime`. ## [0.7.0] - 2025-07-04 ### Added - cargo-auditable can now use [Cargo's native SBOM precursor](https://doc.rust-lang.org/cargo/reference/unstable.html#sbom) for more accurate dependency trees. This feature is nightly-only as of this writing. - Introduced the `format` field to the encoded data, to let the data consumer know how the dependency tree was obtained (e.g. via the SBOM precursor or not) ### Fixed - Procedural macros are no longer erroneously reported as runtime dependencies. If the `format` field is set to 1 higher, they should be assumed to be reported correctly. ## [0.6.7] - 2025-05-04 ### Changed - Made the `rustc` argument parser more lenient. This allows proc macros such as the `embed-licensing` crate that call arbitrary `rustc` commands to work with `cargo-auditable`. - Added a heuristic to detect projects that use a "bare" linker. Normally `rustc` uses a C compiler as a linker, but it is possible to (mis)configure it to call a linker directly on some platforms. `cargo auditable` now tries to detect that and adjust its linker arguments accordingly on Unix-like systems. We do not recommend using this configuration, since it is likely to break things other than `cargo auditable`. ## [0.6.6] - 2024-11-24 ### Changed - Audit data is now injected when `--print` argument is passed to `rustc` if `--emit=link` is also present in the same invocation. This adds support for `cargo c` third-party subcommand. - When `--emit` argument is passed to `rustc`, audit data will only be injected if one of the values passed is `link`. This should avoid messing with modes that emit assembly or LLVM bitcode. - Upgraded to `object` crate from v0.30 to v0.36 in order to reduce the dependency footprint. ### Fixed - Arguments to `rustc` in the style of `--key=value` (as opposed to `--key value`) are now parsed correctly. This was never an issue in practice because Cargo passes the arguments we care about separated by space, not `=`. ## [0.6.5] - 2024-11-11 ### Added - Upgraded the `cargo_metadata` dependency to gain support for Rust 2024 edition ### Fixed - Fixed build on `riscv64-linux-android` target and certain custom RISC-V targets ## [0.6.4] - 2024-05-08 ### Added - LoongArch support ## [0.6.3] - 2024-05-03 ### Added - WebAssembly support ### Fixed - Pass the correct flag to MSVC link.exe to preserve the symbol containing audit data - This is not known to cause issues in practice - the symbol was preserved anyway, even with LTO. - Tests no longer fail on Rust 1.77 and later. The issue affected test code only. ### Changed - Refactored platform detection to be more robust ## [0.6.2] - 2024-02-19 ### Fixed - Fixed `cargo auditable` encoding a cyclic dependency graph under [certain conditions](https://github.com/rustsec/rustsec/issues/1043) - Fixed an integration test failing intermittently on recent Rust versions ### Changed - No longer attempt to add audit info if `--print` arguments are passed to `rustc`, which disable code generation - Print a more meaningful error when invoking `rustc` fails ## [0.6.1] - 2023-03-06 ### Added - A Unix manpage - An explanation of how the project relates to supply chain attacks to the README - Keywords to the Cargo manifest to make discovering the project easier ### Changed - Updated to `object` crate version 0.30 to enable packaging for Debian - Synced to the latest object writing code from the Rust compiler. This should improve support for very obscure architectures. ## [0.6.0] - 2022-12-07 ### Changed - A build with `cargo auditable` no longer fails when targeting an unsupported architecture. Instead a warning is printed. - The `CARGO` environment variable is now read and honored; calls to Cargo will go through the binary specified in this variable instead of just `cargo`. ### Added - Added documentation on using `cargo auditable` as a drop-in replacement for `cargo`. ### Fixed - Fixed build failures when the `RUSTC` environment variable or the `build.rustc` configuration option is set. ## [0.5.5] - 2022-12-01 ### Fixed - Long builds with `sccache` now work as expected. They require additional quirks compared to regular Cargo builds, see [#87](https://github.com/rust-secure-code/cargo-auditable/issues/87). - Note that `sccache` v0.3.1 or later is required even with this fix - earlier versions have a [bug](https://github.com/mozilla/sccache/issues/1274) that prevents them from working with `cargo auditable`. ## [0.5.4] - 2022-11-12 ### Changed - Updated README.md ## [0.5.3] - 2022-11-12 ### Fixed - `--offline`, `--locked`, `--frozen` and `--config` flags now work as expected. Previously they were not forwarded to `cargo metadata`, so it could still access the network, etc. ### Added - Re-introduced CHANGELOG.md cargo-auditable-0.7.2/Cargo.lock0000644000000250650000000000100120500ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "auditable-extract" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44371e9f9759dea49c42b6c6fe4c64ea216ee2af325a4524a7180823e00d3e7a" dependencies = [ "binfarce", "wasmparser", ] [[package]] name = "auditable-info" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c692b37b578433ebc75db30941a7ff137c381a204beb2429a30b7587d4d4dff3" dependencies = [ "auditable-extract", "auditable-serde", "miniz_oxide", "serde_json", ] [[package]] name = "auditable-serde" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d026218ae25ba5c72834245412dd1338f6d270d2c5109ee03a4badec288d4056" dependencies = [ "semver", "serde", "serde_json", "topological-sort", ] [[package]] name = "binfarce" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18464ccbb85e5dede30d70cc7676dc9950a0fb7dbf595a43d765be9123c616a2" [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "byteorder" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc10e8cc6b2580fda3f36eb6dc5316657f812a3df879a44a66fc9f0fdbc4855" [[package]] name = "camino" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" dependencies = [ "serde_core", ] [[package]] name = "cargo-auditable" version = "0.7.2" dependencies = [ "auditable-info", "auditable-serde", "cargo_metadata", "miniz_oxide", "object", "pico-args", "serde", "serde_json", "wasm-gen", "which", ] [[package]] name = "cargo-platform" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "122ec45a44b270afd1402f351b782c676b173e3c3fb28d86ff7ebfb4d86a4ee4" dependencies = [ "serde", ] [[package]] name = "cargo_metadata" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "981a6f317983eec002839b90fae7411a85621410ae591a9cab2ecf5cb5744873" dependencies = [ "camino", "cargo-platform", "semver", "serde", "serde_json", "thiserror", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "env_home" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys", ] [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash", ] [[package]] name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "indexmap" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "leb128" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "object" version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "crc32fast", "hashbrown 0.15.5", "indexmap", "memchr", ] [[package]] name = "pico-args" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "proc-macro2" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] name = "rustix" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", "serde_core", ] [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", "serde_core", ] [[package]] name = "syn" version = "2.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "topological-sort" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "wasm-gen" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b854b1461005a7b3365742310f7faa3cac3add809d66928c64a40c7e9e842ebb" dependencies = [ "byteorder", "leb128", ] [[package]] name = "wasmparser" version = "0.207.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e19bb9f8ab07616da582ef8adb24c54f1424c7ec876720b7da9db8ec0626c92c" dependencies = [ "bitflags", ] [[package]] name = "which" version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ "env_home", "rustix", "winsafe", ] [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "winsafe" version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" cargo-auditable-0.7.2/Cargo.toml0000644000000033660000000000100120730ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "cargo-auditable" version = "0.7.2" authors = ['Sergey "Shnatsel" Davidoff '] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Make production Rust binaries auditable" readme = "README.md" keywords = [ "security", "supply-chain", "sbom", "vulnerabilities", ] categories = [ "development-tools::cargo-plugins", "encoding", ] license = "MIT OR Apache-2.0" repository = "https://github.com/rust-secure-code/cargo-auditable" resolver = "1" [[bin]] name = "cargo-auditable" path = "src/main.rs" [[test]] name = "it" path = "tests/it.rs" [dependencies.auditable-serde] version = "0.9.0" [dependencies.cargo_metadata] version = "0.23" [dependencies.miniz_oxide] version = "0.8.0" [dependencies.object] version = "0.37" features = ["write"] default-features = false [dependencies.pico-args] version = "0.5" features = [ "eq-separator", "short-space-opt", ] [dependencies.serde] version = "1.0.147" [dependencies.serde_json] version = "1.0.57" [dependencies.wasm-gen] version = "0.1.4" [dev-dependencies.auditable-info] version = "0.10.0" features = ["wasm"] [dev-dependencies.cargo_metadata] version = "0.23" [dev-dependencies.which] version = "8.0.0" cargo-auditable-0.7.2/Cargo.toml.orig000064400000000000000000000020211046102023000155370ustar 00000000000000[package] name = "cargo-auditable" version = "0.7.2" edition = "2021" authors = ["Sergey \"Shnatsel\" Davidoff "] license = "MIT OR Apache-2.0" repository = "https://github.com/rust-secure-code/cargo-auditable" description = "Make production Rust binaries auditable" categories = ["development-tools::cargo-plugins", "encoding"] keywords = ["security", "supply-chain", "sbom", "vulnerabilities"] readme = "../README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] object = {version = "0.37", default-features = false, features = ["write"]} auditable-serde = {version = "0.9.0", path = "../auditable-serde"} miniz_oxide = {version = "0.8.0"} serde_json = "1.0.57" cargo_metadata = "0.23" pico-args = { version = "0.5", features = ["eq-separator", "short-space-opt"] } serde = "1.0.147" wasm-gen = "0.1.4" [dev-dependencies] cargo_metadata = "0.23" auditable-info = {version = "0.10.0", path = "../auditable-info", features = ["wasm"]} which = "8.0.0" cargo-auditable-0.7.2/README.md000064400000000000000000000222561046102023000141430ustar 00000000000000## cargo-auditable Know the exact crate versions used to build your Rust executable. Audit binaries for known bugs or security vulnerabilities in production, at scale, with zero bookkeeping. This works by embedding data about the dependency tree in JSON format into a dedicated linker section of the compiled executable. Linux, Windows and Mac OS are officially supported. [WebAssembly](https://en.wikipedia.org/wiki/WebAssembly) is also supported starting with v0.6.3. All other ELF targets should work, but are not tested on CI. The end goal is to get Cargo itself to encode this information in binaries. There is an RFC for an implementation within Cargo, for which this project paves the way: https://github.com/rust-lang/rfcs/pull/2801 ## Usage ```bash # Install the tools cargo install cargo-auditable cargo-audit # Build your project with dependency lists embedded in the binaries cargo auditable build --release # Scan the binary for vulnerabilities cargo audit bin target/release/your-project ``` `cargo auditable` works with any Cargo command. All arguments are passed to `cargo` as-is. ### On nightly Rust On nightly we can take advantage of Cargo's [native SBOM precursor](https://doc.rust-lang.org/cargo/reference/unstable.html#sbom) to record dependencies more accurately: ```bash CARGO_BUILD_SBOM=true cargo +nightly auditable build -Z sbom --release ``` Due to [a bug in Cargo](https://github.com/rust-lang/cargo/issues/15695) you may have to `touch src/*` or `cargo clean` first if you also used `cargo auditable` without `-Z sbom` in the same project. ### Through other tools If you're not calling `cargo` directly and cannot change how it's invoked, you can use `cargo auditable` as a drop-in replacement for `cargo`. See [here](REPLACING_CARGO.md) for details. ### For Github releases [`cargo dist`](https://github.com/axodotdev/cargo-dist) has opt-in support for `cargo auditable`, see [here](https://axodotdev.github.io/cargo-dist/book/supplychain-security/index.html) for details. ## Adoption Microsoft uses `cargo auditable` internally and previously maintained the [data extraction library for Go](https://github.com/microsoft/go-rustaudit). Multiple Linux distributions build their Rust packages with `cargo auditable`: [Alpine Linux](https://www.alpinelinux.org/), [NixOS](https://nixos.org/), [openSUSE](https://www.opensuse.org/), [Void Linux](https://voidlinux.org/), [Chimera Linux](https://chimera-linux.org/) and [Wolfi OS](https://wolfi.dev). If you install packages from their repositories, you can audit them! [Chainguard](https://chainguard.dev/) includes `cargo auditable` in their [rust base container](https://images.chainguard.dev/directory/image/rust/overview), with a default `cargo` wrapper to always call `cargo auditable`, so that Rust applications built using this container are auditable by default. ## FAQ ### Doesn't this bloat my binary? In a word, no. The embedded dependency list uses under 4kB even on large dependency trees with 400+ entries. This typically translates to between 1/1000 and 1/10,000 of the size of the binary. ### Can I make `cargo` always build with `cargo auditable`? Yes! For example, on Linux/macOS/etc add this to your `.bashrc`: ```bash alias cargo="cargo auditable" ``` If you're using a shell other than bash, or if using an alias is not an option, [see here.](REPLACING_CARGO.md) ### Is there any tooling to consume this data? #### Vulnerability reporting * [cargo audit](https://crates.io/crates/cargo-audit) v0.17.3+ can detect this data in binaries and report on vulnerabilities. See [here](https://github.com/rustsec/rustsec/tree/main/cargo-audit#cargo-audit-bin-subcommand) for details. * [trivy](https://github.com/aquasecurity/trivy) v0.31.0+ detects this data in binaries and reports on vulnerabilities. See the [v0.31.0 release notes](https://github.com/aquasecurity/trivy/discussions/2716) for an end-to-end example. * [grype](https://github.com/anchore/grype) v0.83.0+ detects this data in binaries and container images and reports on vulnerabilities. * [osv-scanner](https://github.com/google/osv-scanner/) v2.0.1+ [reads this data](https://github.com/google/osv-scalibr/pull/377) when scanning container images. #### Recovering the dependency list * [syft](https://github.com/anchore/syft) v1.15.0+ has support for recovering this data and converting it to various formats. Older versions require the `--catalogers all` CLI option. * [docker](https://docs.docker.com/build/metadata/attestations/sbom/) supports embedding CycloneDX documents into container images. If you build a container image with `docker buildx build --tag /: --attest type=sbom --push .` and use `cargo auditable` to build rust binaries in the `Dockerfile`, the SBOM attestation attached to the container image will include your rust dependencies. This is powerd by [BuildKit Syft scanner](https://github.com/docker/buildkit-syft-scanner). * [blint](https://github.com/owasp-dep-scan/blint) v2.1.3+ can recover this data and output it as CycloneDX. * [wasm-tools](https://github.com/bytecodealliance/wasm-tools) v1.227.0+ can recover this data from WebAssembly. Try `wasm-tools metadata show`. * [rust-audit-info](https://crates.io/crates/rust-audit-info) recovers the dependency list from a binary and prints it in JSON. * [auditable2cdx](https://crates.io/crates/auditable2cdx) recovers the dependency list from a binary and prints it in CycloneDX. ### Can I read this data using a tool written in a different language? Yes. The data format is designed for interoperability with alternative implementations. In fact, parsing it only takes [5 lines of Python](PARSING.md). See [here](PARSING.md) for documentation on parsing the data. Besides that, Syft can read it and convert it to a multitude of formats. `auditable2cdx` can convert it to CycloneDX, which is understood by most tools. This conversion lets you feed this data even to tools you cannot modify. ### What is the data format, exactly? The data format is described by the JSON schema [here](cargo-auditable.schema.json). The JSON is Zlib-compressed and placed in a linker section named `.dep-v0`. You can find more info about parsing it [here](PARSING.md). ### What about embedded platforms? Embedded platforms where you cannot spare a byte should not add anything in the executable. Instead they should record the hash of every executable in a database and associate the hash with its Cargo.lock, compiler and LLVM version, build date, etc. This would make for an excellent Cargo wrapper or plugin. Since that can be done in a 5-line shell script, writing that tool is left as an exercise to the reader. ### Does this impact reproducible builds? The data format is specifically designed not to disrupt reproducible builds. It contains no timestamps, and the generated JSON is sorted to make sure it is identical between compilations. If anything, this *helps* with reproducible builds, since you know all the versions for a given binary now. ### Does this disclose any sensitive information? No. All URLs and file paths are redacted, but the crate names and versions are recorded as-is. At present panic messages already disclose all this info and more. Also, chances are that you're legally obligated have to disclose use of specific open-source crates anyway, since MIT and many other licenses require it. ### What about recording the compiler version? The compiler itself [embeds it](https://github.com/rust-lang/rust/pull/97550) in v1.73 and later. On older versions it's already there in the debug info. On Unix you can run `strings your_executable | grep 'rustc version'` to see it. ### What about keeping track of versions of statically linked C libraries? Good question. I don't think they are exposed in any reasonable way right now. Would be a great addition, but not required for the initial launch. We can add it later in a backwards-compatible way. Adopting [the `-src` crate convention](https://internals.rust-lang.org/t/statically-linked-c-c-libraries/17175?u=shnatsel) would make it happen naturally, and will have other benefits as well, so that's probably the best route. ### Does this protect against supply chain attacks? No. Use [`cargo-vet`](https://github.com/mozilla/cargo-vet) or [`cargo-crev`](https://github.com/crev-dev/cargo-crev) for that. [Software Bills of Materials](https://en.wikipedia.org/wiki/Software_supply_chain) (SBOMs) do not prevent supply chain attacks. They cannot even be used to assess the impact of such an attack after it is discovered, because any malicious library worth its bytes will remove itself from the SBOM. This applies to nearly every language and build system, not just Rust and Cargo. Do not rely on SBOMs when dealing with supply chain attacks! ### What is blocking uplifting this into Cargo? The [RFC for this functionality in Cargo itself](https://github.com/rust-lang/rfcs/pull/2801) has been [postponed](https://github.com/rust-lang/rfcs/pull/2801#issuecomment-2122880841) by the Cargo team until the [more foundational SBOM RFC](https://github.com/rust-lang/rfcs/pull/3553). That RFC has now been implemented and is available via an [unstable feature](https://doc.rust-lang.org/cargo/reference/unstable.html#sbom). This opens the door to submitting an RFC for this functionality into `cargo` itself once again. cargo-auditable-0.7.2/cargo-auditable.1000064400000000000000000000124151046102023000157650ustar 00000000000000.TH CARGO-AUDITABLE 1 .SH NAME cargo\-auditable \- Embed a JSON formatted dependency tree into a dedicated linker section of the compiled executable .SH SYNOPSIS \fBcargo\-auditable\fR .SH DESCRIPTION Know the exact crate versions used to build your Rust executable. Audit binaries for known bugs or security vulnerabilities in production, at scale, with zero bookkeeping. This works by embedding data about the dependency tree in JSON format into a dedicated linker section of the compiled executable. Linux, Windows and Mac OS are officially supported. All other ELF targets should work, but are not tested on CI. WASM is currently not supported, but patches are welcome. The end goal is to get Cargo itself to encode this information in binaries. There is an RFC for an implementation within Cargo, for which this project paves the way: https://github.com/rust\-lang/rfcs/pull/2801 .SH USAGE cargo auditable works with any Cargo command. All arguments are passed to cargo as\-is. .SH FAQ Doesn't this bloat my binary? In a word, no. The embedded dependency list uses under 4kB even on large dependency trees with 400+ entries. This typically translates to between 1/1000 and 1/10,000 of the size of the binary. Can I make cargo always build with cargo auditable? Yes! For example, on Linux/macOS/etc add this to your .bashrc: alias cargo="cargo auditable" If you're using a shell other than bash, or if using an alias is not an option, see https://github.com/rust\-secure\-code/cargo\-auditable/blob/HEAD/REPLACING_CARGO.md. Is there any tooling to consume this data? Vulnerability reporting cargo audit v0.17.3+ can detect this data in binaries and report on vulnerabilities. See here for details. trivy v0.31.0+ detects this data in binaries and reports on vulnerabilities. See the v0.31.0 release notes for an end\-to\-end example. Recovering the dependency list syft v0.53.0+ has experimental support for detecting this data in binaries. When used on images or directories, Rust audit support must be enabled by adding the \-\-catalogers all CLI option, e.g syft \-\-catalogers all . rust\-audit\-info recovers the dependency list from a binary and prints it in JSON. It is also interoperable with existing tooling that consumes Cargo.lock via the JSON\-to\-TOML convertor. However, we recommend supporting the format natively; the format is designed to be very easy to parse, even if your language does not have a library for that yet. Can I read this data using a tool written in a different language? Yes. The data format is designed for interoperability with alternative implementations. In fact, parsing it only takes 5 lines of Python. See https://github.com/rust\-secure\-code/cargo\-auditable/blob/HEAD/PARSING.md for documentation on parsing the data. What is the data format, exactly? The data format is described by the JSON schema https://github.com/rust\-secure\-code/cargo\-auditable/blob/HEAD/cargo\-auditable.schema.json. The JSON is Zlib\-compressed and placed in a linker section named .dep\-v0. You can find more info about parsing it here. What about embedded platforms? Embedded platforms where you cannot spare a byte should not add anything in the executable. Instead they should record the hash of every executable in a database and associate the hash with its Cargo.lock, compiler and LLVM version, build date, etc. This would make for an excellent Cargo wrapper or plugin. Since that can be done in a 5\-line shell script, writing that tool is left as an exercise to the reader. Does this impact reproducible builds? The data format is specifically designed not to disrupt reproducible builds. It contains no timestamps, and the generated JSON is sorted to make sure it is identical between compilations. If anything, this helps with reproducible builds, since you know all the versions for a given binary now. Does this disclose any sensitive information? No. All URLs and file paths are redacted, but the crate names and versions are recorded as\-is. At present panic messages already disclose all this info and more. Also, chances are that you're legally obligated have to disclose use of specific open\-source crates anyway, since MIT and many other licenses require it. What about recording the compiler version? The compiler itself will start embedding it soon. On older versions it's already there in the debug info. On Unix you can run strings your_executable | grep 'rustc version' to see it. What about keeping track of versions of statically linked C libraries? Good question. I don't think they are exposed in any reasonable way right now. Would be a great addition, but not required for the initial launch. We can add it later in a backwards\-compatible way. Adopting the \-src crate convention would make it happen naturally, and will have other benefits as well, so that's probably the best route. What is blocking uplifting this into Cargo? Cargo itself is currently in a feature freeze. .SH EXIT STATUS .TP \fB0\fR Successful program execution. .TP \fB1\fR Unsuccessful program execution. .TP \fB101\fR The program panicked. .SH EXAMPLES .TP Build your project with dependency lists embedded in the binaries \fB# cargo auditable build \-\-release\fR .SH AUTHOR .P .RS 2 .nf Sergey "Shnatsel" Davidoff cargo-auditable-0.7.2/src/auditable_from_metadata.rs000064400000000000000000000264421046102023000206370ustar 00000000000000//! Converts from `cargo_metadata` crate structs to `auditable-serde` structs, //! which map to our own serialialized representation. use std::{ cmp::{min, Ordering::*}, collections::{HashMap, HashSet}, error::Error, fmt::Display, }; use auditable_serde::{DependencyKind, Package, Source, VersionInfo}; use cargo_metadata::TargetKind; fn source_from_meta(meta_source: &cargo_metadata::Source) -> Source { match meta_source.repr.as_str() { "registry+https://github.com/rust-lang/crates.io-index" => Source::CratesIo, source => Source::from( source .split('+') .next() .expect("Encoding of source strings in `cargo metadata` has changed!"), ), } } /// The values are ordered from weakest to strongest so that casting to integer would make sense #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] enum PrivateDepKind { Development, Build, Runtime, } impl From for DependencyKind { fn from(priv_kind: PrivateDepKind) -> Self { match priv_kind { PrivateDepKind::Development => { panic!("Cannot convert development dependency to serializable format") } PrivateDepKind::Build => DependencyKind::Build, PrivateDepKind::Runtime => DependencyKind::Runtime, } } } impl From<&cargo_metadata::DependencyKind> for PrivateDepKind { fn from(kind: &cargo_metadata::DependencyKind) -> Self { match kind { cargo_metadata::DependencyKind::Normal => PrivateDepKind::Runtime, cargo_metadata::DependencyKind::Development => PrivateDepKind::Development, cargo_metadata::DependencyKind::Build => PrivateDepKind::Build, _ => panic!("Unknown dependency kind"), } } } /// Error returned by the conversion from /// [`cargo_metadata::Metadata`](https://docs.rs/cargo_metadata/0.11.1/cargo_metadata/struct.Metadata.html) #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum InsufficientMetadata { NoDeps, VirtualWorkspace, } impl Display for InsufficientMetadata { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { InsufficientMetadata::NoDeps => { write!(f, "Missing dependency information! Please call 'cargo metadata' without '--no-deps' flag.") } InsufficientMetadata::VirtualWorkspace => { write!(f, "Missing root crate! Please call this from a package directory, not workspace root.") } } } } impl Error for InsufficientMetadata {} pub fn encode_audit_data( metadata: &cargo_metadata::Metadata, ) -> Result { let toplevel_crate_id = metadata .resolve .as_ref() .ok_or(InsufficientMetadata::NoDeps)? .root .as_ref() .ok_or(InsufficientMetadata::VirtualWorkspace)? .repr .as_str(); let proc_macros = proc_macro_packages(metadata); // Walk the dependency tree and resolve dependency kinds for each package. // We need this because there may be several different paths to the same package // and we need to aggregate dependency types across all of them. // Moreover, `cargo metadata` doesn't propagate dependency information: // A runtime dependency of a build dependency of your package should be recorded // as *build* dependency, but Cargo flags it as a runtime dependency. // Hoo boy, here I go hand-rolling BFS again! let nodes = &metadata.resolve.as_ref().unwrap().nodes; let id_to_node: HashMap<&str, &cargo_metadata::Node> = nodes.iter().map(|n| (n.id.repr.as_str(), n)).collect(); let mut id_to_dep_kind: HashMap<&str, PrivateDepKind> = HashMap::new(); id_to_dep_kind.insert(toplevel_crate_id, PrivateDepKind::Runtime); let mut current_queue: Vec<&cargo_metadata::Node> = vec![id_to_node[toplevel_crate_id]]; let mut next_step_queue: Vec<&cargo_metadata::Node> = Vec::new(); while !current_queue.is_empty() { for parent in current_queue.drain(..) { let parent_dep_kind = id_to_dep_kind[parent.id.repr.as_str()]; for child in &parent.deps { let child_id = child.pkg.repr.as_str(); let mut dep_kind = strongest_dep_kind(child.dep_kinds.as_slice()); // If the parent is a build dependency that has a runtime dependency, overall dependency should be 'build'. // This propagates the dependency kinds that way from parent to child. dep_kind = min(dep_kind, parent_dep_kind); // proc macros require special handling since cargo_metadata reports them as normal deps if proc_macros.contains(child_id) { dep_kind = min(dep_kind, PrivateDepKind::Build); } let dep_kind_on_previous_visit = id_to_dep_kind.get(child_id); if dep_kind_on_previous_visit.is_none() || &dep_kind > dep_kind_on_previous_visit.unwrap() { // if we haven't visited this node in dependency graph yet // or if we've visited it with a weaker dependency type, // records its new dependency type and add it to the queue to visit its dependencies id_to_dep_kind.insert(child_id, dep_kind); next_step_queue.push(id_to_node[child_id]); } } } std::mem::swap(&mut next_step_queue, &mut current_queue); } let metadata_package_dep_kind = |p: &cargo_metadata::Package| { let package_id = p.id.repr.as_str(); id_to_dep_kind.get(package_id) }; // Remove dev-only dependencies from the package list and collect them to Vec let mut packages: Vec<&cargo_metadata::Package> = metadata .packages .iter() .filter(|p| { let dep_kind = metadata_package_dep_kind(p); // Dependencies that are present in the workspace but not used by the current root crate // will not be in the map we've built by traversing the root crate's dependencies. // In this case they will not be in the map at all. We skip them, along with dev-dependencies. dep_kind.is_some() && dep_kind.unwrap() != &PrivateDepKind::Development }) .collect(); // This function is the simplest place to introduce sorting, since // it contains enough data to distinguish between equal-looking packages // and provide a stable sorting that might not be possible // using the data from VersionInfo struct alone. // // We use sort_unstable here because there is no point in // not reordering equal elements, since they're supplied by // in arbitrary order by cargo-metadata anyway // and the order even varies between executions. packages.sort_unstable_by(|a, b| { // This is a workaround for Package not implementing Ord. // Deriving it in cargo_metadata might be more reliable? let names_order = a.name.cmp(&b.name); if names_order != Equal { return names_order; } let versions_order = a.name.cmp(&b.name); if versions_order != Equal { return versions_order; } // IDs are unique so comparing them should be sufficient a.id.repr.cmp(&b.id.repr) }); // Build a mapping from package ID to the index of that package in the Vec // because serializable representation doesn't store IDs let mut id_to_index = HashMap::new(); for (index, package) in packages.iter().enumerate() { id_to_index.insert(package.id.repr.as_str(), index); } // Convert packages from cargo-metadata representation to our representation let mut packages: Vec = packages .into_iter() .map(|p| Package { name: p.name.to_string(), version: p.version.clone(), source: p.source.as_ref().map_or(Source::Local, source_from_meta), kind: (*metadata_package_dep_kind(p).unwrap()).into(), dependencies: Vec::new(), root: p.id.repr == toplevel_crate_id, }) .collect(); // Fill in dependency info from resolved dependency graph for node in metadata.resolve.as_ref().unwrap().nodes.iter() { let package_id = node.id.repr.as_str(); if id_to_index.contains_key(package_id) { // dev-dependencies are not included let package: &mut Package = &mut packages[id_to_index[package_id]]; // Dependencies for dep in node.deps.iter() { // Omit the graph edge if this is a development dependency // to fix https://github.com/rustsec/rustsec/issues/1043 // It is possible that something that we depend on normally // is also a dev-dependency for something, // and dev-dependencies are allowed to have cycles, // so we may end up encoding cyclic graph if we don't handle that. let dep_id = dep.pkg.repr.as_str(); if strongest_dep_kind(&dep.dep_kinds) != PrivateDepKind::Development { package.dependencies.push(id_to_index[dep_id]); } } // .sort_unstable() is fine because they're all integers package.dependencies.sort_unstable(); } } Ok(VersionInfo { packages, format: 1, }) } fn strongest_dep_kind(deps: &[cargo_metadata::DepKindInfo]) -> PrivateDepKind { deps.iter() .map(|d| PrivateDepKind::from(&d.kind)) .max() .unwrap_or(PrivateDepKind::Runtime) // for compatibility with Rust earlier than 1.41 } fn proc_macro_packages(metadata: &cargo_metadata::Metadata) -> HashSet<&str> { metadata .packages .iter() .filter_map(|pkg| { // As of Rust 1.88 a single crate cannot be both a proc macro and something else. // Checking that length is 1 is purely to hedge against support for it being added in the future. if pkg.targets.len() == 1 && pkg.targets[0].kind.len() == 1 && pkg.targets[0].kind[0] == TargetKind::ProcMacro { Some(pkg.id.repr.as_str()) } else { None } }) .collect() } #[cfg(test)] mod tests { #![allow(unused_imports)] // otherwise conditional compilation emits warnings use super::*; use std::fs; use std::{ convert::TryInto, path::{Path, PathBuf}, str::FromStr, }; fn load_metadata(cargo_toml_path: &Path) -> cargo_metadata::Metadata { let mut cmd = cargo_metadata::MetadataCommand::new(); cmd.manifest_path(cargo_toml_path); cmd.exec().unwrap() } #[test] fn dependency_cycle() { let cargo_toml_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()) .join("tests/fixtures/cargo-audit-dep-cycle/Cargo.toml"); let metadata = load_metadata(&cargo_toml_path); let version_info_struct: VersionInfo = encode_audit_data(&metadata).unwrap(); let json = serde_json::to_string(&version_info_struct).unwrap(); VersionInfo::from_str(&json).unwrap(); // <- the part we care about succeeding } } cargo-auditable-0.7.2/src/binary_file.rs000064400000000000000000000027771046102023000163120ustar 00000000000000//! Wrapper around object_file.rs to keep it as intact as possible, //! because it is lifted from rustc use crate::{object_file, platform_detection::is_wasm, target_info::RustcTargetInfo}; /// Creates a binary file (ELF/Mach-O/PE/WASM) with the specified contents in a given section /// which can be passed to the linker to include the section into the final executable. /// /// Returns `None` if the architecture is not supported. pub fn create_binary_file( target_info: &RustcTargetInfo, target_triple: &str, contents: &[u8], symbol_name: &str, ) -> Option> { if is_wasm(target_info) { Some(create_wasm_file(target_info, contents)) } else { object_file::create_metadata_file(target_info, target_triple, contents, symbol_name) } } pub fn create_wasm_file( // formerly `create_compressed_metadata_file` in the rustc codebase target_info: &RustcTargetInfo, contents: &[u8], ) -> Vec { assert!(is_wasm(target_info)); // Start with the minimum valid WASM file let mut result: Vec = vec![0, b'a', b's', b'm', 1, 0, 0, 0]; // Add the `linking` section with version 2 that rust-lld expects. // This is required to mark the WASM file as relocatable, // otherwise the linker will reject it as a non-linkable file. // https://github.com/WebAssembly/tool-conventions/blob/master/Linking.md wasm_gen::write_custom_section(&mut result, "linking", &[2]); wasm_gen::write_custom_section(&mut result, ".dep-v0", contents); result } cargo-auditable-0.7.2/src/cargo_arguments.rs000064400000000000000000000066731046102023000172060ustar 00000000000000use std::ffi::OsString; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] /// Includes only the cargo arguments we care about pub struct CargoArgs { pub offline: bool, pub locked: bool, pub frozen: bool, pub config: Vec, } impl CargoArgs { /// Extracts Cargo flags from the arguments to the current process pub fn from_args() -> CargoArgs { // we .skip(3) to get over `cargo auditable build` and to the start of the flags let raw_args: Vec = std::env::args_os().skip(3).collect(); Self::from_args_vec(raw_args) } /// Split into its own function for unit testing fn from_args_vec(mut raw_args: Vec) -> CargoArgs { // if there is a -- in the invocation somewhere, only parse up to it if let Some(position) = raw_args.iter().position(|s| s == "--") { raw_args.truncate(position); } let mut parser = pico_args::Arguments::from_vec(raw_args); CargoArgs { config: parser.values_from_str("--config").unwrap(), offline: parser.contains("--offline"), locked: parser.contains("--locked"), frozen: parser.contains("--frozen"), } } /// Recovers `SerializedCargoArgs` from an environment variable (if it was exported earlier) pub fn from_env() -> Result { let json_args = std::env::var("CARGO_AUDITABLE_ORIG_ARGS")?; // We unwrap here because we've serialized these args ourselves and they should roundtrip cleanly. // Deserialization would only fail if someone tampered with them in transit. Ok(serde_json::from_str(&json_args).unwrap()) } } #[cfg(test)] mod tests { use super::*; #[test] fn basic_parsing() { let input = [ "cargo", "auditable", "build", "--locked", "--config", "net.git-fetch-with-cli=true", "--offline", ]; let raw_args = input.iter().map(OsString::from).collect(); let args = CargoArgs::from_args_vec(raw_args); assert!(args.locked); assert!(args.offline); assert!(!args.frozen); assert_eq!(args.config, vec!["net.git-fetch-with-cli=true"]); } #[test] fn with_unrelated_flags() { let input = [ "cargo", "auditable", "build", "--locked", "--target", "x86_64-unknown-linux-gnu", "--release", "--config", "net.git-fetch-with-cli=true", "--offline", "--ignore-rust-version", ]; let raw_args = input.iter().map(OsString::from).collect(); let args = CargoArgs::from_args_vec(raw_args); assert!(args.locked); assert!(args.offline); assert!(!args.frozen); assert_eq!(args.config, vec!["net.git-fetch-with-cli=true"]); } #[test] fn double_dash_to_ignore_args() { let input = [ "cargo", "auditable", "run", "--release", "--config", "net.git-fetch-with-cli=true", "--", "--offline", ]; let raw_args = input.iter().map(OsString::from).collect(); let args = CargoArgs::from_args_vec(raw_args); assert!(!args.offline); assert_eq!(args.config, vec!["net.git-fetch-with-cli=true"]); } } cargo-auditable-0.7.2/src/cargo_auditable.rs000064400000000000000000000054351046102023000171260ustar 00000000000000use crate::cargo_arguments::CargoArgs; use std::{env, process::Command}; pub fn main() { // set the RUSTFLAGS environment variable to inject our object and call Cargo with all the Cargo args // Cargo sets the path to itself in the `CARGO` environment variable: // https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-3rd-party-subcommands // This is also useful for using `cargo auditable` as a drop-in replacement for Cargo. let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); let mut command = Command::new(cargo); // Pass along all our arguments; we don't currently have any args specific to `cargo auditable` // We skip argv[0] which is the path to this binary and the first argument which is 'auditable' passed by Cargo command.args(env::args_os().skip(2)); // Set the environment variable to use this binary as a rustc wrapper, that's when we do the real work // It's important that we set RUSTC_WORKSPACE_WRAPPER and not RUSTC_WRAPPER because only the former invalidates cache. // If we use RUSTC_WRAPPER, running `cargo auditable` will not trigger a rebuild. // The WORKSPACE part is a bit of a misnomer: it will be run for a local crate even if there's just one, not a workspace. // Security note: // `std::env::current_exe()` is not supposed to be relied on for security - the binary may be moved, etc. // But should not a code execution vulnerability since whoever sets this could set RUSTC_WORKSPACE_WRAPPER themselves // This would matter if the binary was made setuid, but it isn't, so this should be fine. let path_to_this_binary = std::env::current_exe().unwrap(); command.env("RUSTC_WORKSPACE_WRAPPER", path_to_this_binary); // Pass on the arguments we received so that they can be inspected later. // We're interested in flags like `--offline` and `--config` which have to be passed to `cargo metadata` later. // The shell has already split them for us and we don't want to mangle them, but we need to round-trip them // through a string. Since we already depend on `serde-json` and it does the job, use JSON. // This doesn't support non-UTF8 arguments, but `cargo_metadata` crate doesn't support them either, // so this is not an issue right now. // If it ever becomes one, we could use the `serde-bytes-repr` crate for a clean round-trip. let args = CargoArgs::from_args(); let args_in_json = serde_json::to_string(&args).unwrap(); command.env("CARGO_AUDITABLE_ORIG_ARGS", args_in_json); let results = command .status() .expect("Failed to invoke cargo! Make sure it's in your $PATH"); let code = results .code() .expect("cargo was terminated by a deadly signal"); std::process::exit(code); } cargo-auditable-0.7.2/src/collect_audit_data.rs000064400000000000000000000121601046102023000176160ustar 00000000000000use cargo_metadata::{Metadata, MetadataCommand}; use miniz_oxide::deflate::compress_to_vec_zlib; use std::str::from_utf8; use crate::{ auditable_from_metadata::encode_audit_data, cargo_arguments::CargoArgs, rustc_arguments::RustcArgs, sbom_precursor, }; /// Calls `cargo metadata` to obtain the dependency tree, serializes it to JSON and compresses it pub fn compressed_dependency_list(rustc_args: &RustcArgs, target_triple: &str) -> Vec { let sbom_path = std::env::var_os("CARGO_SBOM_PATH"); // If cargo has created precursor SBOM files, use them instead of `cargo metadata`. let version_info = if sbom_path.as_ref().map(|p| !p.is_empty()).unwrap_or(false) { // Cargo creates an SBOM file for each output file (rlib, bin, cdylib, etc), // but the SBOM file is identical for each output file in a given rustc crate compilation, // so we can just use the first SBOM we find. let sbom_path = std::env::split_paths(&sbom_path.unwrap()).next().unwrap(); let sbom_data: Vec = std::fs::read(&sbom_path) .unwrap_or_else(|_| panic!("Failed to read SBOM file at {}", sbom_path.display())); let sbom_precursor: sbom_precursor::SbomPrecursor = serde_json::from_slice(&sbom_data) .unwrap_or_else(|_| panic!("Failed to parse SBOM file at {}", sbom_path.display())); sbom_precursor.into() } else { // If no SBOM files are available, fall back to `cargo metadata` let metadata = get_metadata(rustc_args, target_triple); encode_audit_data(&metadata).unwrap() }; let json = serde_json::to_string(&version_info).unwrap(); // compression level 7 makes this complete in a few milliseconds, so no need to drop to a lower level in debug mode let compressed_json = compress_to_vec_zlib(json.as_bytes(), 7); compressed_json } fn get_metadata(args: &RustcArgs, target_triple: &str) -> Metadata { let mut metadata_command = MetadataCommand::new(); // Cargo sets the path to itself in the `CARGO` environment variable: // https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-3rd-party-subcommands // This is also useful for using `cargo auditable` as a drop-in replacement for Cargo. if let Some(path) = std::env::var_os("CARGO") { metadata_command.cargo_path(path); } // Point cargo-metadata to the correct Cargo.toml in a workspace. // CARGO_MANIFEST_DIR env var will be set by Cargo when it calls our rustc wrapper let manifest_dir = std::env::var_os("CARGO_MANIFEST_DIR").unwrap(); metadata_command.current_dir(manifest_dir); // Pass the features that are actually enabled for this crate to cargo-metadata let mut features = args.enabled_features(); if let Some(index) = features.iter().position(|x| x == &"default") { features.remove(index); } else { metadata_command.features(cargo_metadata::CargoOpt::NoDefaultFeatures); } let owned_features: Vec = features.iter().map(|s| s.to_string()).collect(); metadata_command.features(cargo_metadata::CargoOpt::SomeFeatures(owned_features)); // Restrict the dependency resolution to just the platform the binary is being compiled for. // By default `cargo metadata` resolves the dependency tree for all platforms. let mut other_args = vec!["--filter-platform".to_owned(), target_triple.to_owned()]; // Pass arguments such as `--config`, `--offline` and `--locked` // from the original CLI invocation of `cargo auditable` let orig_args = CargoArgs::from_env() .expect("Env var 'CARGO_AUDITABLE_ORIG_ARGS' set by 'cargo-auditable' is unset!"); if orig_args.offline { other_args.push("--offline".to_owned()); } if orig_args.frozen { other_args.push("--frozen".to_owned()); } if orig_args.locked { other_args.push("--locked".to_owned()); } for arg in orig_args.config { other_args.push("--config".to_owned()); other_args.push(arg); } // This can only be done once, multiple calls will replace previously set options. metadata_command.other_options(other_args); // Get the underlying std::process::Command and re-implement MetadataCommand::exec, // to clear RUSTC_WORKSPACE_WRAPPER in the child process to avoid recursion. // The alternative would be modifying the environment of our own process, // which is sketchy and discouraged on POSIX because it's not thread-safe: // https://doc.rust-lang.org/stable/std/env/fn.remove_var.html let mut metadata_command = metadata_command.cargo_command(); metadata_command.env_remove("RUSTC_WORKSPACE_WRAPPER"); let output = metadata_command.output().unwrap(); if !output.status.success() { panic!( "cargo metadata failure: {}", String::from_utf8_lossy(&output.stderr) ); } let stdout = from_utf8(&output.stdout) .expect("cargo metadata output not utf8") .lines() .find(|line| line.starts_with('{')) .expect("cargo metadata output not json"); MetadataCommand::parse(stdout).expect("failed to parse cargo metadata output") } cargo-auditable-0.7.2/src/main.rs000064400000000000000000000031401046102023000147340ustar 00000000000000#![forbid(unsafe_code)] mod auditable_from_metadata; mod binary_file; mod cargo_arguments; mod cargo_auditable; mod collect_audit_data; mod object_file; mod platform_detection; mod rustc_arguments; mod rustc_wrapper; mod sbom_precursor; mod target_info; use std::process::exit; /// Dispatches the call to either `cargo auditable` when invoked through cargo, /// or to `rustc_wrapper` when Cargo internals invoke it fn main() { let first_arg = std::env::args_os().nth(1); if let Some(arg) = first_arg { if arg == "auditable" { cargo_auditable::main() } // When this binary is called as a rustc wrapper, the first argument is the path to rustc: // https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-reads // It's important to read it because it can be overridden via env vars or config files. // In order to distinguish that from someone running the binary directly by mistake, // we check if the env var we set earlier is still present. // The "rustc" special-case is purely to accommodate the weird things `sccache` does: // https://github.com/rust-secure-code/cargo-auditable/issues/87 // We should push back and make it sccache's problem if this ever causes issues. else if arg == "rustc" || std::env::var_os("CARGO_AUDITABLE_ORIG_ARGS").is_some() { rustc_wrapper::main(&arg) } else { shoo(); } } else { shoo(); } } fn shoo() -> ! { eprintln!("'cargo auditable' should be invoked through Cargo"); exit(1); } cargo-auditable-0.7.2/src/object_file.rs000064400000000000000000000345301046102023000162640ustar 00000000000000//! Shamelessly copied from rustc codebase: //! https://github.com/rust-lang/rust/blob/dcca6a375bd4eddb3deea7038ebf29d02af53b48/compiler/rustc_codegen_ssa/src/back/metadata.rs#L97-L206 //! and butchered ever so slightly use object::write::{self, StandardSegment, Symbol, SymbolSection}; use object::{ elf, Architecture, BinaryFormat, Endianness, FileFlags, SectionFlags, SectionKind, SymbolFlags, SymbolKind, SymbolScope, }; use crate::platform_detection::{is_32bit, is_apple, is_windows}; use crate::target_info::RustcTargetInfo; /// Returns None if the architecture is not supported pub fn create_metadata_file( // formerly `create_compressed_metadata_file` in the rustc codebase target_info: &RustcTargetInfo, target_triple: &str, contents: &[u8], symbol_name: &str, ) -> Option> { let mut file = create_object_file(target_info, target_triple)?; let section = file.add_section( file.segment_name(StandardSegment::Data).to_vec(), b".dep-v0".to_vec(), SectionKind::ReadOnlyData, ); if let BinaryFormat::Elf = file.format() { // Explicitly set no flags to avoid SHF_ALLOC default for data section. file.section_mut(section).flags = SectionFlags::Elf { sh_flags: 0 }; }; let offset = file.append_section_data(section, contents, 1); // For MachO and probably PE this is necessary to prevent the linker from throwing away the // .rustc section. For ELF this isn't necessary, but it also doesn't harm. file.add_symbol(Symbol { name: symbol_name.as_bytes().to_vec(), value: offset, size: contents.len() as u64, kind: SymbolKind::Data, scope: SymbolScope::Dynamic, weak: false, section: SymbolSection::Section(section), flags: SymbolFlags::None, }); Some(file.write().unwrap()) } fn create_object_file( info: &RustcTargetInfo, target_triple: &str, ) -> Option> { // This conversion evolves over time, and has some subtle logic for MIPS and RISC-V later on, that also evolves. // If/when uplifiting this into Cargo, we will need to extract this code from rustc and put it in the `object` crate // so that it could be shared between rustc and Cargo. let endianness = match info["target_endian"].as_str() { "little" => Endianness::Little, "big" => Endianness::Big, _ => unreachable!(), }; let architecture = match info["target_arch"].as_str() { "arm" => Architecture::Arm, "aarch64" => { if is_32bit(info) { Architecture::Aarch64_Ilp32 } else { Architecture::Aarch64 } } "x86" => Architecture::I386, "s390x" => Architecture::S390x, "mips" => Architecture::Mips, "mips64" => Architecture::Mips64, "x86_64" => { if is_32bit(info) { Architecture::X86_64_X32 } else { Architecture::X86_64 } } "powerpc" => Architecture::PowerPc, "powerpc64" => Architecture::PowerPc64, "riscv32" => Architecture::Riscv32, "riscv64" => Architecture::Riscv64, "sparc64" => Architecture::Sparc64, "loongarch64" => Architecture::LoongArch64, // Unsupported architecture. _ => return None, }; let binary_format = if is_apple(info) { BinaryFormat::MachO } else if is_windows(info) { BinaryFormat::Coff } else { BinaryFormat::Elf }; let mut file = write::Object::new(binary_format, architecture, endianness); let e_flags = match architecture { Architecture::Mips => { // the original code matches on info we don't have to support pre-1999 MIPS variants: // https://github.com/rust-lang/rust/blob/dcca6a375bd4eddb3deea7038ebf29d02af53b48/compiler/rustc_codegen_ssa/src/back/metadata.rs#L144C3-L153 // We can't support them, so this part was was modified significantly. let arch = if target_triple.contains("r6") { elf::EF_MIPS_ARCH_32R6 } else { elf::EF_MIPS_ARCH_32R2 }; // end of modified part // The only ABI LLVM supports for 32-bit MIPS CPUs is o32. let mut e_flags = elf::EF_MIPS_CPIC | elf::EF_MIPS_ABI_O32 | arch; // commented out: insufficient info to support this outside rustc // if sess.target.options.relocation_model != RelocModel::Static { // e_flags |= elf::EF_MIPS_PIC; // } if target_triple.contains("r6") { e_flags |= elf::EF_MIPS_NAN2008; } e_flags } Architecture::Mips64 => { // copied from `mips64el-linux-gnuabi64-gcc foo.c -c` #[allow(clippy::let_and_return)] // for staying as close to upstream as possible let e_flags = elf::EF_MIPS_CPIC | elf::EF_MIPS_PIC | if target_triple.contains("r6") { elf::EF_MIPS_ARCH_64R6 | elf::EF_MIPS_NAN2008 } else { elf::EF_MIPS_ARCH_64R2 }; e_flags } Architecture::Riscv32 | Architecture::Riscv64 => { // Source: https://github.com/riscv-non-isa/riscv-elf-psabi-doc/blob/079772828bd10933d34121117a222b4cc0ee2200/riscv-elf.adoc let mut e_flags: u32 = 0x0; let features = riscv_features(target_triple, info); // Check if compressed is enabled if features.contains('c') { e_flags |= elf::EF_RISCV_RVC; } // Select the appropriate floating-point ABI if features.contains('d') { e_flags |= elf::EF_RISCV_FLOAT_ABI_DOUBLE; } else if features.contains('f') { e_flags |= elf::EF_RISCV_FLOAT_ABI_SINGLE; } else { e_flags |= elf::EF_RISCV_FLOAT_ABI_SOFT; } e_flags } Architecture::LoongArch64 => { // Source: https://github.com/loongson/la-abi-specs/blob/release/laelf.adoc#e_flags-identifies-abi-type-and-version let mut e_flags: u32 = elf::EF_LARCH_OBJABI_V1; let features = loongarch_features(target_triple); // Select the appropriate floating-point ABI if features.contains('d') { e_flags |= elf::EF_LARCH_ABI_DOUBLE_FLOAT; } else if features.contains('f') { e_flags |= elf::EF_LARCH_ABI_SINGLE_FLOAT; } else { e_flags |= elf::EF_LARCH_ABI_SOFT_FLOAT; } e_flags } _ => 0, }; // adapted from LLVM's `MCELFObjectTargetWriter::getOSABI` let os_abi = match info["target_os"].as_str() { "hermit" => elf::ELFOSABI_STANDALONE, "freebsd" => elf::ELFOSABI_FREEBSD, "solaris" => elf::ELFOSABI_SOLARIS, _ => elf::ELFOSABI_NONE, }; let abi_version = 0; file.flags = FileFlags::Elf { os_abi, abi_version, e_flags, }; Some(file) } // This function was not present in the original rustc code, which simply used // `sess.target.options.features` // We do not have access to compiler internals, so we have to reimplement this function. // And `rustc --print=cfg` doesn't expose some of the features we care about, // specifically the 'd' and 'f' features. // Hence this function, which is not as robust as I would like. fn riscv_features(target_triple: &str, info: &RustcTargetInfo) -> String { let arch = target_triple.split('-').next().unwrap(); assert_eq!(&arch[..5], "riscv"); let mut extensions = arch[7..].to_owned(); if extensions.contains('g') { extensions.push_str("imadf"); } // Most but not all riscv targets declare target features. // A notable exception is `riscv64-linux-android`. // We assume that all Linux-capable targets are -gc. match info["target_os"].as_str() { "linux" | "android" => extensions.push_str("imadfc"), _ => (), } extensions } // This function was not present in the original rustc code, which simply used // `sess.target.options.features` // We do not have access to compiler internals, so we have to reimplement this function. fn loongarch_features(target_triple: &str) -> String { match target_triple { "loongarch64-unknown-none-softfloat" => "".to_string(), _ => "f,d".to_string(), } } #[cfg(test)] mod tests { use std::collections::HashMap; use super::*; use crate::target_info::parse_rustc_target_info; #[test] fn test_riscv_abi_detection() { // real-world target with double floats let info = HashMap::from([("target_os".to_owned(), "linux".to_owned())]); let features = riscv_features("riscv64gc-unknown-linux-gnu", &info); assert!(features.contains('c')); assert!(features.contains('d')); assert!(features.contains('f')); // real-world target without floats let info = HashMap::from([("target_os".to_owned(), "none".to_owned())]); let features = riscv_features("riscv32imac-unknown-none-elf", &info); assert!(features.contains('c')); assert!(!features.contains('d')); assert!(!features.contains('f')); // real-world target without floats or compression let info = HashMap::from([("target_os".to_owned(), "none".to_owned())]); let features = riscv_features("riscv32i-unknown-none-elf", &info); assert!(!features.contains('c')); assert!(!features.contains('d')); assert!(!features.contains('f')); // made-up target without compression and with single floats let info = HashMap::from([("target_os".to_owned(), "none".to_owned())]); let features = riscv_features("riscv32if-unknown-none-elf", &info); assert!(!features.contains('c')); assert!(!features.contains('d')); assert!(features.contains('f')); // real-world Android riscv target let info = HashMap::from([("target_os".to_owned(), "android".to_owned())]); let features = riscv_features("riscv64-linux-android", &info); assert!(features.contains('c')); assert!(features.contains('d')); assert!(features.contains('f')); } #[test] fn test_loongarch_abi_detection() { // real-world target with double floats let features = loongarch_features("loongarch64-unknown-linux-gnu"); assert!(features.contains('d')); assert!(features.contains('f')); // real-world target with double floats let features = loongarch_features("loongarch64-unknown-linux-musl"); assert!(features.contains('d')); assert!(features.contains('f')); // real-world target with double floats let features = loongarch_features("loongarch64-unknown-none"); assert!(features.contains('d')); assert!(features.contains('f')); // real-world target with soft floats let features = loongarch_features("loongarch64-unknown-none-softfloat"); assert!(!features.contains('d')); assert!(!features.contains('f')); } #[test] fn test_create_object_file_linux() { let rustc_output = br#"debug_assertions target_arch="x86_64" target_endian="little" target_env="gnu" target_family="unix" target_feature="fxsr" target_feature="sse" target_feature="sse2" target_os="linux" target_pointer_width="64" target_vendor="unknown" unix "#; let target_triple = "x86_64-unknown-linux-gnu"; let target_info = parse_rustc_target_info(rustc_output); let result = create_object_file(&target_info, target_triple).unwrap(); assert_eq!(result.format(), BinaryFormat::Elf); assert_eq!(result.architecture(), Architecture::X86_64); } #[test] fn test_create_object_file_windows_msvc() { let rustc_output = br#"debug_assertions target_arch="x86_64" target_endian="little" target_env="msvc" target_family="windows" target_feature="fxsr" target_feature="sse" target_feature="sse2" target_os="windows" target_pointer_width="64" target_vendor="pc" windows "#; let target_triple = "x86_64-pc-windows-msvc"; let target_info = parse_rustc_target_info(rustc_output); let result = create_object_file(&target_info, target_triple).unwrap(); assert_eq!(result.format(), BinaryFormat::Coff); assert_eq!(result.architecture(), Architecture::X86_64); } #[test] fn test_create_object_file_windows_gnu() { let rustc_output = br#"debug_assertions target_arch="x86_64" target_endian="little" target_env="gnu" target_family="windows" target_feature="fxsr" target_feature="sse" target_feature="sse2" target_os="windows" target_pointer_width="64" target_vendor="pc" windows "#; let target_triple = "x86_64-pc-windows-gnu"; let target_info = crate::target_info::parse_rustc_target_info(rustc_output); let result = create_object_file(&target_info, target_triple).unwrap(); assert_eq!(result.format(), BinaryFormat::Coff); assert_eq!(result.architecture(), Architecture::X86_64); } #[test] fn test_create_object_file_macos() { let rustc_output = br#"debug_assertions target_arch="x86_64" target_endian="little" target_env="" target_family="unix" target_feature="fxsr" target_feature="sse" target_feature="sse2" target_feature="sse3" target_feature="ssse3" target_os="macos" target_pointer_width="64" target_vendor="apple" unix "#; let target_triple = "x86_64-apple-darwin"; let target_info = crate::target_info::parse_rustc_target_info(rustc_output); let result = create_object_file(&target_info, target_triple).unwrap(); assert_eq!(result.format(), BinaryFormat::MachO); assert_eq!(result.architecture(), Architecture::X86_64); } #[test] fn test_create_object_file_linux_arm() { let rustc_output = br#"debug_assertions target_arch="aarch64" target_endian="little" target_env="gnu" target_family="unix" target_os="linux" target_pointer_width="64" target_vendor="unknown" unix "#; let target_triple = "aarch64-unknown-linux-gnu"; let target_info = parse_rustc_target_info(rustc_output); let result = create_object_file(&target_info, target_triple).unwrap(); assert_eq!(result.format(), BinaryFormat::Elf); assert_eq!(result.architecture(), Architecture::Aarch64); } } cargo-auditable-0.7.2/src/platform_detection.rs000064400000000000000000000014631046102023000177000ustar 00000000000000//! Utilities to reliably and consistently detect various platforms use crate::target_info::RustcTargetInfo; pub fn is_wasm(target_info: &RustcTargetInfo) -> bool { key_equals(target_info, "target_family", "wasm") } pub fn is_msvc(target_info: &RustcTargetInfo) -> bool { key_equals(target_info, "target_env", "msvc") } pub fn is_apple(target_info: &RustcTargetInfo) -> bool { key_equals(target_info, "target_vendor", "apple") } pub fn is_windows(target_info: &RustcTargetInfo) -> bool { key_equals(target_info, "target_os", "windows") } pub fn is_32bit(target_info: &RustcTargetInfo) -> bool { key_equals(target_info, "target_pointer_width", "32") } fn key_equals(target_info: &RustcTargetInfo, key: &str, value: &str) -> bool { target_info.get(key).map(|s| s.as_str()) == Some(value) } cargo-auditable-0.7.2/src/rustc_arguments.rs000064400000000000000000000234421046102023000172440ustar 00000000000000//! Parses rustc arguments to extract the info not provided via environment variables. use std::{ffi::OsString, path::PathBuf}; // We use pico-args because we only need to extract a few specific arguments out of a larger set, // and other parsers (rustc's `getopts`, cargo's `clap`) make that difficult. // // We also intentionally do very little validation, to avoid rejecting new configurations // that may be added to rustc in the future. // // For reference, the rustc argument parsing code is at // https://github.com/rust-lang/rust/blob/26ecd44160f54395b3bd5558cc5352f49cb0a0ba/compiler/rustc_session/src/config.rs /// Includes only the rustc arguments we care about #[derive(Debug)] pub struct RustcArgs { pub crate_name: Option, pub crate_types: Vec, pub cfg: Vec, pub emit: Vec, pub out_dir: Option, pub target: Option, pub print: Vec, pub codegen: Vec, } impl RustcArgs { pub fn enabled_features(&self) -> Vec<&str> { let mut result = Vec::new(); for item in &self.cfg { if item.starts_with("feature=\"") { // feature names cannot contain quotes according to the documentation: // https://doc.rust-lang.org/cargo/reference/features.html#the-features-section result.push(item.split('"').nth(1).unwrap()); } } result } /// Normally `rustc` uses a C compiler such as `cc` or `clang` as linker, /// and arguments to the actual linker need to be passed prefixed with `-Wl,`. /// But it is possible to configure Cargo and rustc to call a linker directly, /// and the breakage it causes is subtle enough that people just roll with it /// and complain when cargo-auditable doesn't support this configuration: /// /// /// This function can tell you if a bare linker is in use /// and whether you need to prepend `-Wl,` or not. /// /// Such setups are exceptionally rare and frankly it's a misconfiguration /// that will break more than just `cargo auditable`, but I am feeling generous. pub fn bare_linker(&self) -> bool { let linker_flag = self.codegen.iter().find(|s| s.starts_with("linker=")); if let Some(linker_flag) = linker_flag { let linker = linker_flag.strip_prefix("linker=").unwrap(); if linker.ends_with("ld") { return true; } } false } } impl RustcArgs { // Split into its own function for unit testing fn from_vec(raw_args: Vec) -> Result { let mut parser = pico_args::Arguments::from_vec(raw_args); // --emit requires slightly more complex parsing let raw_emit_args: Vec = parser.values_from_str("--emit")?; let mut emit: Vec = Vec::new(); for raw_arg in raw_emit_args { for item in raw_arg.split(',') { emit.push(item.to_owned()); } } Ok(RustcArgs { crate_name: parser.opt_value_from_str("--crate-name")?, crate_types: parser.values_from_str("--crate-type")?, cfg: parser.values_from_str("--cfg")?, emit, out_dir: parser .opt_value_from_os_str::<&str, PathBuf, pico_args::Error>("--out-dir", |s| { Ok(PathBuf::from(s)) })?, target: parser.opt_value_from_str("--target")?, print: parser.values_from_str("--print")?, codegen: parser.values_from_str("-C")?, }) } } pub fn parse_args() -> Result { let raw_args: Vec = std::env::args_os().skip(2).collect(); RustcArgs::from_vec(raw_args) } pub fn should_embed_audit_data(args: &RustcArgs) -> bool { // Only inject audit data into crate types 'bin' and 'cdylib', // it doesn't make sense for static libs and weird other types. if !(args.crate_types.contains(&"bin".to_owned()) || args.crate_types.contains(&"cdylib".to_owned())) { return false; } // when --emit is specified explicitly, only inject audit data for --emit=link // because it doesn't make sense for all other types such as llvm-ir, asm, etc. if !args.emit.is_empty() && !args.emit.contains(&"link".to_owned()) { return false; } // --print disables compilation UNLESS --emit is also specified if !args.print.is_empty() && args.emit.is_empty() { return false; } true } #[cfg(test)] mod tests { use super::*; #[test] fn rustc_vv() { let raw_rustc_args = vec!["-vV"]; let raw_rustc_args: Vec = raw_rustc_args.into_iter().map(|s| s.into()).collect(); let args = RustcArgs::from_vec(raw_rustc_args).unwrap(); assert!(!should_embed_audit_data(&args)); } #[test] fn rustc_version_verbose() { let raw_rustc_args = vec!["--version", "--verbose"]; let raw_rustc_args: Vec = raw_rustc_args.into_iter().map(|s| s.into()).collect(); let args = RustcArgs::from_vec(raw_rustc_args).unwrap(); assert!(!should_embed_audit_data(&args)); } #[test] fn cargo_c_compatibility() { let raw_rustc_args = vec!["--crate-name", "rustls", "--edition=2021", "src/lib.rs", "--error-format=json", "--json=diagnostic-rendered-ansi,artifacts,future-incompat", "--crate-type", "staticlib", "--crate-type", "cdylib", "--emit=dep-info,link", "-C", "embed-bitcode=no", "-C", "debuginfo=2", "-C", "link-arg=-Wl,-soname,librustls.so.0.14.0", "-Cmetadata=rustls-ffi", "--cfg", "cargo_c", "--print", "native-static-libs", "--cfg", "feature=\"aws-lc-rs\"", "--cfg", "feature=\"capi\"", "--cfg", "feature=\"default\"", "--check-cfg", "cfg(docsrs)", "--check-cfg", "cfg(feature, values(\"aws-lc-rs\", \"capi\", \"cert_compression\", \"default\", \"no_log_capture\", \"read_buf\", \"ring\"))", "-C", "metadata=b6a43041f637feb8", "--out-dir", "/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps", "--target", "x86_64-unknown-linux-gnu", "-C", "linker=clang", "-C", "incremental=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/incremental", "-L", "dependency=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps", "-L", "dependency=/home/user/Code/rustls-ffi/target/debug/deps", "--extern", "libc=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/liblibc-4fc7c9f82dda33ee.rlib", "--extern", "log=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/liblog-6f7c8f4d1d5ec422.rlib", "--extern", "rustls=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/librustls-a93cda0ba0380929.rlib", "--extern", "pki_types=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/librustls_pki_types-27749859644f0979.rlib", "--extern", "rustls_platform_verifier=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/librustls_platform_verifier-bceca5cf09f3d7ba.rlib", "--extern", "webpki=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/libwebpki-bc4a16dd84e0b062.rlib", "-C", "link-arg=-fuse-ld=/home/user/mold-2.32.0-x86_64-linux/bin/mold", "-L", "native=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/build/aws-lc-sys-d52f8990d9ede41d/out"]; let raw_rustc_args: Vec = raw_rustc_args.into_iter().map(|s| s.into()).collect(); let args = RustcArgs::from_vec(raw_rustc_args).unwrap(); assert!(should_embed_audit_data(&args)); } #[test] fn embed_licensing_compatibility() { // https://github.com/rust-secure-code/cargo-auditable/issues/198 let raw_rustc_args = vec![ "-", "--crate-name ___", "--print=file-names", "--crate-type bin", "--crate-type rlib", "--crate-type dylib", "--crate-type cdylib", "--crate-type staticlib", "--crate-type proc-macro", "--print=sysroot", "--print=split-debuginfo", "--print=crate-name", "--print=cfg", ]; let raw_rustc_args: Vec = raw_rustc_args.into_iter().map(|s| s.into()).collect(); let args = RustcArgs::from_vec(raw_rustc_args).unwrap(); assert!(!should_embed_audit_data(&args)); } #[test] fn multiple_emit_values() { let raw_rustc_args = vec!["--emit=dep-info,link", "--emit", "llvm-bc"]; let raw_rustc_args: Vec = raw_rustc_args.into_iter().map(|s| s.into()).collect(); let mut args = RustcArgs::from_vec(raw_rustc_args).unwrap(); let expected = vec!["dep-info", "link", "llvm-bc"]; let mut expected: Vec = expected.into_iter().map(|s| s.into()).collect(); args.emit.sort(); expected.sort(); assert_eq!(args.emit, expected) } #[test] fn detect_bare_linker() { let raw_rustc_args = vec!["-C", "linker=rust-lld"]; let raw_rustc_args: Vec = raw_rustc_args.into_iter().map(|s| s.into()).collect(); let args = RustcArgs::from_vec(raw_rustc_args).unwrap(); assert!(args.bare_linker()); } #[test] fn multiple_codegen_options() { let raw_rustc_args = vec!["-Clinker=clang", "-C", "link-arg=-fuse-ld=/usr/bin/mold"]; let raw_rustc_args: Vec = raw_rustc_args.into_iter().map(|s| s.into()).collect(); let mut args = RustcArgs::from_vec(raw_rustc_args).unwrap(); let expected = vec!["linker=clang", "link-arg=-fuse-ld=/usr/bin/mold"]; let mut expected: Vec = expected.into_iter().map(|s| s.into()).collect(); args.codegen.sort(); expected.sort(); assert_eq!(args.codegen, expected); assert!(!args.bare_linker()); } } cargo-auditable-0.7.2/src/rustc_wrapper.rs000064400000000000000000000137371046102023000167250ustar 00000000000000use std::{ env, ffi::{OsStr, OsString}, process::Command, }; use crate::{ binary_file, collect_audit_data, platform_detection::{is_apple, is_msvc, is_wasm}, rustc_arguments::{self, should_embed_audit_data}, target_info, }; use std::io::BufRead; pub fn main(rustc_path: &OsStr) { let mut command = match rustc_command_with_audit_data(rustc_path) { Some(cmd) => cmd, None => rustc_command(rustc_path), // could not construct command that injects audit data, skip it }; // Invoke rustc let results = command.status().unwrap_or_else(|err| { let mut command_with_args: Vec<&OsStr> = vec![command.get_program()]; command_with_args.extend(command.get_args()); eprintln!( "Failed to invoke rustc! Make sure it's in your $PATH\n\ The error was: {err}\n\ The attempted call was: {command_with_args:?}", ); std::process::exit(1); }); let code = results .code() .expect("rustc was terminated by a deadly signal"); std::process::exit(code); } /// Creates a rustc command line and populates arguments from arguments passed to us. fn rustc_command(rustc_path: &OsStr) -> Command { let mut command = Command::new(rustc_path); // Pass along all the arguments that Cargo meant to pass to rustc // We skip the path to our binary as well as the first argument passed by Cargo, // which is the path to rustc to use (or just "rustc") command.args(env::args_os().skip(2)); command } /// Returns the default target triple for the rustc we're running fn rustc_host_target_triple(rustc_path: &OsStr) -> String { Command::new(rustc_path) .arg("-vV") .output() .expect("Failed to invoke rustc! Is it in your $PATH?") .stdout .lines() .map(|l| l.unwrap()) .find(|l| l.starts_with("host: ")) .map(|l| l[6..].to_string()) .expect("Failed to parse rustc output to determine the current platform. Please report this bug!") } fn rustc_command_with_audit_data(rustc_path: &OsStr) -> Option { let mut command = rustc_command(rustc_path); // Only inject audit data if CARGO_PRIMARY_PACKAGE is set. // This allows linking audit data only in toplevel binaries, not intermediate artifacts. // // Binaries and C dynamic libraries are not built as non-primary packages, // so this should not cause issues with Cargo caches. #[allow(clippy::question_mark)] if env::var_os("CARGO_PRIMARY_PACKAGE").is_none() { return None; } let args = rustc_arguments::parse_args().unwrap(); // descriptive enough message if !should_embed_audit_data(&args) { return None; } // Get the audit data to embed let target_triple = args .target .clone() .unwrap_or_else(|| rustc_host_target_triple(rustc_path)); let contents: Vec = collect_audit_data::compressed_dependency_list(&args, &target_triple); // write the audit info to an object file let target_info = target_info::rustc_target_info(rustc_path, &target_triple); let binfile = binary_file::create_binary_file( &target_info, &target_triple, &contents, "AUDITABLE_VERSION_INFO", ); if let Some(file) = binfile { // Place the audit data in the output dir. // We can place it anywhere really, the only concern is clutter and name collisions, // and the target dir is locked so we're probably good let crate_name = match args.crate_name.as_deref() { Some(name) => name, None => { eprintln!( "WARNING: cargo-auditable: rustc command is missing --crate-name\n\ Please double-check that the audit data was injected into the binary.\n\ If it wasn't, please report a bug." ); return None; } }; let out_dir = match args.out_dir.as_deref() { Some(name) => name, None => { eprintln!( "WARNING: cargo-auditable: rustc command is missing --out-dir\n\ Please double-check that the audit data was injected into the binary.\n\ If it wasn't, please report a bug." ); return None; } }; let filename = format!("{crate_name}_audit_data.o"); let path = out_dir.join(filename); std::fs::write(&path, file).expect("Unable to write output file"); // Modify the rustc command to link the object file with audit data let mut linker_command = OsString::from("-Clink-arg="); linker_command.push(&path); command.arg(linker_command); // Prevent the symbol from being removed as unused by the linker if is_apple(&target_info) { if args.bare_linker() { command.arg("-Clink-arg=-u,_AUDITABLE_VERSION_INFO"); } else { command.arg("-Clink-arg=-Wl,-u,_AUDITABLE_VERSION_INFO"); } } else if is_msvc(&target_info) { command.arg("-Clink-arg=/INCLUDE:AUDITABLE_VERSION_INFO"); } else if is_wasm(&target_info) { // We don't emit the symbol name in WASM, so nothing to do } else { // Unrecognized platform, assume it to be unix-like #[allow(clippy::collapsible_else_if)] if args.bare_linker() { command.arg("-Clink-arg=--undefined=AUDITABLE_VERSION_INFO"); } else { command.arg("-Clink-arg=-Wl,--undefined=AUDITABLE_VERSION_INFO"); } } Some(command) } else { // create_binary_file() returned None, indicating an unsupported architecture eprintln!( "WARNING: cargo-auditable: target '{target_triple}' is not supported!\n\ The build will continue, but no audit data will be injected into the binary." ); None } } cargo-auditable-0.7.2/src/sbom_precursor.rs000064400000000000000000000177161046102023000170720ustar 00000000000000use std::collections::HashMap; use auditable_serde::{Package, Source, VersionInfo}; use cargo_metadata::{ semver::{self, Version}, DependencyKind, }; use serde::{Deserialize, Serialize}; /// Cargo SBOM precursor format. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SbomPrecursor { /// Schema version pub version: u32, /// Index into the crates array for the root crate pub root: usize, /// Array of all crates pub crates: Vec, /// Information about rustc used to perform the compilation pub rustc: RustcInfo, } impl From for VersionInfo { fn from(sbom: SbomPrecursor) -> Self { // cargo sbom data format has more nodes than the auditable info format - if a crate is both a build // and runtime dependency it will appear twice in the `crates` array. // The `VersionInfo` format lists each package only once, with a single `kind` field // (Runtime having precedence over other kinds). // Firstly, we deduplicate the (name, version) pairs and create a mapping from the // original indices in the cargo sbom array to the new index in the auditable info package array. let (_, mut packages, indices) = sbom.crates.iter().enumerate().fold( (HashMap::new(), Vec::new(), Vec::new()), |(mut id_to_index_map, mut packages, mut indices), (index, crate_)| { match id_to_index_map.entry(crate_.id.clone()) { std::collections::hash_map::Entry::Occupied(entry) => { // Just store the new index in the indices array indices.push(*entry.get()); } std::collections::hash_map::Entry::Vacant(entry) => { let (name, version, source) = parse_fully_qualified_package_id(&crate_.id); // If the entry does not exist, we create it packages.push(Package { name, version, source, // Assume build, if we determine this is a runtime dependency we'll update later kind: auditable_serde::DependencyKind::Build, // We will fill this in later dependencies: Vec::new(), root: index == sbom.root, }); entry.insert(packages.len() - 1); indices.push(packages.len() - 1); } } (id_to_index_map, packages, indices) }, ); // Traverse the graph as given by the sbom to fill in the dependencies with the new indices. // // Keep track of whether the dependency is a runtime dependency. // If we ever encounter a non-runtime dependency, all deps in the remaining subtree // are not runtime dependencies, i.e a runtime dep of a build dep is not recognized as a runtime dep. let mut stack = Vec::new(); stack.push((sbom.root, true)); while let Some((old_index, is_runtime)) = stack.pop() { let crate_ = &sbom.crates[old_index]; for dep in &crate_.dependencies { stack.push((dep.index, dep.kind == DependencyKind::Normal && is_runtime)); } let package = &mut packages[indices[old_index]]; if is_runtime { package.kind = auditable_serde::DependencyKind::Runtime }; for dep in &crate_.dependencies { let new_dep_index = indices[dep.index]; if package.dependencies.contains(&new_dep_index) { continue; // Already added this dependency } else if new_dep_index == indices[old_index] { // If the dependency is the same as the package itself, skip it continue; } else { package.dependencies.push(new_dep_index); } } } VersionInfo { packages, format: 8, } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Crate { /// Package ID specification pub id: String, /// List of target kinds pub kind: Vec, /// Enabled feature flags pub features: Vec, /// Dependencies for this crate pub dependencies: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Dependency { /// Index into the crates array pub index: usize, /// Dependency kind: "normal", "build", or "dev" pub kind: DependencyKind, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RustcInfo { /// Compiler version pub version: String, /// Compiler wrapper pub wrapper: Option, /// Compiler workspace wrapper pub workspace_wrapper: Option, /// Commit hash for rustc pub commit_hash: String, /// Host target triple pub host: String, /// Verbose version string: `rustc -vV` pub verbose_version: String, } const CRATES_IO_INDEX: &str = "https://github.com/rust-lang/crates.io-index"; /// Parses a fully qualified package ID spec string into a tuple of (name, version, source). /// The package ID spec format is defined at https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#package-id-specifications-1 /// /// The fully qualified form of a package ID spec is mentioned in the Cargo documentation, /// figuring it out is left as an exercise to the reader. /// /// Adapting the grammar in the cargo doc, the format appears to be : /// ```norust /// fully_qualified_spec := kind "+" proto "://" hostname-and-path [ "?" query] "#" [ name "@" ] semver /// query := ( "branch" | "tag" | "rev" ) "=" ref /// semver := digits "." digits "." digits [ "-" prerelease ] [ "+" build ] /// kind := "registry" | "git" | "path" /// proto := "http" | "git" | "file" | ... /// ``` /// where: /// - the name is always present except when the kind is `path` and the last segment of the path doesn't match the name /// - the query string is only present for git dependencies (which we can ignore since we don't record git information) fn parse_fully_qualified_package_id(id: &str) -> (String, Version, Source) { let (kind, rest) = id.split_once('+').expect("Package ID to have a kind"); let (url, rest) = rest .split_once('#') .expect("Package ID to have version information"); let source = match (kind, url) { ("registry", CRATES_IO_INDEX) => Source::CratesIo, ("registry", _) => Source::Registry, ("git", _) => Source::Git, ("path", _) => Source::Local, _ => Source::Other(kind.to_string()), }; if source == Source::Local { // For local packages, the name might be in the suffix after '#' if it has // a diferent name than the last segment of the path. if let Some((name, version)) = rest.split_once('@') { ( name.to_string(), semver::Version::parse(version).expect("Version to be valid SemVer"), source, ) } else { // If no name is specified, use the last segment of the path as the name let name = url .split('/') .next_back() .unwrap() .split('\\') .next_back() .unwrap(); ( name.to_string(), semver::Version::parse(rest).expect("Version to be valid SemVer"), source, ) } } else { // For other sources, the name and version are after the '#', separated by '@' let (name, version) = rest .split_once('@') .expect("Package ID to have a name and version"); ( name.to_string(), semver::Version::parse(version).expect("Version to be valid SemVer"), source, ) } } cargo-auditable-0.7.2/src/target_info.rs000064400000000000000000000045131046102023000163160ustar 00000000000000use std::{ffi::OsStr, io::BufRead}; pub type RustcTargetInfo = std::collections::HashMap; pub fn rustc_target_info(rustc_path: &OsStr, target_triple: &str) -> RustcTargetInfo { // this is hand-rolled because the relevant piece of Cargo is hideously complex for some reason parse_rustc_target_info(&std::process::Command::new(rustc_path) .arg("--print=cfg") .arg(format!("--target={target_triple}")) //not being parsed by the shell, so not a vulnerability .output() .unwrap_or_else(|_| panic!("Failed to invoke rustc; make sure it's in $PATH and that '{target_triple}' is a valid target triple")) .stdout) } pub(crate) fn parse_rustc_target_info(rustc_output: &[u8]) -> RustcTargetInfo { // Decoupled from `rustc_target_info` to allow unit testing // `pub(crate)` so that unit tests in other modules could use it rustc_output .lines() .filter_map(|line| { let line = line.unwrap(); // rustc outputs some free-standing values as well as key-value pairs // we're only interested in the pairs, which are separated by '=' and the value is quoted if line.contains('=') { let key = line.split('=').next().unwrap(); let mut value: String = line.split('=').skip(1).collect(); // strip first and last chars of the quoted value. Verify that they're quotes assert!(value.pop().unwrap() == '"'); assert!(value.remove(0) == '"'); Some((key.to_owned(), value)) } else { None } }) .collect() } #[cfg(test)] mod tests { use super::*; #[test] fn test_rustc_parser_linux() { let rustc_output = br#"debug_assertions target_arch="x86_64" target_endian="little" target_env="gnu" target_family="unix" target_feature="fxsr" target_feature="sse" target_feature="sse2" target_os="linux" target_pointer_width="64" target_vendor="unknown" unix "#; let result = parse_rustc_target_info(rustc_output); assert_eq!(result.get("target_arch").unwrap(), "x86_64"); assert_eq!(result.get("target_endian").unwrap(), "little"); assert_eq!(result.get("target_pointer_width").unwrap(), "64"); assert_eq!(result.get("target_vendor").unwrap(), "unknown"); } } cargo-auditable-0.7.2/tests/.gitignore000064400000000000000000000000131046102023000160010ustar 00000000000000Cargo.lock cargo-auditable-0.7.2/tests/it.rs000064400000000000000000000545061046102023000150130ustar 00000000000000//! Integration Tests for cargo auditable use std::{ collections::HashMap, ffi::OsStr, io::Write, path::PathBuf, process::{Command, Output, Stdio}, }; use auditable_serde::{DependencyKind, VersionInfo}; use cargo_metadata::{ camino::{Utf8Path, Utf8PathBuf}, Artifact, TargetKind, }; // Path to cargo-auditable binary under test const EXE: &str = env!("CARGO_BIN_EXE_cargo-auditable"); // Path to Cargo itself const CARGO: &str = env!("CARGO"); /// Run cargo auditable with --manifest-path and extra args, /// returning of map of workspace member names -> produced binaries (bin and cdylib) /// Reads the AUDITABLE_TEST_TARGET environment variable to determine the target to compile for /// Uses `CARGO_BUILD_SBOM` environment variable to enable SBOM generation if `sbom` is true fn run_cargo_auditable

( cargo_toml_path: P, args: &[&str], env: &[(&str, &OsStr)], sbom: bool, ) -> HashMap> where P: AsRef, { // run `cargo clean` before performing the build, // otherwise already built binaries will be used // and we won't actually test the *current* version of `cargo auditable` let status = Command::new(CARGO) .arg("clean") .arg("--manifest-path") .arg(&cargo_toml_path) .status() .unwrap(); assert!(status.success(), "Failed to invoke `cargo clean`!"); let mut command = Command::new(EXE); command .arg("auditable") .arg("build") .arg("--release") .arg("--manifest-path") .arg(&cargo_toml_path) // We'll parse these to get binary paths .arg("--message-format=json"); if sbom { command.arg("-Z").arg("sbom"); command.env("CARGO_BUILD_SBOM", "true"); // Enable SBOM tests to run on stable rust command.env("RUSTC_BOOTSTRAP", "1"); } command.args(args); if let Ok(target) = std::env::var("AUDITABLE_TEST_TARGET") { if args.iter().all(|arg| !arg.starts_with("--target")) { command.arg(format!("--target={target}")); } } for (name, value) in env { command.env(name, value); } let output = command // We don't need to read stderr, so inherit for easier test debugging .stderr(Stdio::inherit()) .stdout(Stdio::piped()) .output() .unwrap(); ensure_build_succeeded(&output); let mut bins = HashMap::new(); std::str::from_utf8(&output.stdout) .unwrap() .lines() .flat_map(|line: &str| { let mut binaries = vec![]; if let Ok(artifact) = serde_json::from_str::(line) { // workspace member name is first word in package ID let member = artifact .package_id .to_string() .split(' ') .next() .unwrap() .to_string(); // bin targets are straightforward - use executable if let Some(executable) = artifact.executable { binaries.push((member, executable)); // cdylibs less so } else if artifact .target .kind .iter() .any(|kind| *kind == TargetKind::CDyLib) { // Detect files with .so (Linux), .dylib (Mac) and .dll (Windows) extensions artifact .filenames .into_iter() .filter(|f| { f.extension() == Some("dylib") || f.extension() == Some("so") || f.extension() == Some("dll") }) .for_each(|f| { binaries.push((member.clone(), f)); }); } } binaries }) .for_each(|(package, binary)| { bins.entry(pkgid_to_bin_name(&package)) .or_insert(Vec::new()) .push(binary); }); bins } fn pkgid_to_bin_name(pkgid: &str) -> String { // the input is string in the format such as // "path+file:///home/shnatsel/Code/cargo-auditable/cargo-auditable/tests/fixtures/lib_and_bin_crate#0.1.0" // (for full docs see `cargo pkgid`) // and we need just the crate name, e.g. "lib_and_bin_crate". // Weirdly it doesn't use OS path separator, it always uses '/' pkgid .rsplit_once('/') .unwrap() .1 .split_once('#') .unwrap() .0 .to_owned() } fn ensure_build_succeeded(output: &Output) { if !output.status.success() { let stderr = std::io::stderr(); let mut handle = stderr.lock(); handle.write_all(&output.stdout).unwrap(); handle.write_all(&output.stderr).unwrap(); handle.flush().unwrap(); panic!("Build with `cargo auditable` failed"); } } fn get_dependency_info(binary: &Utf8Path) -> VersionInfo { auditable_info::audit_info_from_file(binary.as_std_path(), Default::default()).unwrap() } #[test] fn test_cargo_auditable_workspaces() { test_cargo_auditable_workspaces_inner(false); test_cargo_auditable_workspaces_inner(true); } fn test_cargo_auditable_workspaces_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/workspace/Cargo.toml"); // Run in workspace root with default features let bins = run_cargo_auditable(&workspace_cargo_toml, &[], &[], sbom); eprintln!("Test fixture binary map: {bins:?}"); // No binaries for library_crate assert!(!bins.contains_key("library_crate")); // binary_and_cdylib_crate let binary_and_cdylib_crate_bins = bins.get("binary_and_cdylib_crate").unwrap(); match std::env::var("AUDITABLE_TEST_TARGET") { // musl targets do not produce cdylibs by default: https://github.com/rust-lang/cargo/issues/8607 // So when targeting musl, we only check that the binary has been built, not the cdylib. Ok(target) if target.contains("musl") => assert!(!binary_and_cdylib_crate_bins.is_empty()), // everything else should build both the binary and cdylib _ => assert_eq!(binary_and_cdylib_crate_bins.len(), 2), } for binary in binary_and_cdylib_crate_bins { let dep_info = get_dependency_info(binary); eprintln!("{binary} dependency info: {dep_info:?}"); // binary_and_cdylib_crate should have two dependencies, library_crate and itself assert!(dep_info.packages.len() == 2); assert!(dep_info.packages.iter().any(|p| p.name == "library_crate")); assert!(dep_info .packages .iter() .any(|p| p.name == "binary_and_cdylib_crate")); } // crate_with_features should create a binary with two dependencies, library_crate and itself let crate_with_features_bin = &bins.get("crate_with_features").unwrap()[0]; let dep_info = get_dependency_info(crate_with_features_bin); eprintln!("{crate_with_features_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 2); assert!(dep_info.packages.iter().any(|p| p.name == "library_crate")); assert!(dep_info .packages .iter() .any(|p| p.name == "crate_with_features")); // Run enabling binary_and_cdylib_crate feature let bins = run_cargo_auditable( &workspace_cargo_toml, &["--features", "binary_and_cdylib_crate"], &[], sbom, ); // crate_with_features should now have three dependencies, library_crate binary_and_cdylib_crate and crate_with_features, let crate_with_features_bin = &bins.get("crate_with_features").unwrap()[0]; let dep_info = get_dependency_info(crate_with_features_bin); eprintln!("{crate_with_features_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 3); assert!(dep_info.packages.iter().any(|p| p.name == "library_crate")); assert!(dep_info .packages .iter() .any(|p| p.name == "crate_with_features")); assert!(dep_info .packages .iter() .any(|p| p.name == "binary_and_cdylib_crate")); // Run without default features let bins = run_cargo_auditable(&workspace_cargo_toml, &["--no-default-features"], &[], sbom); // crate_with_features should now only depend on itself let crate_with_features_bin = &bins.get("crate_with_features").unwrap()[0]; let dep_info = get_dependency_info(crate_with_features_bin); eprintln!("{crate_with_features_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 1); assert!(dep_info .packages .iter() .any(|p| p.name == "crate_with_features")); } /// This exercises a small real-world project #[test] fn test_self_hosting() { test_self_hosting_inner(false); test_self_hosting_inner(true); } fn test_self_hosting_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../rust-audit-info/Cargo.toml"); // Run in workspace root with default features let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Self-hosting binary map: {bins:?}"); // verify that the dependency info is present at all let bin = &bins.get("rust-audit-info").unwrap()[0]; let dep_info = get_dependency_info(bin); eprintln!("{bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() > 1); assert!(dep_info .packages .iter() .any(|p| p.name == "rust-audit-info")); } #[test] fn test_lto() { test_lto_inner(false); test_lto_inner(true); } fn test_lto_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/lto_binary_crate/Cargo.toml"); // Run in workspace root with default features let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("LTO binary map: {bins:?}"); // lto_binary_crate should only depend on itself let lto_binary_crate_bin = &bins.get("lto_binary_crate").unwrap()[0]; let dep_info = get_dependency_info(lto_binary_crate_bin); eprintln!("{lto_binary_crate_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 1); assert!(dep_info .packages .iter() .any(|p| p.name == "lto_binary_crate")); } #[test] fn test_lto_stripped() { test_lto_stripped_inner(false); test_lto_stripped_inner(true); } fn test_lto_stripped_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/lto_stripped_binary/Cargo.toml"); // Run in workspace root with default features let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Stripped binary map: {bins:?}"); // lto_stripped_binary should only depend on itself let lto_stripped_binary_bin = &bins.get("lto_stripped_binary").unwrap()[0]; let dep_info = get_dependency_info(lto_stripped_binary_bin); eprintln!("{lto_stripped_binary_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 1); assert!(dep_info .packages .iter() .any(|p| p.name == "lto_stripped_binary")); } #[test] fn test_bin_and_lib_in_one_crate() { test_bin_and_lib_in_one_crate_inner(false); test_bin_and_lib_in_one_crate_inner(true); } fn test_bin_and_lib_in_one_crate_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/lib_and_bin_crate/Cargo.toml"); let bins = run_cargo_auditable(workspace_cargo_toml, &["--bin=some_binary"], &[], sbom); eprintln!("Test fixture binary map: {bins:?}"); // lib_and_bin_crate should only depend on itself let lib_and_bin_crate_bin = &bins.get("lib_and_bin_crate").unwrap()[0]; let dep_info = get_dependency_info(lib_and_bin_crate_bin); eprintln!("{lib_and_bin_crate_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 1); assert!(dep_info .packages .iter() .any(|p| p.name == "lib_and_bin_crate")); } #[test] fn test_build_script() { test_build_script_inner(false); test_build_script_inner(true); } fn test_build_script_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/crate_with_build_script/Cargo.toml"); let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Test fixture binary map: {bins:?}"); // crate_with_build_script should only depend on itself let crate_with_build_script_bin = &bins.get("crate_with_build_script").unwrap()[0]; let dep_info = get_dependency_info(crate_with_build_script_bin); eprintln!("{crate_with_build_script_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 1); assert!(dep_info .packages .iter() .any(|p| p.name == "crate_with_build_script")); } #[test] fn test_platform_specific_deps() { test_platform_specific_deps_inner(false); test_platform_specific_deps_inner(true); } fn test_platform_specific_deps_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/platform_specific_deps/Cargo.toml"); // Run in workspace root with default features let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Test fixture binary map: {bins:?}"); let test_target = std::env::var("AUDITABLE_TEST_TARGET"); if test_target.is_err() || !test_target.unwrap().starts_with("m68k") { // 'with_platform_dep' should only depend on 'should_not_be_included' on m68k processors // and we're not building for those, so it should be omitted let bin = &bins.get("with_platform_dep").unwrap()[0]; let dep_info = get_dependency_info(bin); eprintln!("{bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 1); assert!(!dep_info .packages .iter() .any(|p| p.name == "should_not_be_included")); } } #[test] fn test_build_then_runtime_dep() { test_build_then_runtime_dep_inner(false); test_build_then_runtime_dep_inner(true); } fn test_build_then_runtime_dep_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/build_then_runtime_dep/Cargo.toml"); // Run in workspace root with default features let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Test fixture binary map: {bins:?}"); // check that the build types are propagated correctly let toplevel_crate_bin = &bins.get("top_level_crate").unwrap()[0]; let dep_info = get_dependency_info(toplevel_crate_bin); eprintln!("{toplevel_crate_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 3); assert!(dep_info .packages .iter() .any(|p| p.name == "build_dep" && p.kind == DependencyKind::Build)); assert!(dep_info .packages .iter() .any(|p| p.name == "runtime_dep_of_build_dep" && p.kind == DependencyKind::Build)); } #[test] fn test_runtime_then_build_dep() { test_runtime_then_build_dep_inner(false); test_runtime_then_build_dep_inner(true); } fn test_runtime_then_build_dep_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/runtime_then_build_dep/Cargo.toml"); // Run in workspace root with default features let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Test fixture binary map: {bins:?}"); // check that the build types are propagated correctly let toplevel_crate_bin = &bins.get("top_level_crate").unwrap()[0]; let dep_info = get_dependency_info(toplevel_crate_bin); eprintln!("{toplevel_crate_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 3); assert!(dep_info .packages .iter() .any(|p| p.name == "runtime_dep" && p.kind == DependencyKind::Runtime)); assert!(dep_info .packages .iter() .any(|p| p.name == "build_dep_of_runtime_dep" && p.kind == DependencyKind::Build)); } #[test] fn test_custom_rustc_path() { test_custom_rustc_path_inner(false); test_custom_rustc_path_inner(true); } fn test_custom_rustc_path_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/custom_rustc_path/Cargo.toml"); // locate rustc let rustc_path = which::which("rustc").unwrap(); // Run in workspace root with a custom path to rustc let bins = run_cargo_auditable( workspace_cargo_toml, &[], &[("RUSTC", rustc_path.as_ref())], sbom, ); eprintln!("Test fixture binary map: {bins:?}"); // check that the build types are propagated correctly let toplevel_crate_bin = &bins.get("top_level_crate").unwrap()[0]; let dep_info = get_dependency_info(toplevel_crate_bin); eprintln!("{toplevel_crate_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 3); assert!(dep_info .packages .iter() .any(|p| p.name == "runtime_dep" && p.kind == DependencyKind::Runtime)); assert!(dep_info .packages .iter() .any(|p| p.name == "build_dep_of_runtime_dep" && p.kind == DependencyKind::Build)); } #[test] fn test_workspace_member_version_info() { // Test that `/path/to/cargo-auditable rustc -vV works when compiling a workspace member // // Never happens with Cargo - it does call `rustc -vV`, // but either bypasses the wrapper or doesn't set CARGO_PRIMARY_PACKAGE=true. // However it does happen with `sccache`: // https://github.com/rust-secure-code/cargo-auditable/issues/87 let mut command = Command::new(EXE); command.env("CARGO_PRIMARY_PACKAGE", "true"); command.args(["rustc", "-vV"]); let status = command.status().unwrap(); assert!(status.success()); } #[test] fn test_wasm() { test_wasm_inner(false); test_wasm_inner(true); } fn test_wasm_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/wasm_crate/Cargo.toml"); // Run in workspace root with default features run_cargo_auditable( workspace_cargo_toml, &["--target=wasm32-unknown-unknown"], &[], sbom, ); // check that the build types are propagated correctly let dep_info = get_dependency_info( "tests/fixtures/wasm_crate/target/wasm32-unknown-unknown/release/wasm_crate.wasm".into(), ); eprintln!("wasm_crate.wasm dependency info: {dep_info:?}"); assert_eq!(dep_info.packages.len(), 16); } #[test] fn test_path_not_equal_name() { test_path_not_equal_name_inner(false); test_path_not_equal_name_inner(true); } fn test_path_not_equal_name_inner(sbom: bool) { // This tests a case where a path dependency's directory name is not equal to the crate name. let cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/path_not_equal_name/foo/Cargo.toml"); let bins = run_cargo_auditable(cargo_toml, &[], &[], sbom); let foo_bin = &bins.get("foo").unwrap()[0]; let dep_info = get_dependency_info(foo_bin); eprintln!("{foo_bin} dependency info: {dep_info:?}"); assert!(dep_info.packages.len() == 3); assert!(dep_info .packages .iter() .any(|p| p.name == "bar" && p.kind == DependencyKind::Runtime)); assert!(dep_info .packages .iter() .any(|p| p.name == "baz" && p.kind == DependencyKind::Runtime)); } #[test] fn test_proc_macro() { test_proc_macro_inner(false); test_proc_macro_inner(true); } fn test_proc_macro_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/proc-macro-dependency/Cargo.toml"); // Run in workspace root with default features let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Proc macro binary map: {bins:?}"); // proc-macro-dependency should depend on let binary = &bins.get("proc-macro-dependency").unwrap()[0]; let dep_info = get_dependency_info(binary); eprintln!("{binary} dependency info: {dep_info:?}"); // locate the serde_derive proc macro package let serde_derive_info = dep_info .packages .iter() .find(|p| p.name == "serde_derive") .expect("Could not find 'serde_derive' in the embedded dependency list!"); assert_eq!(serde_derive_info.kind, DependencyKind::Build); // locate the syn package which is norm a dependency of serde-derive let syn_info = dep_info .packages .iter() .find(|p| p.name == "syn") .expect("Could not find 'syn' in the embedded dependency list!"); assert_eq!(syn_info.kind, DependencyKind::Build); }