From fa19769bf6bee84ea3c5712bb716c55d37ad9e1f Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sat, 24 Feb 2024 00:00:09 +0000 Subject: [PATCH 01/16] Initial dummy package for auditable-cyclonedx --- Cargo.toml | 1 + auditable-cyclonedx/Cargo.toml | 8 ++++++++ auditable-cyclonedx/src/lib.rs | 14 ++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 auditable-cyclonedx/Cargo.toml create mode 100644 auditable-cyclonedx/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index de85ac4..fa70793 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,4 +5,5 @@ members = [ "auditable-extract", "auditable-serde", "cargo-auditable", + "auditable-cyclonedx", ] diff --git a/auditable-cyclonedx/Cargo.toml b/auditable-cyclonedx/Cargo.toml new file mode 100644 index 0000000..e7cc992 --- /dev/null +++ b/auditable-cyclonedx/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "auditable-cyclonedx" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/auditable-cyclonedx/src/lib.rs b/auditable-cyclonedx/src/lib.rs new file mode 100644 index 0000000..7d12d9a --- /dev/null +++ b/auditable-cyclonedx/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From 3a3846f7ae4cc766182aace103c95598542b2a37 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sat, 24 Feb 2024 00:08:53 +0000 Subject: [PATCH 02/16] Add the necessary dependencies to auditable-cyclonedx --- Cargo.lock | 284 ++++++++++++++++++++++++++++++--- auditable-cyclonedx/Cargo.toml | 7 + 2 files changed, 272 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d039aae..cbf18cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,23 @@ dependencies = [ "version_check", ] +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "auditable-cyclonedx" +version = "0.1.0" +dependencies = [ + "auditable-serde", + "cyclonedx-bom", +] + [[package]] name = "auditable-extract" version = "0.3.2" @@ -55,12 +72,24 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "binfarce" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18464ccbb85e5dede30d70cc7676dc9950a0fb7dbf595a43d765be9123c616a2" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "camino" version = "1.1.1" @@ -134,6 +163,36 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cyclonedx-bom" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed94ea2aaea25fdfec8a03ce34f92c4d2c00d741d0de681b923256448d3835b" +dependencies = [ + "base64", + "fluent-uri", + "once_cell", + "ordered-float", + "packageurl", + "regex", + "serde", + "serde_json", + "spdx", + "thiserror", + "time", + "uuid", + "xml-rs", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "dyn-clone" version = "1.0.9" @@ -146,6 +205,15 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags", +] + [[package]] name = "form_urlencoded" version = "1.1.0" @@ -155,6 +223,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -198,15 +277,15 @@ checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "libc" -version = "0.2.132" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "miniz_oxide" @@ -217,6 +296,21 @@ dependencies = [ "adler", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.30.3" @@ -231,9 +325,28 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.14.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "ordered-float" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "packageurl" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c53362339d1c48910f1b0c35e2ae96e2d32e442c7dc3ac5f622908ec87221f08" +dependencies = [ + "percent-encoding", + "thiserror", +] [[package]] name = "percent-encoding" @@ -247,24 +360,59 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.21" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + [[package]] name = "ryu" version = "1.0.11" @@ -292,7 +440,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 1.0.99", ] [[package]] @@ -306,22 +454,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.147" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.147" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.50", ] [[package]] @@ -332,14 +480,14 @@ checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.99", ] [[package]] name = "serde_json" -version = "1.0.85" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", @@ -355,6 +503,21 @@ dependencies = [ "serde", ] +[[package]] +name = "smallvec" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + +[[package]] +name = "spdx" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bde1398b09b9f93fc2fc9b9da86e362693e999d3a54a8ac47a99a5a73f638b" +dependencies = [ + "smallvec", +] + [[package]] name = "syn" version = "1.0.99" @@ -366,6 +529,68 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.50", +] + +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -453,12 +678,27 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +dependencies = [ + "getrandom", +] + [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "which" version = "4.3.0" @@ -478,3 +718,9 @@ checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" dependencies = [ "memchr", ] + +[[package]] +name = "xml-rs" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" diff --git a/auditable-cyclonedx/Cargo.toml b/auditable-cyclonedx/Cargo.toml index e7cc992..0cbd753 100644 --- a/auditable-cyclonedx/Cargo.toml +++ b/auditable-cyclonedx/Cargo.toml @@ -2,7 +2,14 @@ name = "auditable-cyclonedx" version = "0.1.0" edition = "2021" +authors = ["Sergey \"Shnatsel\" Davidoff "] +license = "MIT OR Apache-2.0" +repository = "https://github.com/rust-secure-code/cargo-auditable" +description = "Convert data encoded by `cargo auditable` to CycloneDX format" +categories = ["encoding"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +cyclonedx-bom = "0.5.0" +auditable-serde = {version = "0.6.1", path = "../auditable-serde"} From b34d6fc85acfce12787d6a593d207d80712eeebc Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sat, 24 Feb 2024 01:20:00 +0000 Subject: [PATCH 03/16] Initial implementation of auditable-to-cyclonedx conversion --- auditable-cyclonedx/src/lib.rs | 70 +++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 10 deletions(-) diff --git a/auditable-cyclonedx/src/lib.rs b/auditable-cyclonedx/src/lib.rs index 7d12d9a..9dac926 100644 --- a/auditable-cyclonedx/src/lib.rs +++ b/auditable-cyclonedx/src/lib.rs @@ -1,14 +1,64 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} +pub use auditable_serde; +use auditable_serde::Package; +pub use cyclonedx_bom; -#[cfg(test)] -mod tests { - use super::*; +use cyclonedx_bom::models::{component::Classification, component::Component, metadata::Metadata}; +use cyclonedx_bom::prelude::*; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); +/// Converts the metadata embedded by `cargo auditable` to a minimal CycloneDX document +/// that is heavily optimized to reduce the size +pub fn auditable_to_minimal_cdx(input: &auditable_serde::VersionInfo) -> Bom { + let mut bom = Bom::default(); + // The toplevel component goes into its own field, as per the spec: + // https://cyclonedx.org/docs/1.5/json/#metadata_component + let (root_idx, root_pkg) = root_package(input); + let root_component = pkg_to_component(root_pkg, root_idx); + let mut metadata = Metadata::default(); + metadata.component = Some(root_component); + bom.metadata = Some(metadata); + // Fill in the component list, excluding the toplevel component (already encoded) + let components: Vec = input + .packages + .iter() + .enumerate() + .filter(|(_idx, pkg)| !pkg.root) + .map(|(idx, pkg)| pkg_to_component(pkg, idx)) + .collect(); + let components = Components(components); + bom.components = Some(components); + // TODO: dependency tree + if cfg!(debug_assertions) { + assert_eq!(bom.validate(), ValidationResult::Passed); } + bom +} + +fn pkg_to_component(pkg: &auditable_serde::Package, idx: usize) -> Component { + let component_type = if pkg.root { + Classification::Application + } else { + Classification::Library + }; + // The only requirement for `bom_ref` according to the spec is that it's unique, + // so we just keep the unique numbering already used in the original + let bom_ref = idx.to_string(); + Component::new( + component_type, + &pkg.name, + &pkg.version.to_string(), + Some(bom_ref), + ) + // TODO: source + // TODO: dependency kind + // TODO: purl +} + +fn root_package(input: &auditable_serde::VersionInfo) -> (usize, &Package) { + // we can unwrap here because VersionInfo is already validated during deserialization + input + .packages + .iter() + .enumerate() + .find(|(_idx, pkg)| pkg.root) + .expect("VersionInfo contains no root package!") } From 7f02a5ac3116a6442f9a43300498b7effe7932a1 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sat, 24 Feb 2024 01:22:41 +0000 Subject: [PATCH 04/16] add #![forbid(unsafe_code)] --- auditable-cyclonedx/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/auditable-cyclonedx/src/lib.rs b/auditable-cyclonedx/src/lib.rs index 9dac926..d5cddf3 100644 --- a/auditable-cyclonedx/src/lib.rs +++ b/auditable-cyclonedx/src/lib.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + pub use auditable_serde; use auditable_serde::Package; pub use cyclonedx_bom; From 4f521aa7c1a9b88f5d29dc516798f5d27c03cdae Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sat, 24 Feb 2024 01:32:18 +0000 Subject: [PATCH 05/16] Initial auditable2cdx boilerplace --- Cargo.toml | 2 +- auditable2cdx/Cargo.toml | 8 ++++++++ auditable2cdx/src/main.rs | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 auditable2cdx/Cargo.toml create mode 100644 auditable2cdx/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index fa70793..dd10c59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,5 +5,5 @@ members = [ "auditable-extract", "auditable-serde", "cargo-auditable", - "auditable-cyclonedx", + "auditable-cyclonedx", "auditable2cdx", ] diff --git a/auditable2cdx/Cargo.toml b/auditable2cdx/Cargo.toml new file mode 100644 index 0000000..5ab2eeb --- /dev/null +++ b/auditable2cdx/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "auditable2cdx" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/auditable2cdx/src/main.rs b/auditable2cdx/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/auditable2cdx/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} From dbd4653c5400ac9c9f800753e85bd0c708f4f4f4 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sat, 24 Feb 2024 01:34:28 +0000 Subject: [PATCH 06/16] Fill in auditable2cdx dependencies --- auditable2cdx/Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/auditable2cdx/Cargo.toml b/auditable2cdx/Cargo.toml index 5ab2eeb..7c7e635 100644 --- a/auditable2cdx/Cargo.toml +++ b/auditable2cdx/Cargo.toml @@ -6,3 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +auditable-info = {version = "0.7.0", path = "../auditable-info"} +auditable-cyclonedx = {version = "0.1.0", path = "../auditable-cyclonedx"} From b569ddcc89e03463e77236958ec9a7f078149caa Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sat, 24 Feb 2024 03:05:14 +0000 Subject: [PATCH 07/16] Prototype impl of auditable2cdx --- Cargo.lock | 8 ++++++++ auditable2cdx/src/main.rs | 13 ++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index cbf18cd..3ce9dce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,6 +66,14 @@ dependencies = [ "topological-sort", ] +[[package]] +name = "auditable2cdx" +version = "0.1.0" +dependencies = [ + "auditable-cyclonedx", + "auditable-info", +] + [[package]] name = "autocfg" version = "1.1.0" diff --git a/auditable2cdx/src/main.rs b/auditable2cdx/src/main.rs index e7a11a9..7145c80 100644 --- a/auditable2cdx/src/main.rs +++ b/auditable2cdx/src/main.rs @@ -1,3 +1,14 @@ +use std::path::Path; + +use auditable_cyclonedx::auditable_to_minimal_cdx; +use auditable_info::audit_info_from_file; + fn main() { - println!("Hello, world!"); + let input_filename = std::env::args_os() + .nth(1) + .expect("No input file specified!"); + let info = audit_info_from_file(Path::new(&input_filename), Default::default()).unwrap(); + let cyclonedx = auditable_to_minimal_cdx(&info); + let mut stdout = std::io::stdout().lock(); + cyclonedx.output_as_json_v1_3(&mut stdout).unwrap(); } From c2136294ea0f622d17365c066ebebfc64c835c09 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sat, 24 Feb 2024 03:09:57 +0000 Subject: [PATCH 08/16] Clear the serial number in the minimal CycloneDX variant --- auditable-cyclonedx/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/auditable-cyclonedx/src/lib.rs b/auditable-cyclonedx/src/lib.rs index d5cddf3..1e135bb 100644 --- a/auditable-cyclonedx/src/lib.rs +++ b/auditable-cyclonedx/src/lib.rs @@ -11,6 +11,9 @@ use cyclonedx_bom::prelude::*; /// that is heavily optimized to reduce the size pub fn auditable_to_minimal_cdx(input: &auditable_serde::VersionInfo) -> Bom { let mut bom = Bom::default(); + // Clear the serial number which would mess with reproducible builds + // and also take up valuable space + bom.serial_number = None; // The toplevel component goes into its own field, as per the spec: // https://cyclonedx.org/docs/1.5/json/#metadata_component let (root_idx, root_pkg) = root_package(input); From 16099592e7c9ab7c3445488c5ab567809d03ce49 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sat, 24 Feb 2024 03:47:49 +0000 Subject: [PATCH 09/16] Also write the dependency tree --- auditable-cyclonedx/src/lib.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/auditable-cyclonedx/src/lib.rs b/auditable-cyclonedx/src/lib.rs index 1e135bb..4538c49 100644 --- a/auditable-cyclonedx/src/lib.rs +++ b/auditable-cyclonedx/src/lib.rs @@ -4,16 +4,23 @@ pub use auditable_serde; use auditable_serde::Package; pub use cyclonedx_bom; -use cyclonedx_bom::models::{component::Classification, component::Component, metadata::Metadata}; +use cyclonedx_bom::models::{ + component::Classification, + component::Component, + dependency::{Dependencies, Dependency}, + metadata::Metadata, +}; use cyclonedx_bom::prelude::*; /// Converts the metadata embedded by `cargo auditable` to a minimal CycloneDX document /// that is heavily optimized to reduce the size pub fn auditable_to_minimal_cdx(input: &auditable_serde::VersionInfo) -> Bom { let mut bom = Bom::default(); + // Clear the serial number which would mess with reproducible builds // and also take up valuable space bom.serial_number = None; + // The toplevel component goes into its own field, as per the spec: // https://cyclonedx.org/docs/1.5/json/#metadata_component let (root_idx, root_pkg) = root_package(input); @@ -21,6 +28,7 @@ pub fn auditable_to_minimal_cdx(input: &auditable_serde::VersionInfo) -> Bom { let mut metadata = Metadata::default(); metadata.component = Some(root_component); bom.metadata = Some(metadata); + // Fill in the component list, excluding the toplevel component (already encoded) let components: Vec = input .packages @@ -31,7 +39,21 @@ pub fn auditable_to_minimal_cdx(input: &auditable_serde::VersionInfo) -> Bom { .collect(); let components = Components(components); bom.components = Some(components); - // TODO: dependency tree + + // Populate the dependency tree. Actually really easy, it's the same format as ours! + let dependencies: Vec = input + .packages + .iter() + .enumerate() + .map(|(idx, pkg)| Dependency { + dependency_ref: idx.to_string(), + dependencies: pkg.dependencies.iter().map(|idx| idx.to_string()).collect(), + }) + .collect(); + let dependencies = Dependencies(dependencies); + bom.dependencies = Some(dependencies); + + // Validate the generated SBOM if running in debug mode (or release with debug assertions) if cfg!(debug_assertions) { assert_eq!(bom.validate(), ValidationResult::Passed); } From 44e1abc26d34f2a6eec658963b13ab9f5673f5dd Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Mon, 26 Feb 2024 04:18:53 +0000 Subject: [PATCH 10/16] cyclonedx-bom: also record PURL --- auditable-cyclonedx/src/lib.rs | 51 ++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/auditable-cyclonedx/src/lib.rs b/auditable-cyclonedx/src/lib.rs index 4538c49..56f325d 100644 --- a/auditable-cyclonedx/src/lib.rs +++ b/auditable-cyclonedx/src/lib.rs @@ -1,16 +1,21 @@ #![forbid(unsafe_code)] +use std::str::FromStr; + pub use auditable_serde; -use auditable_serde::Package; +use auditable_serde::{Package, Source}; pub use cyclonedx_bom; -use cyclonedx_bom::models::{ - component::Classification, - component::Component, - dependency::{Dependencies, Dependency}, - metadata::Metadata, -}; use cyclonedx_bom::prelude::*; +use cyclonedx_bom::{ + external_models::uri::Purl, + models::{ + component::Classification, + component::Component, + dependency::{Dependencies, Dependency}, + metadata::Metadata, + }, +}; /// Converts the metadata embedded by `cargo auditable` to a minimal CycloneDX document /// that is heavily optimized to reduce the size @@ -69,15 +74,18 @@ fn pkg_to_component(pkg: &auditable_serde::Package, idx: usize) -> Component { // The only requirement for `bom_ref` according to the spec is that it's unique, // so we just keep the unique numbering already used in the original let bom_ref = idx.to_string(); - Component::new( + let mut result = Component::new( component_type, &pkg.name, &pkg.version.to_string(), Some(bom_ref), - ) - // TODO: source + ); + // PURL encodes the package origin (registry, git, local) - sort of, anyway + let purl = purl(&pkg); + let purl = Purl::from_str(&purl).unwrap(); + result.purl = Some(purl); // TODO: dependency kind - // TODO: purl + result } fn root_package(input: &auditable_serde::VersionInfo) -> (usize, &Package) { @@ -89,3 +97,24 @@ fn root_package(input: &auditable_serde::VersionInfo) -> (usize, &Package) { .find(|(_idx, pkg)| pkg.root) .expect("VersionInfo contains no root package!") } + +fn purl(pkg: &auditable_serde::Package) -> String { + // The purl crate exposed by `cyclonedx-bom` doesn't support the qualifiers we need, + // so we just build the PURL as a string. + // Yeah, we could use *yet another* dependency to build the PURL, + // but we use it such trivial ways that it isn't worth the trouble. + // Specifically, the crate names that crates.io accepts don't need percent-encoding + // and the fixed values we put in arguments don't either + // (but percent-encoding is underspecified and not interoperable anyway, + // see e.g. https://github.com/package-url/purl-spec/pull/261) + let mut purl = format!("pkg:cargo/{}@{}", pkg.name, pkg.version); + purl.push_str(match &pkg.source { + Source::CratesIo => "", // this is the default, nothing to qualify + Source::Git => "&vcs_url=redacted", + Source::Local => "&download_url=redacted", + Source::Registry => "&repository_url=redacted", + Source::Other(_) => "&download_url=redacted", + unknown => panic!("Unknown source: {:?}", unknown), + }); + purl +} From 6eb05d2ccc186e150ab44fdd9a1fbbd1bfba3121 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Mon, 26 Feb 2024 04:31:39 +0000 Subject: [PATCH 11/16] Also record the dependency kind --- auditable-cyclonedx/src/lib.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/auditable-cyclonedx/src/lib.rs b/auditable-cyclonedx/src/lib.rs index 56f325d..3d33f2a 100644 --- a/auditable-cyclonedx/src/lib.rs +++ b/auditable-cyclonedx/src/lib.rs @@ -6,6 +6,7 @@ pub use auditable_serde; use auditable_serde::{Package, Source}; pub use cyclonedx_bom; +use cyclonedx_bom::models::property::{Properties, Property}; use cyclonedx_bom::prelude::*; use cyclonedx_bom::{ external_models::uri::Purl, @@ -84,7 +85,15 @@ fn pkg_to_component(pkg: &auditable_serde::Package, idx: usize) -> Component { let purl = purl(&pkg); let purl = Purl::from_str(&purl).unwrap(); result.purl = Some(purl); - // TODO: dependency kind + // Record the dependency kind + match pkg.kind { + // `Runtime` is the default and does not need to be recorded. + auditable_serde::DependencyKind::Runtime => (), + auditable_serde::DependencyKind::Build => { + let p = Property::new("cdx:rustc:dependency_kind".to_owned(), "build".into()); + result.properties = Some(Properties(vec![p])); + } + } result } From 968107029bddcfc2d2c70cc6b0a14880e29e4a00 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Mon, 26 Feb 2024 05:26:46 +0000 Subject: [PATCH 12/16] Work around cyclonedx-bom limitations to produce minified JSON --- Cargo.lock | 1 + auditable2cdx/Cargo.toml | 1 + auditable2cdx/src/main.rs | 9 +++++++-- auditable2cdx/src/workarounds.rs | 25 +++++++++++++++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 auditable2cdx/src/workarounds.rs diff --git a/Cargo.lock b/Cargo.lock index 3ce9dce..160e332 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,6 +72,7 @@ version = "0.1.0" dependencies = [ "auditable-cyclonedx", "auditable-info", + "serde_json", ] [[package]] diff --git a/auditable2cdx/Cargo.toml b/auditable2cdx/Cargo.toml index 7c7e635..65bd62f 100644 --- a/auditable2cdx/Cargo.toml +++ b/auditable2cdx/Cargo.toml @@ -8,3 +8,4 @@ edition = "2021" [dependencies] auditable-info = {version = "0.7.0", path = "../auditable-info"} auditable-cyclonedx = {version = "0.1.0", path = "../auditable-cyclonedx"} +serde_json = "1.0.114" diff --git a/auditable2cdx/src/main.rs b/auditable2cdx/src/main.rs index 7145c80..47488a7 100644 --- a/auditable2cdx/src/main.rs +++ b/auditable2cdx/src/main.rs @@ -1,14 +1,19 @@ -use std::path::Path; +use std::{io::Write, path::Path}; use auditable_cyclonedx::auditable_to_minimal_cdx; use auditable_info::audit_info_from_file; +mod workarounds; + fn main() { let input_filename = std::env::args_os() .nth(1) .expect("No input file specified!"); let info = audit_info_from_file(Path::new(&input_filename), Default::default()).unwrap(); let cyclonedx = auditable_to_minimal_cdx(&info); + let mut json_bytes: Vec = Vec::new(); + cyclonedx.output_as_json_v1_3(&mut json_bytes).unwrap(); + let min_json = workarounds::minify_bom(&json_bytes); let mut stdout = std::io::stdout().lock(); - cyclonedx.output_as_json_v1_3(&mut stdout).unwrap(); + stdout.write_all(min_json.as_bytes()).unwrap(); } diff --git a/auditable2cdx/src/workarounds.rs b/auditable2cdx/src/workarounds.rs new file mode 100644 index 0000000..e07c3d8 --- /dev/null +++ b/auditable2cdx/src/workarounds.rs @@ -0,0 +1,25 @@ +use serde_json; + +/// Accepts BOM in JSON and minifies it, +/// working around https://github.com/CycloneDX/cyclonedx-rust-cargo/issues/628 +pub fn minify_bom(bom: &[u8]) -> String { + let mut json: serde_json::Value = serde_json::from_slice(bom).unwrap(); + // clear the unnecessary toplevel fields + let toplevel = json.as_object_mut().unwrap(); + toplevel.remove("version"); + toplevel.remove("serialNumber"); + // clear empty arrays in dependencies + if let Some(deps) = toplevel.get_mut("dependencies") { + let deps = deps.as_array_mut().unwrap(); + deps.iter_mut().for_each(|dependency| { + if let Some(deps_array) = dependency.get("dependsOn") { + let deps_array = deps_array.as_array().unwrap(); + if deps_array.is_empty() { + dependency.as_object_mut().unwrap().remove("dependsOn"); + } + } + }); + } + // .to_string() writes the minified JSON, unlike .to_string_pretty() + serde_json::to_string(&json).unwrap() +} From c6fbed83983fbd0cde1fa37cf97bdec76c0f286b Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Mon, 26 Feb 2024 05:37:14 +0000 Subject: [PATCH 13/16] Use serde_json with order preservation feature to get a more compressible JSON after workarounds --- Cargo.lock | 27 +++++++++++++++++++++++++-- auditable2cdx/Cargo.toml | 2 +- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 160e332..8b3ab39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -214,6 +214,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "fluent-uri" version = "0.1.4" @@ -258,6 +264,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "idna" version = "0.3.0" @@ -278,6 +290,16 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "indexmap" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", +] + [[package]] name = "itoa" version = "1.0.3" @@ -328,7 +350,7 @@ checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" dependencies = [ "crc32fast", "hashbrown 0.13.2", - "indexmap", + "indexmap 1.9.1", "memchr", ] @@ -498,6 +520,7 @@ version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ + "indexmap 2.2.3", "itoa", "ryu", "serde", @@ -642,7 +665,7 @@ version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" dependencies = [ - "indexmap", + "indexmap 1.9.1", "serde", "serde_spanned", "toml_datetime", diff --git a/auditable2cdx/Cargo.toml b/auditable2cdx/Cargo.toml index 65bd62f..8d1f15f 100644 --- a/auditable2cdx/Cargo.toml +++ b/auditable2cdx/Cargo.toml @@ -8,4 +8,4 @@ edition = "2021" [dependencies] auditable-info = {version = "0.7.0", path = "../auditable-info"} auditable-cyclonedx = {version = "0.1.0", path = "../auditable-cyclonedx"} -serde_json = "1.0.114" +serde_json = {version = "1.0.114", features = ["preserve_order"] } # the feature is needed for workarounds module only From f67b5236f4670fc2af069ce1d5ba9e62c3871da2 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Mon, 26 Feb 2024 05:38:52 +0000 Subject: [PATCH 14/16] Also remove the dependencies field if empty --- auditable2cdx/src/workarounds.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/auditable2cdx/src/workarounds.rs b/auditable2cdx/src/workarounds.rs index e07c3d8..cff49a3 100644 --- a/auditable2cdx/src/workarounds.rs +++ b/auditable2cdx/src/workarounds.rs @@ -8,6 +8,13 @@ pub fn minify_bom(bom: &[u8]) -> String { let toplevel = json.as_object_mut().unwrap(); toplevel.remove("version"); toplevel.remove("serialNumber"); + // clear components field if empty + if let Some(components) = toplevel.get_mut("dependencies") { + let components = components.as_array().unwrap(); + if components.is_empty() { + toplevel.remove("dependencies"); + } + } // clear empty arrays in dependencies if let Some(deps) = toplevel.get_mut("dependencies") { let deps = deps.as_array_mut().unwrap(); From 0437f61dd731fb15444703417a4ab395d13da646 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Mon, 26 Feb 2024 05:49:23 +0000 Subject: [PATCH 15/16] Oopps, I meant components field --- auditable2cdx/src/workarounds.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auditable2cdx/src/workarounds.rs b/auditable2cdx/src/workarounds.rs index cff49a3..6c3cc9f 100644 --- a/auditable2cdx/src/workarounds.rs +++ b/auditable2cdx/src/workarounds.rs @@ -9,10 +9,10 @@ pub fn minify_bom(bom: &[u8]) -> String { toplevel.remove("version"); toplevel.remove("serialNumber"); // clear components field if empty - if let Some(components) = toplevel.get_mut("dependencies") { + if let Some(components) = toplevel.get_mut("components") { let components = components.as_array().unwrap(); if components.is_empty() { - toplevel.remove("dependencies"); + toplevel.remove("components"); } } // clear empty arrays in dependencies From da268ef6733cf855d245b744b7c4bce56e3b05da Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 29 Feb 2024 00:13:06 +0000 Subject: [PATCH 16/16] Placate clippy --- auditable-cyclonedx/src/lib.rs | 19 ++++++++++--------- auditable2cdx/src/workarounds.rs | 2 -- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/auditable-cyclonedx/src/lib.rs b/auditable-cyclonedx/src/lib.rs index 3d33f2a..af87169 100644 --- a/auditable-cyclonedx/src/lib.rs +++ b/auditable-cyclonedx/src/lib.rs @@ -21,18 +21,19 @@ use cyclonedx_bom::{ /// Converts the metadata embedded by `cargo auditable` to a minimal CycloneDX document /// that is heavily optimized to reduce the size pub fn auditable_to_minimal_cdx(input: &auditable_serde::VersionInfo) -> Bom { - let mut bom = Bom::default(); - - // Clear the serial number which would mess with reproducible builds - // and also take up valuable space - bom.serial_number = None; + let mut bom = Bom { + serial_number: None, // the serial number would mess with reproducible builds + ..Default::default() + }; // The toplevel component goes into its own field, as per the spec: // https://cyclonedx.org/docs/1.5/json/#metadata_component let (root_idx, root_pkg) = root_package(input); let root_component = pkg_to_component(root_pkg, root_idx); - let mut metadata = Metadata::default(); - metadata.component = Some(root_component); + let metadata = Metadata { + component: Some(root_component), + ..Default::default() + }; bom.metadata = Some(metadata); // Fill in the component list, excluding the toplevel component (already encoded) @@ -82,7 +83,7 @@ fn pkg_to_component(pkg: &auditable_serde::Package, idx: usize) -> Component { Some(bom_ref), ); // PURL encodes the package origin (registry, git, local) - sort of, anyway - let purl = purl(&pkg); + let purl = purl(pkg); let purl = Purl::from_str(&purl).unwrap(); result.purl = Some(purl); // Record the dependency kind @@ -90,7 +91,7 @@ fn pkg_to_component(pkg: &auditable_serde::Package, idx: usize) -> Component { // `Runtime` is the default and does not need to be recorded. auditable_serde::DependencyKind::Runtime => (), auditable_serde::DependencyKind::Build => { - let p = Property::new("cdx:rustc:dependency_kind".to_owned(), "build".into()); + let p = Property::new("cdx:rustc:dependency_kind".to_owned(), "build"); result.properties = Some(Properties(vec![p])); } } diff --git a/auditable2cdx/src/workarounds.rs b/auditable2cdx/src/workarounds.rs index 6c3cc9f..d824b20 100644 --- a/auditable2cdx/src/workarounds.rs +++ b/auditable2cdx/src/workarounds.rs @@ -1,5 +1,3 @@ -use serde_json; - /// Accepts BOM in JSON and minifies it, /// working around https://github.com/CycloneDX/cyclonedx-rust-cargo/issues/628 pub fn minify_bom(bom: &[u8]) -> String {