From 27c8d540ef5a4edfd11f2c2486a548a1449aea6d Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sun, 21 Jan 2024 19:34:19 +0000 Subject: [PATCH 01/14] Added command-line arguments for emitting per-binary and per-cargo-target SBOMs Signed-off-by: Sergey "Shnatsel" Davidoff --- cargo-cyclonedx/src/cli.rs | 2 +- cargo-cyclonedx/src/config.rs | 6 ++++++ cargo-cyclonedx/src/generator.rs | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cargo-cyclonedx/src/cli.rs b/cargo-cyclonedx/src/cli.rs index 1dfb6fea..afd2c042 100644 --- a/cargo-cyclonedx/src/cli.rs +++ b/cargo-cyclonedx/src/cli.rs @@ -80,7 +80,7 @@ Defaults to the host target, as printed by 'rustc -vV'" #[clap(long = "output-cdx")] pub output_cdx: bool, - /// Prefix patterns to use for the filename: bom, package + /// Prefix patterns to use for the filename: bom, package, binary, cargo-target #[clap( name = "output-pattern", long = "output-pattern", diff --git a/cargo-cyclonedx/src/config.rs b/cargo-cyclonedx/src/config.rs index 8b6119ad..c76fda32 100644 --- a/cargo-cyclonedx/src/config.rs +++ b/cargo-cyclonedx/src/config.rs @@ -164,6 +164,10 @@ pub enum Pattern { #[default] Bom, Package, + Binary, + /// Not to be confused with a compilation target: + /// https://doc.rust-lang.org/cargo/reference/cargo-targets.html + CargoTarget, } impl FromStr for Pattern { @@ -173,6 +177,8 @@ impl FromStr for Pattern { match s { "bom" => Ok(Self::Bom), "package" => Ok(Self::Package), + "binary" => Ok(Self::Binary), + "cargo-target" => Ok(Self::CargoTarget), _ => Err(format!("Expected bom or package, got `{}`", s)), } } diff --git a/cargo-cyclonedx/src/generator.rs b/cargo-cyclonedx/src/generator.rs index ddd23aa4..80365d49 100644 --- a/cargo-cyclonedx/src/generator.rs +++ b/cargo-cyclonedx/src/generator.rs @@ -663,6 +663,8 @@ impl GeneratedSbom { let prefix = match output_options.prefix { Prefix::Pattern(Pattern::Bom) => "bom".to_string(), Prefix::Pattern(Pattern::Package) => self.package_name.clone(), + Prefix::Pattern(Pattern::Binary) => todo!(), + Prefix::Pattern(Pattern::CargoTarget) => todo!(), Prefix::Custom(c) => c.to_string(), }; From 467d88d4d9e022e3c77621faf1d658f57cf99825 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Wed, 24 Jan 2024 01:00:55 +0000 Subject: [PATCH 02/14] More robust subcompoment numbering --- cargo-cyclonedx/src/generator.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cargo-cyclonedx/src/generator.rs b/cargo-cyclonedx/src/generator.rs index 80365d49..9d741296 100644 --- a/cargo-cyclonedx/src/generator.rs +++ b/cargo-cyclonedx/src/generator.rs @@ -181,7 +181,6 @@ impl SbomGenerator { fn create_toplevel_component(&self, package: &Package) -> Component { let mut top_component = self.create_component(package, package); let mut subcomponents: Vec = Vec::new(); - let mut subcomp_count: u32 = 0; for tgt in &package.targets { // Ignore tests, benches, examples and build scripts. // They are not part of the final build artifacts, which is what we are after. @@ -209,9 +208,8 @@ impl SbomGenerator { let bom_ref = format!( "{} bin-target-{}", top_component.bom_ref.as_ref().unwrap(), - subcomp_count + subcomponents.len(), // numbers the components ); - subcomp_count += 1; // create the subcomponent let mut subcomponent = Component::new( From 90151134e33f940bbd96f9041e0aa7ffc96cfd4c Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Wed, 24 Jan 2024 01:27:13 +0000 Subject: [PATCH 03/14] Split target filtering into its own function for future reuse --- cargo-cyclonedx/src/generator.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/cargo-cyclonedx/src/generator.rs b/cargo-cyclonedx/src/generator.rs index 9d741296..b9c72435 100644 --- a/cargo-cyclonedx/src/generator.rs +++ b/cargo-cyclonedx/src/generator.rs @@ -181,10 +181,7 @@ impl SbomGenerator { fn create_toplevel_component(&self, package: &Package) -> Component { let mut top_component = self.create_component(package, package); let mut subcomponents: Vec = Vec::new(); - for tgt in &package.targets { - // Ignore tests, benches, examples and build scripts. - // They are not part of the final build artifacts, which is what we are after. - if !(tgt.is_bench() || tgt.is_example() || tgt.is_test() || tgt.is_custom_build()) { + for tgt in filter_targets(&package.targets) { // classification #[allow(clippy::if_same_then_else)] let cdx_type = if tgt.is_bin() { @@ -243,7 +240,6 @@ impl SbomGenerator { } subcomponents.push(subcomponent); - } } top_component.components = Some(Components(subcomponents)); top_component @@ -472,6 +468,14 @@ impl SbomGenerator { } } +/// Ignore tests, benches, examples and build scripts. +/// They are not part of the final build artifacts, which is what we are after. +fn filter_targets(targets: &[cargo_metadata::Target]) -> impl Iterator { + targets.iter().filter(|tgt| { + !(tgt.is_bench() || tgt.is_example() || tgt.is_test() || tgt.is_custom_build()) + }) +} + fn index_packages(packages: Vec) -> PackageMap { packages .into_iter() From 99a6d067751eb3149689e62b4ba3358fbfb4276b Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Wed, 24 Jan 2024 02:04:32 +0000 Subject: [PATCH 04/14] Pass the target kinds to the struct holding the generated SBOM --- cargo-cyclonedx/src/generator.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cargo-cyclonedx/src/generator.rs b/cargo-cyclonedx/src/generator.rs index b9c72435..5e49dddf 100644 --- a/cargo-cyclonedx/src/generator.rs +++ b/cargo-cyclonedx/src/generator.rs @@ -105,11 +105,20 @@ impl SbomGenerator { } } + // Figure out the types of the various produced artifacts. + // This is additional information on top of the SBOM structure + // that is used to implement emitting a separate SBOM for each binary or artifact. + let root_package = &packages[member]; + let target_kinds: Vec> = filter_targets(&root_package.targets).map(|tgt| { + tgt.kind.clone() + }).collect(); + let generated = GeneratedSbom { bom, manifest_path: packages[member].manifest_path.clone().into_std_path_buf(), package_name: packages[member].name.clone(), sbom_config: generator.config, + target_kinds, }; result.push(generated); @@ -632,6 +641,7 @@ pub struct GeneratedSbom { pub manifest_path: PathBuf, pub package_name: String, pub sbom_config: SbomConfig, + pub target_kinds: Vec>, } impl GeneratedSbom { From df7bd735559648c5c9f86f8a236ce4498ef68255 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 26 Jan 2024 00:27:40 +0000 Subject: [PATCH 05/14] Refactor write_to_file in preparation for outputting a SBOM per binary --- cargo-cyclonedx/src/generator.rs | 22 +++++++++++++++++----- cargo-cyclonedx/src/main.rs | 2 +- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/cargo-cyclonedx/src/generator.rs b/cargo-cyclonedx/src/generator.rs index 5e49dddf..2978cab0 100644 --- a/cargo-cyclonedx/src/generator.rs +++ b/cargo-cyclonedx/src/generator.rs @@ -58,6 +58,7 @@ use std::convert::TryFrom; use std::fs::File; use std::io::BufWriter; use std::io::Write; +use std::path::Path; use std::path::PathBuf; use thiserror::Error; use validator::validate_email; @@ -636,6 +637,7 @@ fn non_dev_dependencies(input: &[NodeDep]) -> impl Iterator { /// * `manifest_path` - Folder containing the `Cargo.toml` manifest /// * `package_name` - Package from which this SBOM was generated /// * `sbom_config` - Configuration options used during generation +/// * `target_kinds` - Detailed information on the kinds of targets in `sbom` pub struct GeneratedSbom { pub bom: Bom, pub manifest_path: PathBuf, @@ -646,19 +648,29 @@ pub struct GeneratedSbom { impl GeneratedSbom { /// Writes SBOM to either a JSON or XML file in the same folder as `Cargo.toml` manifest - pub fn write_to_file(self) -> Result<(), SbomWriterError> { - let path = self.manifest_path.with_file_name(self.filename()); + pub fn write_to_files(self) -> Result<(), SbomWriterError> { + match self.sbom_config.output_options().prefix { + Prefix::Pattern(Pattern::Bom | Pattern::Package) | Prefix::Custom(_) => { + let path = self.manifest_path.with_file_name(self.filename()); + Self::write_to_file(self.bom, &path, &self.sbom_config) + }, + Prefix::Pattern(Pattern::Binary) => todo!(), + Prefix::Pattern(Pattern::CargoTarget) => todo!(), + } + } + + fn write_to_file(bom: Bom, path: &Path, config: &SbomConfig) -> Result<(), SbomWriterError> { log::info!("Outputting {}", path.display()); let file = File::create(path)?; let mut writer = BufWriter::new(file); - match self.sbom_config.format() { + match config.format() { Format::Json => { - self.bom + bom .output_as_json_v1_3(&mut writer) .map_err(SbomWriterError::JsonWriteError)?; } Format::Xml => { - self.bom + bom .output_as_xml_v1_3(&mut writer) .map_err(SbomWriterError::XmlWriteError)?; } diff --git a/cargo-cyclonedx/src/main.rs b/cargo-cyclonedx/src/main.rs index 5c652b48..4f6e9611 100644 --- a/cargo-cyclonedx/src/main.rs +++ b/cargo-cyclonedx/src/main.rs @@ -83,7 +83,7 @@ fn main() -> anyhow::Result<()> { log::trace!("SBOM output started"); for bom in boms { - bom.write_to_file()?; + bom.write_to_files()?; } log::trace!("SBOM output finished"); From eb28ca650c108b0e4a3fbb1c95216a4638116d76 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 26 Jan 2024 00:56:19 +0000 Subject: [PATCH 06/14] Implement target type filtering --- cargo-cyclonedx/src/generator.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/cargo-cyclonedx/src/generator.rs b/cargo-cyclonedx/src/generator.rs index 2978cab0..1a114211 100644 --- a/cargo-cyclonedx/src/generator.rs +++ b/cargo-cyclonedx/src/generator.rs @@ -682,6 +682,29 @@ impl GeneratedSbom { Ok(()) } + fn per_artifact_sboms<'a>(bom: &'a Bom, target_kinds: &'a [Vec], pattern: Pattern) -> impl Iterator + 'a { + let meta = bom.metadata.as_ref().unwrap(); + let component = meta.component.as_ref().unwrap(); + let components = component.components.as_ref().unwrap(); + components.0.iter().zip(target_kinds.iter()).filter(move |(component, target_kind)| { + match pattern { + Pattern::Binary => { + // only record binary artifacts + // TODO: refactor this to use an enum, coming Soon(tm) to cargo-metadata: + // https://github.com/oli-obk/cargo_metadata/pull/258 + target_kind.contains(&"bin".to_owned()) || target_kind.contains(&"cdylib".to_owned()) + }, + Pattern::CargoTarget => true, // pass everything through + Pattern::Bom | Pattern::Package => unreachable!(), + } + }).map(|(component, target_kind)| { + let bom = bom.clone(); + // BIG FAT TODO + + bom + }) + } + fn filename(&self) -> String { let output_options = self.sbom_config.output_options(); let prefix = match output_options.prefix { From 035d766c9a6f0748792dc765eeba3fb6a9936a7a Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 26 Jan 2024 01:50:51 +0000 Subject: [PATCH 07/14] Implement writing SBOMs into separate files, still same SBOM goes everywhere for now. Looking kinda ugly ngl --- cargo-cyclonedx/src/generator.rs | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/cargo-cyclonedx/src/generator.rs b/cargo-cyclonedx/src/generator.rs index 1a114211..29f459a6 100644 --- a/cargo-cyclonedx/src/generator.rs +++ b/cargo-cyclonedx/src/generator.rs @@ -651,11 +651,15 @@ impl GeneratedSbom { pub fn write_to_files(self) -> Result<(), SbomWriterError> { match self.sbom_config.output_options().prefix { Prefix::Pattern(Pattern::Bom | Pattern::Package) | Prefix::Custom(_) => { - let path = self.manifest_path.with_file_name(self.filename()); + let path = self.manifest_path.with_file_name(self.filename(&[])); Self::write_to_file(self.bom, &path, &self.sbom_config) }, - Prefix::Pattern(Pattern::Binary) => todo!(), - Prefix::Pattern(Pattern::CargoTarget) => todo!(), + Prefix::Pattern(pattern @ (Pattern::Binary | Pattern::CargoTarget)) => { + for (sbom, target_kind) in Self::per_artifact_sboms(&self.bom, &self.target_kinds, pattern) { + todo!(); + } + Ok(()) + }, } } @@ -682,11 +686,11 @@ impl GeneratedSbom { Ok(()) } - fn per_artifact_sboms<'a>(bom: &'a Bom, target_kinds: &'a [Vec], pattern: Pattern) -> impl Iterator + 'a { + fn per_artifact_sboms<'a>(bom: &'a Bom, target_kinds: &'a [Vec], pattern: Pattern) -> impl Iterator)> + 'a { let meta = bom.metadata.as_ref().unwrap(); let component = meta.component.as_ref().unwrap(); let components = component.components.as_ref().unwrap(); - components.0.iter().zip(target_kinds.iter()).filter(move |(component, target_kind)| { + components.0.iter().zip(target_kinds.iter()).filter(move |(_component, target_kind)| { match pattern { Pattern::Binary => { // only record binary artifacts @@ -696,18 +700,18 @@ impl GeneratedSbom { }, Pattern::CargoTarget => true, // pass everything through Pattern::Bom | Pattern::Package => unreachable!(), - } + } }).map(|(component, target_kind)| { let bom = bom.clone(); // BIG FAT TODO - bom + (bom, target_kind.clone()) }) } - fn filename(&self) -> String { + fn filename(&self, target_kind: &[String]) -> String { let output_options = self.sbom_config.output_options(); - let prefix = match output_options.prefix { + let prefix = match &output_options.prefix { Prefix::Pattern(Pattern::Bom) => "bom".to_string(), Prefix::Pattern(Pattern::Package) => self.package_name.clone(), Prefix::Pattern(Pattern::Binary) => todo!(), @@ -715,6 +719,13 @@ impl GeneratedSbom { Prefix::Custom(c) => c.to_string(), }; + let target_kind_suffix = if !target_kind.is_empty() { + debug_assert!(matches!(&output_options.prefix, Prefix::Pattern(Pattern::Binary | Pattern::CargoTarget))); + format!("_{}", target_kind.join("-")) + } else { + "".to_owned() + }; + let platform_suffix = match output_options.platform_suffix { PlatformSuffix::NotIncluded => "".to_owned(), PlatformSuffix::Included => { @@ -724,8 +735,9 @@ impl GeneratedSbom { }; format!( - "{}{}{}.{}", + "{}{}{}{}.{}", prefix, + target_kind_suffix, platform_suffix, output_options.cdx_extension.extension(), self.sbom_config.format() From 464363d597849b836fe7dd9dd4d85f7dbdc39dd3 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sat, 17 Feb 2024 00:30:35 +0000 Subject: [PATCH 08/14] Flesh out the toplevel component replacement logic in per-binary SBOM writing Signed-off-by: Sergey "Shnatsel" Davidoff --- cargo-cyclonedx/src/generator.rs | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/cargo-cyclonedx/src/generator.rs b/cargo-cyclonedx/src/generator.rs index b2fed377..95c8b9da 100644 --- a/cargo-cyclonedx/src/generator.rs +++ b/cargo-cyclonedx/src/generator.rs @@ -58,6 +58,7 @@ use std::convert::TryFrom; use std::fs::File; use std::io::BufWriter; use std::io::Write; +use std::mem; use std::path::Path; use std::path::PathBuf; use thiserror::Error; @@ -681,10 +682,12 @@ impl GeneratedSbom { Ok(()) } + /// Returns an iterator over SBOMs and their associated target kinds fn per_artifact_sboms<'a>(bom: &'a Bom, target_kinds: &'a [Vec], pattern: Pattern) -> impl Iterator)> + 'a { let meta = bom.metadata.as_ref().unwrap(); - let component = meta.component.as_ref().unwrap(); - let components = component.components.as_ref().unwrap(); + let crate_component = meta.component.as_ref().unwrap(); + let components = crate_component.components.as_ref().unwrap(); + // Narrow down the set of targets for which we emit a SBOM depending on the configuration components.0.iter().zip(target_kinds.iter()).filter(move |(_component, target_kind)| { match pattern { Pattern::Binary => { @@ -697,10 +700,29 @@ impl GeneratedSbom { Pattern::Bom | Pattern::Package => unreachable!(), } }).map(|(component, target_kind)| { - let bom = bom.clone(); - // BIG FAT TODO + // We need to promote the subcomponent (e.g. a binary) to the top level, + // which in the original SBOM is occupied by the crate. + let mut new_bom = bom.clone(); + let mut component = component.clone(); + // Preserve the bom-ref of the toplevel component (currently a crate). + // We will reuse it so that we don't have to repoint everything + // to the binary's bom-ref. + // The only requirement for a bom-ref is that it is unique within a SBOM. + let bom_ref = crate_component.bom_ref.as_ref().unwrap().clone(); + component.bom_ref = Some(bom_ref); + // Replace the toplevel component (describing a crate) with our modified component describing a binary + let _ = mem::replace(new_bom.metadata.as_mut().unwrap().component.as_mut().unwrap(), component); + + // Validate the generated SBOM if debug assertions are enabled, + // to make sure our monkey-patching didn't break the dependency graph + if cfg!(debug_assertions) { + let result = bom.validate(); + if let ValidationResult::Failed { reasons } = result { + panic!("The generated SBOM for a subcomponent failed validation: {:?}", &reasons); + } + } - (bom, target_kind.clone()) + (new_bom, target_kind.clone()) }) } From ab593fc07fc1218708fc12d63b667e1546a295c5 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sat, 17 Feb 2024 00:51:17 +0000 Subject: [PATCH 09/14] Much more complete toplevel component in per-binary SBOMs Signed-off-by: Sergey "Shnatsel" Davidoff --- cargo-cyclonedx/src/generator.rs | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/cargo-cyclonedx/src/generator.rs b/cargo-cyclonedx/src/generator.rs index 95c8b9da..c053f6b9 100644 --- a/cargo-cyclonedx/src/generator.rs +++ b/cargo-cyclonedx/src/generator.rs @@ -58,7 +58,6 @@ use std::convert::TryFrom; use std::fs::File; use std::io::BufWriter; use std::io::Write; -use std::mem; use std::path::Path; use std::path::PathBuf; use thiserror::Error; @@ -700,27 +699,15 @@ impl GeneratedSbom { Pattern::Bom | Pattern::Package => unreachable!(), } }).map(|(component, target_kind)| { - // We need to promote the subcomponent (e.g. a binary) to the top level, - // which in the original SBOM is occupied by the crate. + // In the original SBOM the toplevel component describes a crate. + // We need to change it to describe a specific binary. + // Most properties apply to the entire package and should be kept; + // we just need to update the name, type and purl. let mut new_bom = bom.clone(); - let mut component = component.clone(); - // Preserve the bom-ref of the toplevel component (currently a crate). - // We will reuse it so that we don't have to repoint everything - // to the binary's bom-ref. - // The only requirement for a bom-ref is that it is unique within a SBOM. - let bom_ref = crate_component.bom_ref.as_ref().unwrap().clone(); - component.bom_ref = Some(bom_ref); - // Replace the toplevel component (describing a crate) with our modified component describing a binary - let _ = mem::replace(new_bom.metadata.as_mut().unwrap().component.as_mut().unwrap(), component); - - // Validate the generated SBOM if debug assertions are enabled, - // to make sure our monkey-patching didn't break the dependency graph - if cfg!(debug_assertions) { - let result = bom.validate(); - if let ValidationResult::Failed { reasons } = result { - panic!("The generated SBOM for a subcomponent failed validation: {:?}", &reasons); - } - } + let toplevel_component = new_bom.metadata.as_mut().unwrap().component.as_mut().unwrap(); + toplevel_component.name = component.name.clone(); + toplevel_component.component_type = component.component_type.clone(); + toplevel_component.purl = component.purl.clone(); (new_bom, target_kind.clone()) }) From 09d5d8889daf661c912a1ec2fbcc3d3b6dc589e7 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sat, 17 Feb 2024 00:54:08 +0000 Subject: [PATCH 10/14] Wire up actually writing out the file Signed-off-by: Sergey "Shnatsel" Davidoff --- cargo-cyclonedx/src/generator.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cargo-cyclonedx/src/generator.rs b/cargo-cyclonedx/src/generator.rs index c053f6b9..afd6e328 100644 --- a/cargo-cyclonedx/src/generator.rs +++ b/cargo-cyclonedx/src/generator.rs @@ -651,7 +651,8 @@ impl GeneratedSbom { }, Prefix::Pattern(pattern @ (Pattern::Binary | Pattern::CargoTarget)) => { for (sbom, target_kind) in Self::per_artifact_sboms(&self.bom, &self.target_kinds, pattern) { - todo!(); + let path = self.manifest_path.with_file_name(self.filename(&target_kind)); + Self::write_to_file(sbom, &path, &self.sbom_config)?; } Ok(()) }, From 5e77da9987582008ffd7d25abd56acf303bf2dbb Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sat, 17 Feb 2024 00:54:32 +0000 Subject: [PATCH 11/14] cargo fmt Signed-off-by: Sergey "Shnatsel" Davidoff --- cargo-cyclonedx/src/generator.rs | 207 +++++++++++++++++-------------- 1 file changed, 115 insertions(+), 92 deletions(-) diff --git a/cargo-cyclonedx/src/generator.rs b/cargo-cyclonedx/src/generator.rs index afd6e328..14ad3a88 100644 --- a/cargo-cyclonedx/src/generator.rs +++ b/cargo-cyclonedx/src/generator.rs @@ -110,9 +110,9 @@ impl SbomGenerator { // This is additional information on top of the SBOM structure // that is used to implement emitting a separate SBOM for each binary or artifact. let root_package = &packages[member]; - let target_kinds: Vec> = filter_targets(&root_package.targets).map(|tgt| { - tgt.kind.clone() - }).collect(); + let target_kinds: Vec> = filter_targets(&root_package.targets) + .map(|tgt| tgt.kind.clone()) + .collect(); let generated = GeneratedSbom { bom, @@ -192,64 +192,64 @@ impl SbomGenerator { let mut top_component = self.create_component(package, package); let mut subcomponents: Vec = Vec::new(); for tgt in filter_targets(&package.targets) { - // classification - #[allow(clippy::if_same_then_else)] - let cdx_type = if tgt.is_bin() { - Classification::Application - // sadly no .is_proc_macro() yet - } else if tgt.kind.iter().any(|kind| kind == "proc-macro") { - // There isn't a better way to express it with CycloneDX types - Classification::Library - } else if tgt.kind.iter().any(|kind| kind.contains("lib")) { - Classification::Library - } else { - log::warn!( - "Target {} is neither a binary nor a library! Kinds: {}", - tgt.name, - tgt.kind.join(", ") - ); - continue; - }; - - // bom_ref - let bom_ref = format!( - "{} bin-target-{}", - top_component.bom_ref.as_ref().unwrap(), - subcomponents.len(), // numbers the components + // classification + #[allow(clippy::if_same_then_else)] + let cdx_type = if tgt.is_bin() { + Classification::Application + // sadly no .is_proc_macro() yet + } else if tgt.kind.iter().any(|kind| kind == "proc-macro") { + // There isn't a better way to express it with CycloneDX types + Classification::Library + } else if tgt.kind.iter().any(|kind| kind.contains("lib")) { + Classification::Library + } else { + log::warn!( + "Target {} is neither a binary nor a library! Kinds: {}", + tgt.name, + tgt.kind.join(", ") ); + continue; + }; - // create the subcomponent - let mut subcomponent = Component::new( - cdx_type, - &tgt.name, - &package.version.to_string(), - Some(bom_ref), - ); + // bom_ref + let bom_ref = format!( + "{} bin-target-{}", + top_component.bom_ref.as_ref().unwrap(), + subcomponents.len(), // numbers the components + ); - // PURL subpaths are computed relative to the directory with the `Cargo.toml` - // *for this specific package*, not the workspace root. - // This is done because the tarball uploaded to crates.io only contains the package, - // not the workspace, so paths resolved relatively to the workspace root would not be valid. - // - // When using a git repo that contains a workspace, Cargo will automatically select - // the right package out of the workspace. Paths can then be resolved relatively to it. - // So the information we encode here is sufficient to idenfity the file in git too. - let package_dir = package - .manifest_path - .parent() - .expect("manifest_path in `cargo metadata` output is not a file!"); - if let Ok(relative_path) = tgt.src_path.strip_prefix(package_dir) { - subcomponent.purl = - get_purl(package, package, &self.workspace_root, Some(relative_path)).ok(); - } else { - log::warn!( - "Source path \"{}\" is not a subpath of workspace root \"{}\"", - tgt.src_path, - self.workspace_root - ); - } + // create the subcomponent + let mut subcomponent = Component::new( + cdx_type, + &tgt.name, + &package.version.to_string(), + Some(bom_ref), + ); + + // PURL subpaths are computed relative to the directory with the `Cargo.toml` + // *for this specific package*, not the workspace root. + // This is done because the tarball uploaded to crates.io only contains the package, + // not the workspace, so paths resolved relatively to the workspace root would not be valid. + // + // When using a git repo that contains a workspace, Cargo will automatically select + // the right package out of the workspace. Paths can then be resolved relatively to it. + // So the information we encode here is sufficient to idenfity the file in git too. + let package_dir = package + .manifest_path + .parent() + .expect("manifest_path in `cargo metadata` output is not a file!"); + if let Ok(relative_path) = tgt.src_path.strip_prefix(package_dir) { + subcomponent.purl = + get_purl(package, package, &self.workspace_root, Some(relative_path)).ok(); + } else { + log::warn!( + "Source path \"{}\" is not a subpath of workspace root \"{}\"", + tgt.src_path, + self.workspace_root + ); + } - subcomponents.push(subcomponent); + subcomponents.push(subcomponent); } top_component.components = Some(Components(subcomponents)); top_component @@ -478,7 +478,9 @@ impl SbomGenerator { /// Ignore tests, benches, examples and build scripts. /// They are not part of the final build artifacts, which is what we are after. -fn filter_targets(targets: &[cargo_metadata::Target]) -> impl Iterator { +fn filter_targets( + targets: &[cargo_metadata::Target], +) -> impl Iterator { targets.iter().filter(|tgt| { !(tgt.is_bench() || tgt.is_example() || tgt.is_test() || tgt.is_custom_build()) }) @@ -648,14 +650,18 @@ impl GeneratedSbom { Prefix::Pattern(Pattern::Bom | Pattern::Package) | Prefix::Custom(_) => { let path = self.manifest_path.with_file_name(self.filename(&[])); Self::write_to_file(self.bom, &path, &self.sbom_config) - }, + } Prefix::Pattern(pattern @ (Pattern::Binary | Pattern::CargoTarget)) => { - for (sbom, target_kind) in Self::per_artifact_sboms(&self.bom, &self.target_kinds, pattern) { - let path = self.manifest_path.with_file_name(self.filename(&target_kind)); + for (sbom, target_kind) in + Self::per_artifact_sboms(&self.bom, &self.target_kinds, pattern) + { + let path = self + .manifest_path + .with_file_name(self.filename(&target_kind)); Self::write_to_file(sbom, &path, &self.sbom_config)?; } Ok(()) - }, + } } } @@ -665,13 +671,11 @@ impl GeneratedSbom { let mut writer = BufWriter::new(file); match config.format() { Format::Json => { - bom - .output_as_json_v1_3(&mut writer) + bom.output_as_json_v1_3(&mut writer) .map_err(SbomWriterError::JsonWriteError)?; } Format::Xml => { - bom - .output_as_xml_v1_3(&mut writer) + bom.output_as_xml_v1_3(&mut writer) .map_err(SbomWriterError::XmlWriteError)?; } } @@ -683,35 +687,51 @@ impl GeneratedSbom { } /// Returns an iterator over SBOMs and their associated target kinds - fn per_artifact_sboms<'a>(bom: &'a Bom, target_kinds: &'a [Vec], pattern: Pattern) -> impl Iterator)> + 'a { + fn per_artifact_sboms<'a>( + bom: &'a Bom, + target_kinds: &'a [Vec], + pattern: Pattern, + ) -> impl Iterator)> + 'a { let meta = bom.metadata.as_ref().unwrap(); let crate_component = meta.component.as_ref().unwrap(); let components = crate_component.components.as_ref().unwrap(); // Narrow down the set of targets for which we emit a SBOM depending on the configuration - components.0.iter().zip(target_kinds.iter()).filter(move |(_component, target_kind)| { - match pattern { - Pattern::Binary => { - // only record binary artifacts - // TODO: refactor this to use an enum, coming Soon(tm) to cargo-metadata: - // https://github.com/oli-obk/cargo_metadata/pull/258 - target_kind.contains(&"bin".to_owned()) || target_kind.contains(&"cdylib".to_owned()) - }, - Pattern::CargoTarget => true, // pass everything through - Pattern::Bom | Pattern::Package => unreachable!(), - } - }).map(|(component, target_kind)| { - // In the original SBOM the toplevel component describes a crate. - // We need to change it to describe a specific binary. - // Most properties apply to the entire package and should be kept; - // we just need to update the name, type and purl. - let mut new_bom = bom.clone(); - let toplevel_component = new_bom.metadata.as_mut().unwrap().component.as_mut().unwrap(); - toplevel_component.name = component.name.clone(); - toplevel_component.component_type = component.component_type.clone(); - toplevel_component.purl = component.purl.clone(); - - (new_bom, target_kind.clone()) - }) + components + .0 + .iter() + .zip(target_kinds.iter()) + .filter(move |(_component, target_kind)| { + match pattern { + Pattern::Binary => { + // only record binary artifacts + // TODO: refactor this to use an enum, coming Soon(tm) to cargo-metadata: + // https://github.com/oli-obk/cargo_metadata/pull/258 + target_kind.contains(&"bin".to_owned()) + || target_kind.contains(&"cdylib".to_owned()) + } + Pattern::CargoTarget => true, // pass everything through + Pattern::Bom | Pattern::Package => unreachable!(), + } + }) + .map(|(component, target_kind)| { + // In the original SBOM the toplevel component describes a crate. + // We need to change it to describe a specific binary. + // Most properties apply to the entire package and should be kept; + // we just need to update the name, type and purl. + let mut new_bom = bom.clone(); + let toplevel_component = new_bom + .metadata + .as_mut() + .unwrap() + .component + .as_mut() + .unwrap(); + toplevel_component.name = component.name.clone(); + toplevel_component.component_type = component.component_type.clone(); + toplevel_component.purl = component.purl.clone(); + + (new_bom, target_kind.clone()) + }) } fn filename(&self, target_kind: &[String]) -> String { @@ -725,7 +745,10 @@ impl GeneratedSbom { }; let target_kind_suffix = if !target_kind.is_empty() { - debug_assert!(matches!(&output_options.prefix, Prefix::Pattern(Pattern::Binary | Pattern::CargoTarget))); + debug_assert!(matches!( + &output_options.prefix, + Prefix::Pattern(Pattern::Binary | Pattern::CargoTarget) + )); format!("_{}", target_kind.join("-")) } else { "".to_owned() From 74b8d5e959d2afe91a170926a3f99fef67953207 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sat, 17 Feb 2024 00:55:44 +0000 Subject: [PATCH 12/14] Adjust the code to make 'cargo fmt' style less gross Signed-off-by: Sergey "Shnatsel" Davidoff --- cargo-cyclonedx/src/generator.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/cargo-cyclonedx/src/generator.rs b/cargo-cyclonedx/src/generator.rs index 14ad3a88..1ceb7931 100644 --- a/cargo-cyclonedx/src/generator.rs +++ b/cargo-cyclonedx/src/generator.rs @@ -719,13 +719,8 @@ impl GeneratedSbom { // Most properties apply to the entire package and should be kept; // we just need to update the name, type and purl. let mut new_bom = bom.clone(); - let toplevel_component = new_bom - .metadata - .as_mut() - .unwrap() - .component - .as_mut() - .unwrap(); + let metadata = new_bom.metadata.as_mut().unwrap(); + let toplevel_component = metadata.component.as_mut().unwrap(); toplevel_component.name = component.name.clone(); toplevel_component.component_type = component.component_type.clone(); toplevel_component.purl = component.purl.clone(); From 71a92203f55d4c84030bca0534abc5a9aa612929 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sat, 17 Feb 2024 00:57:45 +0000 Subject: [PATCH 13/14] Move validation to the writing function so that all SBOMs from all codepaths go through it Signed-off-by: Sergey "Shnatsel" Davidoff --- cargo-cyclonedx/src/generator.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cargo-cyclonedx/src/generator.rs b/cargo-cyclonedx/src/generator.rs index 1ceb7931..0186b8d4 100644 --- a/cargo-cyclonedx/src/generator.rs +++ b/cargo-cyclonedx/src/generator.rs @@ -99,13 +99,6 @@ impl SbomGenerator { }; let bom = generator.create_bom(member, &dependencies, &pruned_resolve)?; - if cfg!(debug_assertions) { - let result = bom.validate(); - if let ValidationResult::Failed { reasons } = result { - panic!("The generated SBOM failed validation: {:?}", &reasons); - } - } - // Figure out the types of the various produced artifacts. // This is additional information on top of the SBOM structure // that is used to implement emitting a separate SBOM for each binary or artifact. @@ -666,6 +659,14 @@ impl GeneratedSbom { } fn write_to_file(bom: Bom, path: &Path, config: &SbomConfig) -> Result<(), SbomWriterError> { + // If running in debug mode, validate that the SBOM is self-consistent and well-formed + if cfg!(debug_assertions) { + let result = bom.validate(); + if let ValidationResult::Failed { reasons } = result { + panic!("The generated SBOM failed validation: {:?}", &reasons); + } + } + log::info!("Outputting {}", path.display()); let file = File::create(path)?; let mut writer = BufWriter::new(file); From 855d00ae710ff34ae732c52dc2c2d2d7b1309eb0 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sat, 17 Feb 2024 01:08:12 +0000 Subject: [PATCH 14/14] Add target filename parameter to the filename-determining function and finish the filename logic for it Signed-off-by: Sergey "Shnatsel" Davidoff --- cargo-cyclonedx/src/generator.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cargo-cyclonedx/src/generator.rs b/cargo-cyclonedx/src/generator.rs index 0186b8d4..8ba2c862 100644 --- a/cargo-cyclonedx/src/generator.rs +++ b/cargo-cyclonedx/src/generator.rs @@ -641,16 +641,18 @@ impl GeneratedSbom { pub fn write_to_files(self) -> Result<(), SbomWriterError> { match self.sbom_config.output_options().prefix { Prefix::Pattern(Pattern::Bom | Pattern::Package) | Prefix::Custom(_) => { - let path = self.manifest_path.with_file_name(self.filename(&[])); + let path = self.manifest_path.with_file_name(self.filename(None, &[])); Self::write_to_file(self.bom, &path, &self.sbom_config) } Prefix::Pattern(pattern @ (Pattern::Binary | Pattern::CargoTarget)) => { for (sbom, target_kind) in Self::per_artifact_sboms(&self.bom, &self.target_kinds, pattern) { + let meta = sbom.metadata.as_ref().unwrap(); + let name = meta.component.as_ref().unwrap().name.as_ref(); let path = self .manifest_path - .with_file_name(self.filename(&target_kind)); + .with_file_name(self.filename(Some(name), &target_kind)); Self::write_to_file(sbom, &path, &self.sbom_config)?; } Ok(()) @@ -730,13 +732,13 @@ impl GeneratedSbom { }) } - fn filename(&self, target_kind: &[String]) -> String { + fn filename(&self, binary_name: Option<&str>, target_kind: &[String]) -> String { let output_options = self.sbom_config.output_options(); let prefix = match &output_options.prefix { Prefix::Pattern(Pattern::Bom) => "bom".to_string(), Prefix::Pattern(Pattern::Package) => self.package_name.clone(), - Prefix::Pattern(Pattern::Binary) => todo!(), - Prefix::Pattern(Pattern::CargoTarget) => todo!(), + Prefix::Pattern(Pattern::Binary) => binary_name.unwrap().to_owned(), + Prefix::Pattern(Pattern::CargoTarget) => binary_name.unwrap().to_owned(), Prefix::Custom(c) => c.to_string(), };