From b2d08045e459dcb21285d2e1490666024d3e2722 Mon Sep 17 00:00:00 2001 From: Mathew Horner Date: Thu, 27 Mar 2025 12:37:31 -0500 Subject: [PATCH 1/3] version and purl fields in components list are now optional in CycloneDX SBOMs. --- CHANGELOG.md | 4 ++ lockfile/src/cyclonedx.rs | 86 ++++++++++++++++++++++++++------------- 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad9571c7e..d436cadf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased +### Changed + +- `version` and `purl` fields in `components` list are now optional in CycloneDX SBOMs. + ## 7.4.0 - 2025-03-20 ### Added diff --git a/lockfile/src/cyclonedx.rs b/lockfile/src/cyclonedx.rs index c59d616b5..f5a699631 100644 --- a/lockfile/src/cyclonedx.rs +++ b/lockfile/src/cyclonedx.rs @@ -12,8 +12,7 @@ use crate::{determine_package_version, formatted_package_name, Package, Parse, U /// Define the generic trait for components. trait Component { fn component_type(&self) -> &str; - fn name(&self) -> &str; - fn version(&self) -> &str; + fn version(&self) -> Option<&str>; fn scope(&self) -> Option<&str>; fn purl(&self) -> Option<&str>; fn components(&self) -> Option<&[Self]> @@ -40,8 +39,9 @@ struct Components { struct XmlComponent { #[serde(rename = "@type")] component_type: String, - name: String, - version: String, + #[serde(rename = "name")] + _name: String, + version: Option, scope: Option, purl: Option, components: Option>, @@ -52,12 +52,8 @@ impl Component for XmlComponent { &self.component_type } - fn name(&self) -> &str { - &self.name - } - - fn version(&self) -> &str { - &self.version + fn version(&self) -> Option<&str> { + self.version.as_deref() } fn scope(&self) -> Option<&str> { @@ -78,8 +74,9 @@ impl Component for XmlComponent { struct JsonComponent { #[serde(rename = "type")] component_type: String, - name: String, - version: String, + #[serde(rename = "name")] + _name: String, + version: Option, scope: Option, purl: Option, #[serde(default)] @@ -91,12 +88,8 @@ impl Component for JsonComponent { &self.component_type } - fn name(&self) -> &str { - &self.name - } - - fn version(&self) -> &str { - &self.version + fn version(&self) -> Option<&str> { + self.version.as_deref() } fn scope(&self) -> Option<&str> { @@ -140,11 +133,12 @@ fn filter_components(components: &[T]) -> impl Iterator(component: &T) -> anyhow::Result { - let purl_str = component - .purl() - .ok_or_else(|| anyhow!("Missing purl for {}:{}", component.name(), component.version()))?; - let purl = GenericPurl::::from_str(purl_str)?; +fn from_purl(component: &T) -> anyhow::Result> { + let purl = match component.purl() { + Some(purl) => purl, + None => return Ok(None), + }; + let purl = GenericPurl::::from_str(purl)?; let package_type = PackageType::from_str(purl.package_type()).map_err(|_| UnknownEcosystem)?; // Determine the package name based on its type and namespace. @@ -159,7 +153,7 @@ fn from_purl(component: &T) -> anyhow::Result { // Use the qualifiers from the PURL to determine the version details. let version = determine_package_version(pkg_version, &purl); - Ok(Package { name, version, package_type }) + Ok(Some(Package { name, version, package_type })) } pub struct CycloneDX; @@ -169,6 +163,7 @@ impl CycloneDX { let comp = components.unwrap_or_default(); let packages = filter_components(comp) .map(from_purl) + .flat_map(Result::transpose) .filter(|r| !r.as_ref().is_err_and(|e| e.is::())) .collect::>>()?; Ok(packages) @@ -278,8 +273,8 @@ mod tests { fn test_ignore_unsupported_ecosystem() { let ignored_component = JsonComponent { component_type: "library".into(), - name: "adduser".into(), - version: "3.118ubuntu5".into(), + _name: "adduser".into(), + version: Some("3.118ubuntu5".into()), scope: None, purl: Some("pkg:deb/ubuntu/adduser@3.118ubuntu5?arch=all&distro=ubuntu-22.04".into()), components: vec![], @@ -287,8 +282,8 @@ mod tests { let component = JsonComponent { component_type: "library".into(), - name: "abbrev".into(), - version: "1.1.1".into(), + _name: "abbrev".into(), + version: Some("1.1.1".into()), scope: None, purl: Some("pkg:npm/abbrev@1.1.1".into()), components: vec![], @@ -308,4 +303,39 @@ mod tests { assert!(packages.len() == 1); assert_eq!(packages[0], expected_package); } + + #[test] + fn test_ignore_missing_purl() { + let ignored_component = JsonComponent { + component_type: "library".into(), + _name: "some-package-1".into(), + version: Some("1.0.0".into()), + scope: None, + purl: None, + components: vec![], + }; + + let component = JsonComponent { + component_type: "library".into(), + _name: "some-package-2".into(), + version: Some("2.0.0".into()), + scope: None, + purl: Some("pkg:npm/some-package-2@2.0.0".into()), + components: vec![], + }; + + let expected_package = Package { + name: "some-package-2".into(), + version: PackageVersion::FirstParty("2.0.0".into()), + package_type: PackageType::Npm, + }; + + let bom: Bom> = + Bom { components: Some(vec![component, ignored_component]) }; + + let packages = CycloneDX::process_components(bom.components.as_deref()).unwrap(); + + assert!(packages.len() == 1); + assert_eq!(packages[0], expected_package); + } } From 8b91feadf59eebc82f434242160c1fecdfe035f9 Mon Sep 17 00:00:00 2001 From: Mathew Horner Date: Thu, 27 Mar 2025 13:33:43 -0500 Subject: [PATCH 2/3] Update CHANGELOG.md Co-authored-by: Christian Duerr <102963075+cd-work@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d436cadf7..67915f38b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed -- `version` and `purl` fields in `components` list are now optional in CycloneDX SBOMs. +- `version` and `purl` fields in `components` list are now optional in CycloneDX SBOMs ## 7.4.0 - 2025-03-20 From 08e8412fc060c75664e39144a45b6eb9a9a08db3 Mon Sep 17 00:00:00 2001 From: Mathew Horner Date: Thu, 27 Mar 2025 15:33:02 -0500 Subject: [PATCH 3/3] Remove _name --- lockfile/src/cyclonedx.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lockfile/src/cyclonedx.rs b/lockfile/src/cyclonedx.rs index f5a699631..10ed8d852 100644 --- a/lockfile/src/cyclonedx.rs +++ b/lockfile/src/cyclonedx.rs @@ -39,8 +39,6 @@ struct Components { struct XmlComponent { #[serde(rename = "@type")] component_type: String, - #[serde(rename = "name")] - _name: String, version: Option, scope: Option, purl: Option, @@ -74,8 +72,6 @@ impl Component for XmlComponent { struct JsonComponent { #[serde(rename = "type")] component_type: String, - #[serde(rename = "name")] - _name: String, version: Option, scope: Option, purl: Option, @@ -273,7 +269,6 @@ mod tests { fn test_ignore_unsupported_ecosystem() { let ignored_component = JsonComponent { component_type: "library".into(), - _name: "adduser".into(), version: Some("3.118ubuntu5".into()), scope: None, purl: Some("pkg:deb/ubuntu/adduser@3.118ubuntu5?arch=all&distro=ubuntu-22.04".into()), @@ -282,7 +277,6 @@ mod tests { let component = JsonComponent { component_type: "library".into(), - _name: "abbrev".into(), version: Some("1.1.1".into()), scope: None, purl: Some("pkg:npm/abbrev@1.1.1".into()), @@ -308,7 +302,6 @@ mod tests { fn test_ignore_missing_purl() { let ignored_component = JsonComponent { component_type: "library".into(), - _name: "some-package-1".into(), version: Some("1.0.0".into()), scope: None, purl: None, @@ -317,7 +310,6 @@ mod tests { let component = JsonComponent { component_type: "library".into(), - _name: "some-package-2".into(), version: Some("2.0.0".into()), scope: None, purl: Some("pkg:npm/some-package-2@2.0.0".into()),