diff --git a/cargo-cyclonedx/src/cli.rs b/cargo-cyclonedx/src/cli.rs index a5ec0218..879ba34c 100644 --- a/cargo-cyclonedx/src/cli.rs +++ b/cargo-cyclonedx/src/cli.rs @@ -100,10 +100,6 @@ Defaults to the host target, as printed by 'rustc -vV'" /// The CycloneDX specification version to output: `1.3`, `1.4` or `1.5`. Defaults to 1.3 #[clap(long = "spec-version")] pub spec_version: Option, - - /// List only dependencies of kind normal (no build deps, no dev deps) - #[clap(name = "only-normal-deps", long = "only-normal-deps")] - pub only_normal_deps: bool, } impl Args { @@ -174,7 +170,6 @@ impl Args { let describe = self.describe; let spec_version = self.spec_version; - let only_normal_deps = Some(self.only_normal_deps); Ok(SbomConfig { format: self.format, @@ -185,7 +180,6 @@ impl Args { license_parser, describe, spec_version, - only_normal_deps, }) } } @@ -196,11 +190,6 @@ pub enum ArgsError { FilenameOverrideError(#[from] FilenameOverrideError), } -#[cfg(test)] -pub fn parse_to_config(args: &[&str]) -> SbomConfig { - Args::parse_from(args.iter()).as_config().unwrap() -} - #[cfg(test)] mod tests { use super::*; @@ -233,6 +222,10 @@ mod tests { assert!(!contains_feature(&config, "")); } + fn parse_to_config(args: &[&str]) -> SbomConfig { + Args::parse_from(args.iter()).as_config().unwrap() + } + fn contains_feature(config: &SbomConfig, feature: &str) -> bool { config .features diff --git a/cargo-cyclonedx/src/config.rs b/cargo-cyclonedx/src/config.rs index 2c54bd5c..c3a11d88 100644 --- a/cargo-cyclonedx/src/config.rs +++ b/cargo-cyclonedx/src/config.rs @@ -33,7 +33,6 @@ pub struct SbomConfig { pub license_parser: Option, pub describe: Option, pub spec_version: Option, - pub only_normal_deps: Option, } impl SbomConfig { @@ -58,7 +57,6 @@ impl SbomConfig { .or_else(|| self.license_parser.clone()), describe: other.describe.or(self.describe), spec_version: other.spec_version.or(self.spec_version), - only_normal_deps: other.only_normal_deps.or(self.only_normal_deps), } } diff --git a/cargo-cyclonedx/src/generator.rs b/cargo-cyclonedx/src/generator.rs index 36bd3d7e..0f453597 100644 --- a/cargo-cyclonedx/src/generator.rs +++ b/cargo-cyclonedx/src/generator.rs @@ -1,5 +1,4 @@ use crate::config::Describe; -use std::cmp::min; /* * This file is part of CycloneDX Rust Cargo. * @@ -55,8 +54,8 @@ use once_cell::sync::Lazy; use regex::Regex; use log::Level; +use std::collections::BTreeMap; use std::collections::HashMap; -use std::collections::{BTreeMap, LinkedList}; use std::convert::TryFrom; use std::fs::File; use std::io::BufWriter; @@ -69,42 +68,6 @@ use validator::validate_email; // Maps from PackageId to Package for efficiency - faster lookups than in a Vec type PackageMap = BTreeMap; type ResolveMap = BTreeMap; -type DependencyKindMap = BTreeMap; - -/// The values are ordered from weakest to strongest so that casting to integer would make sense -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] -enum PrivateDepKind { - Development, - Build, - Runtime, -} -impl From for DependencyKind { - fn from(priv_kind: PrivateDepKind) -> Self { - match priv_kind { - PrivateDepKind::Development => DependencyKind::Development, - PrivateDepKind::Build => DependencyKind::Build, - PrivateDepKind::Runtime => DependencyKind::Normal, - } - } -} - -impl From<&DependencyKind> for PrivateDepKind { - fn from(kind: &DependencyKind) -> Self { - match kind { - DependencyKind::Normal => PrivateDepKind::Runtime, - DependencyKind::Development => PrivateDepKind::Development, - DependencyKind::Build => PrivateDepKind::Build, - _ => panic!("Unknown dependency kind"), - } - } -} - -fn strongest_dep_kind(deps: &[cargo_metadata::DepKindInfo]) -> PrivateDepKind { - deps.iter() - .map(|d| PrivateDepKind::from(&d.kind)) - .max() - .unwrap_or(PrivateDepKind::Runtime) // for compatibility with Rust earlier than 1.41 -} pub struct SbomGenerator { config: SbomConfig, @@ -135,13 +98,11 @@ impl SbomGenerator { for member in members.iter() { log::trace!("Processing the package {}", member); - let dep_kinds = index_dep_kinds(member, &resolve); - let (dependencies, pruned_resolve) = if config.included_dependencies() == IncludedDependencies::AllDependencies { - all_dependencies(member, &packages, &resolve, config) + all_dependencies(member, &packages, &resolve) } else { - top_level_dependencies(member, &packages, &resolve, config) + top_level_dependencies(member, &packages, &resolve) }; let manifest_path = packages[member].manifest_path.clone().into_std_path_buf(); @@ -167,7 +128,7 @@ impl SbomGenerator { crate_hashes, }; let (bom, target_kinds) = - generator.create_bom(member, &dependencies, &pruned_resolve, &dep_kinds)?; + generator.create_bom(member, &dependencies, &pruned_resolve)?; let generated = GeneratedSbom { bom, @@ -188,7 +149,6 @@ impl SbomGenerator { package: &PackageId, packages: &PackageMap, resolve: &ResolveMap, - dep_kinds: &DependencyKindMap, ) -> Result<(Bom, TargetKinds), GeneratorError> { let mut bom = Bom::default(); let root_package = &packages[package]; @@ -196,7 +156,7 @@ impl SbomGenerator { let components: Vec<_> = packages .values() .filter(|p| &p.id != package) - .map(|component| self.create_component(component, root_package, dep_kinds)) + .map(|component| self.create_component(component, root_package)) .collect(); bom.components = Some(Components(components)); @@ -210,12 +170,7 @@ impl SbomGenerator { Ok((bom, target_kinds)) } - fn create_component( - &self, - package: &Package, - root_package: &Package, - dep_kinds: &DependencyKindMap, - ) -> Component { + fn create_component(&self, package: &Package, root_package: &Package) -> Component { let name = package.name.to_owned().trim().to_string(); let version = package.version.to_string(); @@ -235,13 +190,7 @@ impl SbomGenerator { ); component.purl = purl; - component.scope = match dep_kinds - .get(&package.id) - .unwrap_or(&DependencyKind::Normal) - { - DependencyKind::Normal => Some(Scope::Required), - _ => Some(Scope::Excluded), - }; + component.scope = Some(Scope::Required); component.external_references = Self::get_external_references(package); component.licenses = self.get_licenses(package); component.hashes = self.get_hashes(package); @@ -257,7 +206,7 @@ impl SbomGenerator { /// Same as [Self::create_component] but also includes information /// on binaries and libraries comprising it as subcomponents fn create_toplevel_component(&self, package: &Package) -> (Component, TargetKinds) { - let mut top_component = self.create_component(package, package, &DependencyKindMap::new()); + let mut top_component = self.create_component(package, package); let mut subcomponents: Vec = Vec::new(); let mut target_kinds = HashMap::new(); for tgt in filter_targets(&package.targets) { @@ -593,49 +542,6 @@ fn index_resolve(packages: Vec) -> ResolveMap { .collect() } -fn index_dep_kinds(root: &PackageId, resolve: &ResolveMap) -> DependencyKindMap { - // cache strongest found dependency kind for every node - let mut id_to_dep_kind: HashMap<&PackageId, PrivateDepKind> = HashMap::new(); - id_to_dep_kind.insert(root, PrivateDepKind::Runtime); - - let root_node = &resolve[root]; - let mut nodes_to_visit = LinkedList::<&Node>::new(); - nodes_to_visit.push_front(root_node); - - // perform a simple iterative DFS over the dependencies, - // mark child deps with the minimum of parent kind and their own strongest value - // therefore e.g. mark decendants of build dependencies as build dependencies, - // as long as they never occur as normal dependency. - while !nodes_to_visit.is_empty() { - let node = nodes_to_visit.pop_front().unwrap(); - let parent_node_kind = id_to_dep_kind[&node.id]; - - for child_dep in &node.deps { - let child_node = &resolve[&child_dep.pkg]; - let dep_kind = strongest_dep_kind(child_dep.dep_kinds.as_slice()); - let child_node_kind = min(parent_node_kind, dep_kind); - - let dep_kind_on_previous_visit = id_to_dep_kind.get(&child_node.id); - // insert/update a nodes dependency kind, when its new or stronger than the previous value - if dep_kind_on_previous_visit.is_none() - || child_node_kind > *dep_kind_on_previous_visit.unwrap() - { - let _ = id_to_dep_kind.insert(&child_node.id, child_node_kind); - } - - // cargo metadata already return a DAG, so assume this will terminate - // and don't mark already visited notes. It's ok and necessary to visit - // deps as often as they occur. - nodes_to_visit.push_front(child_node); - } - } - - id_to_dep_kind - .iter() - .map(|(x, y)| ((*x).clone(), DependencyKind::from(*y))) - .collect() -} - #[derive(Error, Debug)] pub enum GeneratorError { #[error("Expected a root package in the cargo config: {config_filepath}")] @@ -678,15 +584,13 @@ fn top_level_dependencies( root: &PackageId, packages: &PackageMap, resolve: &ResolveMap, - config: &SbomConfig, ) -> (PackageMap, ResolveMap) { log::trace!("Adding top-level dependencies to SBOM"); // Only include packages that have dependency kinds other than "Development" - let root_node = add_filtered_dependencies(&resolve[root], config); + let root_node = strip_dev_dependencies(&resolve[root]); let mut pkg_result = PackageMap::new(); - // Record the root package, then its direct non-dev dependencies pkg_result.insert(root.to_owned(), packages[root].to_owned()); for id in &root_node.dependencies { @@ -711,7 +615,6 @@ fn all_dependencies( root: &PackageId, packages: &PackageMap, resolve: &ResolveMap, - config: &SbomConfig, ) -> (PackageMap, ResolveMap) { log::trace!("Adding all dependencies to SBOM"); @@ -732,11 +635,9 @@ fn all_dependencies( // If we haven't processed this node yet... if !out_resolve.contains_key(&node.id) { // Add the node to the output - out_resolve.insert(node.id.to_owned(), add_filtered_dependencies(node, config)); + out_resolve.insert(node.id.to_owned(), strip_dev_dependencies(node)); // Queue its dependencies for the next BFS loop iteration - next_queue.extend( - filtered_dependencies(&node.deps, config).map(|dep| &resolve[&dep.pkg]), - ); + next_queue.extend(non_dev_dependencies(&node.deps).map(|dep| &resolve[&dep.pkg])); } } std::mem::swap(&mut current_queue, &mut next_queue); @@ -752,27 +653,20 @@ fn all_dependencies( (out_packages, out_resolve) } -fn add_filtered_dependencies(node: &Node, config: &SbomConfig) -> Node { +fn strip_dev_dependencies(node: &Node) -> Node { let mut node = node.clone(); - node.deps = filtered_dependencies(&node.deps, config).cloned().collect(); + node.deps = non_dev_dependencies(&node.deps).cloned().collect(); node.dependencies = node.deps.iter().map(|d| d.pkg.to_owned()).collect(); node } /// Filters out dependencies only used for development, and not affecting the final binary. /// These are specified under `[dev-dependencies]` in Cargo.toml. -fn filtered_dependencies<'a>( - input: &'a [NodeDep], - config: &'a SbomConfig, -) -> impl Iterator { +fn non_dev_dependencies(input: &[NodeDep]) -> impl Iterator { input.iter().filter(|p| { - p.dep_kinds.iter().any(|dep| { - if let Some(true) = config.only_normal_deps { - dep.kind == DependencyKind::Normal - } else { - dep.kind != DependencyKind::Development - } - }) + p.dep_kinds + .iter() + .any(|dep| dep.kind != DependencyKind::Development) }) } @@ -783,7 +677,6 @@ fn filtered_dependencies<'a>( /// * `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` -#[derive(Debug)] pub struct GeneratedSbom { pub bom: Bom, pub manifest_path: PathBuf, diff --git a/cargo-cyclonedx/src/main.rs b/cargo-cyclonedx/src/main.rs index dd706d35..51a87e4a 100644 --- a/cargo-cyclonedx/src/main.rs +++ b/cargo-cyclonedx/src/main.rs @@ -48,7 +48,6 @@ use cargo_cyclonedx::{ config::{SbomConfig, Target}, generator::SbomGenerator, - GeneratedSbom, }; use std::{ @@ -66,28 +65,22 @@ use log::LevelFilter; mod cli; use cli::{Args, Opts}; -fn generate_sboms(args: &Args) -> Result> { +fn main() -> anyhow::Result<()> { + let Opts::Bom(args) = Opts::parse(); + setup_logging(&args)?; + let cli_config = args.as_config()?; - let manifest_path = locate_manifest(args)?; + let manifest_path = locate_manifest(&args)?; log::debug!("Found the Cargo.toml file at {}", manifest_path.display()); log::trace!("Running `cargo metadata` started"); - let metadata = get_metadata(args, &manifest_path, &cli_config)?; + let metadata = get_metadata(&args, &manifest_path, &cli_config)?; log::trace!("Running `cargo metadata` finished"); log::trace!("SBOM generation started"); let boms = SbomGenerator::create_sboms(metadata, &cli_config)?; log::trace!("SBOM generation finished"); - Ok(boms) -} - -fn main() -> anyhow::Result<()> { - let Opts::Bom(args) = Opts::parse(); - setup_logging(&args)?; - - let boms = generate_sboms(&args)?; - log::trace!("SBOM output started"); for bom in boms { bom.write_to_files()?; @@ -170,62 +163,3 @@ fn get_metadata( Ok(cmd.exec()?) } - -#[cfg(test)] -mod tests { - use cyclonedx_bom::prelude::NormalizedString; - - #[test] - fn parse_toml_only_normal() { - use crate::cli; - use crate::generate_sboms; - use clap::Parser; - use cyclonedx_bom::models::component::Scope; - use std::path::PathBuf; - - let mut test_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - test_cargo_toml.push("tests/fixtures/build_then_runtime_dep/Cargo.toml"); - - let path_arg = &format!("--manifest-path={}", test_cargo_toml.display()); - let args = ["cyclonedx", path_arg, "--only-normal-deps"]; - let args_parsed = cli::Args::parse_from(args.iter()); - - let sboms = generate_sboms(&args_parsed).unwrap(); - - let components = sboms[0].bom.components.as_ref().unwrap(); - assert!(components - .0 - .iter() - .all(|f| f.scope == Some(Scope::Required))); - } - - #[test] - fn parse_toml_with_excluded() { - use crate::cli; - use crate::generate_sboms; - use clap::Parser; - use cyclonedx_bom::models::component::Scope; - use std::path::PathBuf; - - let mut test_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - test_cargo_toml.push("tests/fixtures/build_then_runtime_dep/Cargo.toml"); - - let path_arg = &format!("--manifest-path={}", test_cargo_toml.display()); - let args = ["cyclonedx", path_arg]; - let args_parsed = cli::Args::parse_from(args.iter()); - - let sboms = generate_sboms(&args_parsed).unwrap(); - - // build_dep is a build dependency -> excluded - // runtime_dep_of_build_dep is a dependency of a build dependency -> excluded - let components = sboms[0].bom.components.as_ref().unwrap(); - assert!(components - .0 - .iter() - .all(|c| c.name != NormalizedString::new("build_dep") - || c.scope == Some(Scope::Excluded))); - assert!(components.0.iter().all(|c| c.name - != NormalizedString::new("runtime_dep_of_build_dep") - || c.scope == Some(Scope::Excluded))); - } -} diff --git a/cargo-cyclonedx/src/purl.rs b/cargo-cyclonedx/src/purl.rs index b75dfede..cb075bf4 100644 --- a/cargo-cyclonedx/src/purl.rs +++ b/cargo-cyclonedx/src/purl.rs @@ -107,7 +107,7 @@ mod tests { ) .unwrap(); // Validate that data roundtripped correctly - let parsed_purl = Purl::from_str(purl.as_ref()).unwrap(); + let parsed_purl = Purl::from_str(&purl.to_string()).unwrap(); assert_eq!(parsed_purl.name(), "aho-corasick"); assert_eq!(parsed_purl.version(), Some("1.1.2")); assert!(parsed_purl.qualifiers().is_empty()); @@ -120,7 +120,7 @@ mod tests { let git_package: Package = serde_json::from_str(GIT_PACKAGE_JSON).unwrap(); let purl = get_purl(&git_package, &git_package, Utf8Path::new("/foo/bar"), None).unwrap(); // Validate that data roundtripped correctly - let parsed_purl = Purl::from_str(purl.as_ref()).unwrap(); + let parsed_purl = Purl::from_str(&purl.to_string()).unwrap(); assert_eq!(parsed_purl.name(), "auditable-extract"); assert_eq!(parsed_purl.version(), Some("0.3.2")); assert_eq!(parsed_purl.qualifiers().len(), 1); @@ -142,7 +142,7 @@ mod tests { ) .unwrap(); // Validate that data roundtripped correctly - let parsed_purl = Purl::from_str(purl.as_ref()).unwrap(); + let parsed_purl = Purl::from_str(&purl.to_string()).unwrap(); assert_eq!(parsed_purl.name(), "cargo-cyclonedx"); assert_eq!(parsed_purl.version(), Some("0.3.8")); assert_eq!(parsed_purl.qualifiers().len(), 1); @@ -165,7 +165,7 @@ mod tests { ) .unwrap(); // Validate that data roundtripped correctly - let parsed_purl = Purl::from_str(purl.as_ref()).unwrap(); + let parsed_purl = Purl::from_str(&purl.to_string()).unwrap(); assert_eq!(parsed_purl.name(), "cargo-cyclonedx"); assert_eq!(parsed_purl.version(), Some("0.3.8")); assert_eq!(parsed_purl.qualifiers().len(), 1); @@ -189,7 +189,7 @@ mod tests { ) .unwrap(); // Validate that data roundtripped correctly - let parsed_purl = Purl::from_str(purl.as_ref()).unwrap(); + let parsed_purl = Purl::from_str(&purl.to_string()).unwrap(); assert_eq!(parsed_purl.name(), "cyclonedx-bom"); assert_eq!(parsed_purl.version(), Some("0.4.1")); assert_eq!(parsed_purl.qualifiers().len(), 1); @@ -213,7 +213,7 @@ mod tests { ) .unwrap(); // Validate that data roundtripped correctly - let parsed_purl = Purl::from_str(purl.as_ref()).unwrap(); + let parsed_purl = Purl::from_str(&purl.to_string()).unwrap(); assert_eq!(parsed_purl.name(), "cyclonedx-bom"); assert_eq!(parsed_purl.version(), Some("0.4.1")); assert_eq!(parsed_purl.qualifiers().len(), 1); diff --git a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/Cargo.toml b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/Cargo.toml deleted file mode 100644 index a1531f28..00000000 --- a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[workspace] -resolver = "2" -members = [ - "top_level_crate", - "build_dep", - "runtime_dep_of_build_dep", -] diff --git a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/build_dep/Cargo.toml b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/build_dep/Cargo.toml deleted file mode 100644 index 27cb1e9a..00000000 --- a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/build_dep/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "build_dep" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -runtime_dep_of_build_dep = {path = "../runtime_dep_of_build_dep"} diff --git a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/build_dep/src/lib.rs b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/build_dep/src/lib.rs deleted file mode 100644 index 1b4a90c9..00000000 --- a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/build_dep/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); - } -} diff --git a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_build_dep/Cargo.toml b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_build_dep/Cargo.toml deleted file mode 100644 index 6ec561dc..00000000 --- a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_build_dep/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "runtime_dep_of_build_dep" -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/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_build_dep/src/lib.rs b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_build_dep/src/lib.rs deleted file mode 100644 index 1b4a90c9..00000000 --- a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_build_dep/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); - } -} diff --git a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/top_level_crate/Cargo.toml b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/top_level_crate/Cargo.toml deleted file mode 100644 index 2beb6141..00000000 --- a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/top_level_crate/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "top_level_crate" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[build-dependencies] -build_dep = {path = "../build_dep"} diff --git a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/top_level_crate/src/main.rs b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/top_level_crate/src/main.rs deleted file mode 100644 index e7a11a96..00000000 --- a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/top_level_crate/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -}