diff --git a/cargo-cyclonedx/src/cli.rs b/cargo-cyclonedx/src/cli.rs index 1dfb6fea..6278fbd9 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 #[clap( name = "output-pattern", long = "output-pattern", diff --git a/cargo-cyclonedx/src/config.rs b/cargo-cyclonedx/src/config.rs index 8b6119ad..b61c36c4 100644 --- a/cargo-cyclonedx/src/config.rs +++ b/cargo-cyclonedx/src/config.rs @@ -164,6 +164,7 @@ pub enum Pattern { #[default] Bom, Package, + Binary, } impl FromStr for Pattern { @@ -173,7 +174,11 @@ impl FromStr for Pattern { match s { "bom" => Ok(Self::Bom), "package" => Ok(Self::Package), - _ => Err(format!("Expected bom or package, got `{}`", s)), + "binary" => Ok(Self::Binary), + _ => Err(format!( + "Expected 'bom', 'package' or 'binary', got `{}`", + s + )), } } } diff --git a/cargo-cyclonedx/src/generator.rs b/cargo-cyclonedx/src/generator.rs index 77c3e74f..0d99fb37 100644 --- a/cargo-cyclonedx/src/generator.rs +++ b/cargo-cyclonedx/src/generator.rs @@ -53,9 +53,10 @@ use regex::Regex; use log::Level; use std::collections::BTreeMap; +use std::collections::BTreeSet; use std::convert::TryFrom; use std::fs::File; -use std::io::BufWriter; +use std::io::Cursor; use std::io::Write; use std::path::PathBuf; use thiserror::Error; @@ -609,10 +610,9 @@ 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()); - log::info!("Outputting {}", path.display()); - let file = File::create(path)?; - let mut writer = BufWriter::new(file); + let filenames = self.filenames(); + let serialized_sbom = Vec::new(); + let mut writer = Cursor::new(serialized_sbom); match self.sbom_config.format() { Format::Json => { self.bom @@ -625,19 +625,36 @@ impl GeneratedSbom { .map_err(SbomWriterError::XmlWriteError)?; } } - - // Flush the writer explicitly to catch and report any I/O errors - writer.flush()?; + let serialized_sbom = writer.into_inner(); + + // Why do we write the exact same SBOM into multiple files? + // Good question! And a long story! + // See https://github.com/CycloneDX/cyclonedx-rust-cargo/pull/563#issue-1997891622 + // for a detailed explanation of why this behavior was chosen. + for filename in filenames { + let path = self.manifest_path.with_file_name(filename); + log::info!("Outputting {}", path.display()); + let mut file = File::create(path)?; + file.write_all(&serialized_sbom)?; + } Ok(()) } - fn filename(&self) -> String { + fn filenames(&self) -> BTreeSet { 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::Custom(c) => c.to_string(), + let prefixes = match output_options.prefix { + Prefix::Pattern(Pattern::Bom) => vec!["bom".to_string()], + Prefix::Pattern(Pattern::Package) => vec![self.package_name.clone()], + Prefix::Custom(c) => vec![c.to_string()], + Prefix::Pattern(Pattern::Binary) => { + // Different from the others in that we potentially output the same SBOM to multiple files. + // We can safely `.unwrap()` here because we have just written these fields ourselves. + let meta = self.bom.metadata.as_ref().unwrap(); + let top_component = meta.component.as_ref().unwrap(); + let components = top_component.components.as_ref().unwrap(); + components.0.iter().map(|c| c.name.to_string()).collect() + } }; let platform_suffix = match output_options.platform_suffix { @@ -648,13 +665,18 @@ impl GeneratedSbom { } }; - format!( - "{}{}{}.{}", - prefix, - platform_suffix, - output_options.cdx_extension.extension(), - self.sbom_config.format() - ) + prefixes + .iter() + .map(|prefix| { + format!( + "{}{}{}.{}", + prefix, + platform_suffix, + output_options.cdx_extension.extension(), + self.sbom_config.format() + ) + }) + .collect() } }