From 3634e9499e0ee3b1cf9c45fd6984c2492a8b2372 Mon Sep 17 00:00:00 2001 From: Tom Fay Date: Thu, 19 Jun 2025 07:50:51 +0100 Subject: [PATCH 1/3] cargo sbom integration If cargo sbom function is enabled, cargo-auditable will read the SBOM precursor file and use it to generate dependency information rather than trying to use the `cargo metadata` command. --- Cargo.lock | 400 +++++++++++++++++- README.md | 2 +- cargo-auditable/Cargo.toml | 1 + cargo-auditable/src/collect_audit_data.rs | 26 +- cargo-auditable/src/main.rs | 1 + cargo-auditable/src/sbom_precursor.rs | 132 ++++++ cargo-auditable/tests/.gitignore | 1 + .../top_level_crate/build.rs | 1 + .../custom_rustc_path/runtime_dep/build.rs | 1 + .../runtime_dep/build.rs | 1 + cargo-auditable/tests/it.rs | 93 +++- 11 files changed, 623 insertions(+), 36 deletions(-) create mode 100644 cargo-auditable/src/sbom_precursor.rs create mode 100644 cargo-auditable/tests/.gitignore create mode 100644 cargo-auditable/tests/fixtures/build_then_runtime_dep/top_level_crate/build.rs create mode 100644 cargo-auditable/tests/fixtures/custom_rustc_path/runtime_dep/build.rs create mode 100644 cargo-auditable/tests/fixtures/runtime_then_build_dep/runtime_dep/build.rs diff --git a/Cargo.lock b/Cargo.lock index fcc0578..1985780 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,7 @@ version = "0.6.7" dependencies = [ "auditable-info", "auditable-serde", + "cargo-util-schemas", "cargo_metadata", "miniz_oxide", "object", @@ -133,6 +134,22 @@ dependencies = [ "serde", ] +[[package]] +name = "cargo-util-schemas" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea8b01266e95c3cf839fe626e651fa36a9171033caa917a773d7a0ba1d5ce6be" +dependencies = [ + "semver", + "serde", + "serde-untagged", + "serde-value", + "thiserror 2.0.12", + "toml", + "unicode-xid", + "url", +] + [[package]] name = "cargo_metadata" version = "0.18.1" @@ -144,7 +161,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -173,14 +190,14 @@ dependencies = [ "fluent-uri", "indexmap", "once_cell", - "ordered-float", + "ordered-float 4.5.0", "purl", "regex", "serde", "serde_json", "spdx", "strum", - "thiserror", + "thiserror 1.0.69", "time", "uuid", "xml-rs", @@ -206,6 +223,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dyn-clone" version = "1.0.17" @@ -224,6 +252,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +dependencies = [ + "serde", + "typeid", +] + [[package]] name = "errno" version = "0.3.9" @@ -249,6 +287,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -290,6 +337,113 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.6.0" @@ -324,6 +478,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "memchr" version = "2.7.4" @@ -372,6 +532,15 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-float" version = "4.5.0" @@ -393,6 +562,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -416,7 +594,7 @@ checksum = "c14fe28c8495f7eaf77a6e6106966f95211c0a2404b9da50d248fc32af3a3f14" dependencies = [ "hex", "percent-encoding", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -517,9 +695,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" dependencies = [ "serde", ] @@ -533,6 +711,27 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299d9c19d7d466db4ab10addd5703e4c615dec2a5a16dbbafe191045e87ee66e" +dependencies = [ + "erased-serde", + "serde", + "typeid", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float 2.10.1", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.217" @@ -570,9 +769,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -592,6 +791,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "strum" version = "0.26.3" @@ -625,13 +830,33 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -645,6 +870,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.3.36" @@ -676,11 +912,21 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "toml" -version = "0.8.19" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -690,38 +936,74 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "topological-sort" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "uuid" version = "1.11.0" @@ -843,15 +1125,99 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.24" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" dependencies = [ "memchr", ] +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + [[package]] name = "xml-rs" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af310deaae937e48a26602b730250b4949e125f468f11e6990be3e5304ddd96f" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/README.md b/README.md index 4a52834..8a13cf9 100644 --- a/README.md +++ b/README.md @@ -104,4 +104,4 @@ 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) is implemented. +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) is implemented. That RFC has now been implemented and is available via an [unstable feature](https://doc.rust-lang.org/cargo/reference/unstable.html#sbom). cargo-auditable integrates with this: if you enable that feature and build with cargo auditable, e.g with `CARGO_BUILD_SBOM=true cargo auditable -Z sbom build` and a nightly Rust toolchain, then cargo auditable will use the SBOM precursor files generated by cargo. diff --git a/cargo-auditable/Cargo.toml b/cargo-auditable/Cargo.toml index 957dd72..398ab6e 100644 --- a/cargo-auditable/Cargo.toml +++ b/cargo-auditable/Cargo.toml @@ -21,6 +21,7 @@ cargo_metadata = "0.18" pico-args = { version = "0.5", features = ["eq-separator", "short-space-opt"] } serde = "1.0.147" wasm-gen = "0.1.4" +cargo-util-schemas = "0.8.1" [dev-dependencies] cargo_metadata = "0.18" diff --git a/cargo-auditable/src/collect_audit_data.rs b/cargo-auditable/src/collect_audit_data.rs index 5f20046..55c91a9 100644 --- a/cargo-auditable/src/collect_audit_data.rs +++ b/cargo-auditable/src/collect_audit_data.rs @@ -4,13 +4,33 @@ use std::str::from_utf8; use crate::{ auditable_from_metadata::encode_audit_data, cargo_arguments::CargoArgs, - rustc_arguments::RustcArgs, + 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 metadata = get_metadata(rustc_args, target_triple); - let version_info = encode_audit_data(&metadata).unwrap(); + 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) { + let sbom_paths = std::env::split_paths(&sbom_path.unwrap()).collect::>(); + // Cargo may create multiple SBOM precursor files. + // We can't control per-binary (or cdylib) dependency information, just grab the first non-rlib SBOM we find. + let sbom_path = sbom_paths + .iter() + .find(|p| !p.ends_with(".rlib.cargo-sbom.json")) + .unwrap_or_else(|| &sbom_paths[0]); + 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); diff --git a/cargo-auditable/src/main.rs b/cargo-auditable/src/main.rs index ad12a51..f4d88b7 100644 --- a/cargo-auditable/src/main.rs +++ b/cargo-auditable/src/main.rs @@ -9,6 +9,7 @@ mod object_file; mod platform_detection; mod rustc_arguments; mod rustc_wrapper; +mod sbom_precursor; mod target_info; use std::process::exit; diff --git a/cargo-auditable/src/sbom_precursor.rs b/cargo-auditable/src/sbom_precursor.rs new file mode 100644 index 0000000..5899eaa --- /dev/null +++ b/cargo-auditable/src/sbom_precursor.rs @@ -0,0 +1,132 @@ +use std::collections::HashMap; + +use auditable_serde::{Package, Source, VersionInfo}; +use cargo_metadata::DependencyKind; +use cargo_util_schemas::core::{PackageIdSpec, SourceKind}; +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 precence 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) => { + // If the entry does not exist, we create it + packages.push(Package { + name: crate_.id.name().to_string(), + version: crate_.id.version().expect("Package to have version"), + source: match crate_.id.kind() { + Some(SourceKind::Path) => Source::Local, + Some(SourceKind::Git(_)) => Source::Git, + Some(_) => Source::Registry, + None => Source::CratesIo, + }, + // 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 } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Crate { + /// Package ID specification + pub id: PackageIdSpec, + /// 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, +} diff --git a/cargo-auditable/tests/.gitignore b/cargo-auditable/tests/.gitignore new file mode 100644 index 0000000..03314f7 --- /dev/null +++ b/cargo-auditable/tests/.gitignore @@ -0,0 +1 @@ +Cargo.lock diff --git a/cargo-auditable/tests/fixtures/build_then_runtime_dep/top_level_crate/build.rs b/cargo-auditable/tests/fixtures/build_then_runtime_dep/top_level_crate/build.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/cargo-auditable/tests/fixtures/build_then_runtime_dep/top_level_crate/build.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/cargo-auditable/tests/fixtures/custom_rustc_path/runtime_dep/build.rs b/cargo-auditable/tests/fixtures/custom_rustc_path/runtime_dep/build.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/cargo-auditable/tests/fixtures/custom_rustc_path/runtime_dep/build.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/cargo-auditable/tests/fixtures/runtime_then_build_dep/runtime_dep/build.rs b/cargo-auditable/tests/fixtures/runtime_then_build_dep/runtime_dep/build.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/cargo-auditable/tests/fixtures/runtime_then_build_dep/runtime_dep/build.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/cargo-auditable/tests/it.rs b/cargo-auditable/tests/it.rs index ec94ac8..ff5bd36 100644 --- a/cargo-auditable/tests/it.rs +++ b/cargo-auditable/tests/it.rs @@ -22,10 +22,12 @@ 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, @@ -49,8 +51,16 @@ where .arg("--manifest-path") .arg(&cargo_toml_path) // We'll parse these to get binary paths - .arg("--message-format=json") - .args(args); + .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")) { @@ -153,11 +163,16 @@ fn get_dependency_info(binary: &Utf8Path) -> VersionInfo { #[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, &[], &[]); + let bins = run_cargo_auditable(&workspace_cargo_toml, &[], &[], sbom); eprintln!("Test fixture binary map: {bins:?}"); // No binaries for library_crate assert!(bins.get("library_crate").is_none()); @@ -199,6 +214,7 @@ fn test_cargo_auditable_workspaces() { &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]; @@ -216,7 +232,7 @@ fn test_cargo_auditable_workspaces() { .any(|p| p.name == "binary_and_cdylib_crate")); // Run without default features - let bins = run_cargo_auditable(&workspace_cargo_toml, &["--no-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); @@ -231,11 +247,16 @@ fn test_cargo_auditable_workspaces() { /// 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, &[], &[]); + let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Self-hosting binary map: {bins:?}"); // verify that the dependency info is present at all @@ -251,11 +272,15 @@ fn test_self_hosting() { #[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, &[], &[]); + let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("LTO binary map: {bins:?}"); // lto_binary_crate should only depend on itself @@ -271,11 +296,16 @@ fn test_lto() { #[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, &[], &[]); + let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Stripped binary map: {bins:?}"); // lto_stripped_binary should only depend on itself @@ -291,11 +321,15 @@ fn test_lto_stripped() { #[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"], &[]); + 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 @@ -309,15 +343,17 @@ fn test_bin_and_lib_in_one_crate() { .any(|p| p.name == "lib_and_bin_crate")); } -/// A previous approach had trouble with build scripts and proc macros. -/// Verify that those still work. #[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, &[], &[]); + let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Test fixture binary map: {bins:?}"); // crate_with_build_script should only depend on itself @@ -333,11 +369,15 @@ fn test_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, &[], &[]); + let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Test fixture binary map: {bins:?}"); let test_target = std::env::var("AUDITABLE_TEST_TARGET"); @@ -357,11 +397,15 @@ fn test_platform_specific_deps() { #[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, &[], &[]); + let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Test fixture binary map: {bins:?}"); // check that the build types are propagated correctly @@ -381,11 +425,15 @@ fn test_build_then_runtime_dep() { #[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, &[], &[]); + let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Test fixture binary map: {bins:?}"); // check that the build types are propagated correctly @@ -405,13 +453,22 @@ fn test_runtime_then_build_dep() { #[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())]); + 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 @@ -447,6 +504,11 @@ fn test_workspace_member_version_info() { #[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"); @@ -455,6 +517,7 @@ fn test_wasm() { workspace_cargo_toml, &["--target=wasm32-unknown-unknown"], &[], + sbom, ); // check that the build types are propagated correctly From 15cbd7b5af11062170c3e8d104d7188067fbd2fe Mon Sep 17 00:00:00 2001 From: Tom Fay Date: Sat, 21 Jun 2025 16:43:47 +0100 Subject: [PATCH 2/3] select first discovered SBOM file --- cargo-auditable/src/collect_audit_data.rs | 13 +++++-------- cargo-auditable/src/sbom_precursor.rs | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/cargo-auditable/src/collect_audit_data.rs b/cargo-auditable/src/collect_audit_data.rs index 55c91a9..ffbad46 100644 --- a/cargo-auditable/src/collect_audit_data.rs +++ b/cargo-auditable/src/collect_audit_data.rs @@ -13,14 +13,11 @@ pub fn compressed_dependency_list(rustc_args: &RustcArgs, target_triple: &str) - // 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) { - let sbom_paths = std::env::split_paths(&sbom_path.unwrap()).collect::>(); - // Cargo may create multiple SBOM precursor files. - // We can't control per-binary (or cdylib) dependency information, just grab the first non-rlib SBOM we find. - let sbom_path = sbom_paths - .iter() - .find(|p| !p.ends_with(".rlib.cargo-sbom.json")) - .unwrap_or_else(|| &sbom_paths[0]); - let sbom_data: Vec = std::fs::read(sbom_path) + // 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())); diff --git a/cargo-auditable/src/sbom_precursor.rs b/cargo-auditable/src/sbom_precursor.rs index 5899eaa..c7022b8 100644 --- a/cargo-auditable/src/sbom_precursor.rs +++ b/cargo-auditable/src/sbom_precursor.rs @@ -23,7 +23,7 @@ impl From for VersionInfo { // 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 precence over other kinds). + // (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. From 37cd4fbdad3e7f72e5ffcca84a3efea8e377d4bd Mon Sep 17 00:00:00 2001 From: Tom Fay Date: Tue, 24 Jun 2025 21:51:15 +0100 Subject: [PATCH 3/3] parse fully qualified package ID specs from SBOMs --- Cargo.lock | 376 +----------------- cargo-auditable/Cargo.toml | 1 - cargo-auditable/src/sbom_precursor.rs | 89 ++++- .../path_not_equal_name/bar/Cargo.toml | 8 + .../path_not_equal_name/bar/src/lib.rs | 14 + .../path_not_equal_name/foo/Cargo.toml | 10 + .../path_not_equal_name/foo/src/main.rs | 3 + .../path_not_equal_name/qux/Cargo.toml | 8 + .../path_not_equal_name/qux/src/lib.rs | 14 + cargo-auditable/tests/it.rs | 25 ++ 10 files changed, 165 insertions(+), 383 deletions(-) create mode 100644 cargo-auditable/tests/fixtures/path_not_equal_name/bar/Cargo.toml create mode 100644 cargo-auditable/tests/fixtures/path_not_equal_name/bar/src/lib.rs create mode 100644 cargo-auditable/tests/fixtures/path_not_equal_name/foo/Cargo.toml create mode 100644 cargo-auditable/tests/fixtures/path_not_equal_name/foo/src/main.rs create mode 100644 cargo-auditable/tests/fixtures/path_not_equal_name/qux/Cargo.toml create mode 100644 cargo-auditable/tests/fixtures/path_not_equal_name/qux/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 1985780..c9da4f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,7 +114,6 @@ version = "0.6.7" dependencies = [ "auditable-info", "auditable-serde", - "cargo-util-schemas", "cargo_metadata", "miniz_oxide", "object", @@ -134,22 +133,6 @@ dependencies = [ "serde", ] -[[package]] -name = "cargo-util-schemas" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8b01266e95c3cf839fe626e651fa36a9171033caa917a773d7a0ba1d5ce6be" -dependencies = [ - "semver", - "serde", - "serde-untagged", - "serde-value", - "thiserror 2.0.12", - "toml", - "unicode-xid", - "url", -] - [[package]] name = "cargo_metadata" version = "0.18.1" @@ -161,7 +144,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -190,14 +173,14 @@ dependencies = [ "fluent-uri", "indexmap", "once_cell", - "ordered-float 4.5.0", + "ordered-float", "purl", "regex", "serde", "serde_json", "spdx", "strum", - "thiserror 1.0.69", + "thiserror", "time", "uuid", "xml-rs", @@ -223,17 +206,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "dyn-clone" version = "1.0.17" @@ -252,16 +224,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "erased-serde" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" -dependencies = [ - "serde", - "typeid", -] - [[package]] name = "errno" version = "0.3.9" @@ -287,15 +249,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - [[package]] name = "getrandom" version = "0.2.15" @@ -337,113 +290,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "icu_collections" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" - -[[package]] -name = "icu_properties" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "potential_utf", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" - -[[package]] -name = "icu_provider" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" -dependencies = [ - "displaydoc", - "icu_locale_core", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - [[package]] name = "indexmap" version = "2.6.0" @@ -478,12 +324,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - [[package]] name = "memchr" version = "2.7.4" @@ -532,15 +372,6 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-float" version = "4.5.0" @@ -562,15 +393,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" -[[package]] -name = "potential_utf" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" -dependencies = [ - "zerovec", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -594,7 +416,7 @@ checksum = "c14fe28c8495f7eaf77a6e6106966f95211c0a2404b9da50d248fc32af3a3f14" dependencies = [ "hex", "percent-encoding", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -711,27 +533,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-untagged" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299d9c19d7d466db4ab10addd5703e4c615dec2a5a16dbbafe191045e87ee66e" -dependencies = [ - "erased-serde", - "serde", - "typeid", -] - -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float 2.10.1", - "serde", -] - [[package]] name = "serde_derive" version = "1.0.217" @@ -791,12 +592,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "strum" version = "0.26.3" @@ -830,33 +625,13 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" -dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl", ] [[package]] @@ -870,17 +645,6 @@ dependencies = [ "syn", ] -[[package]] -name = "thiserror-impl" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "time" version = "0.3.36" @@ -912,16 +676,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tinystr" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "toml" version = "0.8.23" @@ -953,57 +707,21 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_write", "winnow", ] -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - [[package]] name = "topological-sort" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "url" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "uuid" version = "1.11.0" @@ -1132,92 +850,8 @@ dependencies = [ "memchr", ] -[[package]] -name = "writeable" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" - [[package]] name = "xml-rs" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af310deaae937e48a26602b730250b4949e125f468f11e6990be3e5304ddd96f" - -[[package]] -name = "yoke" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/cargo-auditable/Cargo.toml b/cargo-auditable/Cargo.toml index 398ab6e..957dd72 100644 --- a/cargo-auditable/Cargo.toml +++ b/cargo-auditable/Cargo.toml @@ -21,7 +21,6 @@ cargo_metadata = "0.18" pico-args = { version = "0.5", features = ["eq-separator", "short-space-opt"] } serde = "1.0.147" wasm-gen = "0.1.4" -cargo-util-schemas = "0.8.1" [dev-dependencies] cargo_metadata = "0.18" diff --git a/cargo-auditable/src/sbom_precursor.rs b/cargo-auditable/src/sbom_precursor.rs index c7022b8..79a22a2 100644 --- a/cargo-auditable/src/sbom_precursor.rs +++ b/cargo-auditable/src/sbom_precursor.rs @@ -1,8 +1,10 @@ use std::collections::HashMap; use auditable_serde::{Package, Source, VersionInfo}; -use cargo_metadata::DependencyKind; -use cargo_util_schemas::core::{PackageIdSpec, SourceKind}; +use cargo_metadata::{ + semver::{self, Version}, + DependencyKind, +}; use serde::{Deserialize, Serialize}; /// Cargo SBOM precursor format. @@ -36,16 +38,12 @@ impl From for VersionInfo { 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: crate_.id.name().to_string(), - version: crate_.id.version().expect("Package to have version"), - source: match crate_.id.kind() { - Some(SourceKind::Path) => Source::Local, - Some(SourceKind::Git(_)) => Source::Git, - Some(_) => Source::Registry, - None => Source::CratesIo, - }, + 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 @@ -98,7 +96,7 @@ impl From for VersionInfo { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Crate { /// Package ID specification - pub id: PackageIdSpec, + pub id: String, /// List of target kinds pub kind: Vec, /// Enabled feature flags @@ -130,3 +128,72 @@ pub struct RustcInfo { /// 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, + ) + } +} diff --git a/cargo-auditable/tests/fixtures/path_not_equal_name/bar/Cargo.toml b/cargo-auditable/tests/fixtures/path_not_equal_name/bar/Cargo.toml new file mode 100644 index 0000000..c22d0be --- /dev/null +++ b/cargo-auditable/tests/fixtures/path_not_equal_name/bar/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "bar" +version = "0.1.0" +edition = "2021" + +[dependencies] + +[workspace] diff --git a/cargo-auditable/tests/fixtures/path_not_equal_name/bar/src/lib.rs b/cargo-auditable/tests/fixtures/path_not_equal_name/bar/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/cargo-auditable/tests/fixtures/path_not_equal_name/bar/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/cargo-auditable/tests/fixtures/path_not_equal_name/foo/Cargo.toml b/cargo-auditable/tests/fixtures/path_not_equal_name/foo/Cargo.toml new file mode 100644 index 0000000..35150f4 --- /dev/null +++ b/cargo-auditable/tests/fixtures/path_not_equal_name/foo/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "foo" +version = "0.1.0" +edition = "2021" + +[dependencies] +bar = { version = "0.1.0", path = "../bar" } +baz = { version = "0.1.0", path = "../qux" } + +[workspace] diff --git a/cargo-auditable/tests/fixtures/path_not_equal_name/foo/src/main.rs b/cargo-auditable/tests/fixtures/path_not_equal_name/foo/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/cargo-auditable/tests/fixtures/path_not_equal_name/foo/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/cargo-auditable/tests/fixtures/path_not_equal_name/qux/Cargo.toml b/cargo-auditable/tests/fixtures/path_not_equal_name/qux/Cargo.toml new file mode 100644 index 0000000..f379c9f --- /dev/null +++ b/cargo-auditable/tests/fixtures/path_not_equal_name/qux/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "baz" +version = "0.1.0" +edition = "2021" + +[dependencies] + +[workspace] diff --git a/cargo-auditable/tests/fixtures/path_not_equal_name/qux/src/lib.rs b/cargo-auditable/tests/fixtures/path_not_equal_name/qux/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/cargo-auditable/tests/fixtures/path_not_equal_name/qux/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/cargo-auditable/tests/it.rs b/cargo-auditable/tests/it.rs index ff5bd36..9e3377a 100644 --- a/cargo-auditable/tests/it.rs +++ b/cargo-auditable/tests/it.rs @@ -527,3 +527,28 @@ fn test_wasm_inner(sbom: bool) { 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)); +}