Skip to content

Commit edf887f

Browse files
version and purl fields in components list are now optional in CycloneDX SBOMs. (#1595)
* version and purl fields in components list are now optional in CycloneDX SBOMs. * Update CHANGELOG.md Co-authored-by: Christian Duerr <[email protected]> * Remove _name --------- Co-authored-by: Christian Duerr <[email protected]>
1 parent c8be0f5 commit edf887f

File tree

2 files changed

+54
-28
lines changed

2 files changed

+54
-28
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
88

99
## Unreleased
1010

11+
### Changed
12+
13+
- `version` and `purl` fields in `components` list are now optional in CycloneDX SBOMs
14+
1115
## 7.4.0 - 2025-03-20
1216

1317
### Added

lockfile/src/cyclonedx.rs

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ use crate::{determine_package_version, formatted_package_name, Package, Parse, U
1212
/// Define the generic trait for components.
1313
trait Component {
1414
fn component_type(&self) -> &str;
15-
fn name(&self) -> &str;
16-
fn version(&self) -> &str;
15+
fn version(&self) -> Option<&str>;
1716
fn scope(&self) -> Option<&str>;
1817
fn purl(&self) -> Option<&str>;
1918
fn components(&self) -> Option<&[Self]>
@@ -40,8 +39,7 @@ struct Components<T> {
4039
struct XmlComponent {
4140
#[serde(rename = "@type")]
4241
component_type: String,
43-
name: String,
44-
version: String,
42+
version: Option<String>,
4543
scope: Option<String>,
4644
purl: Option<String>,
4745
components: Option<Components<XmlComponent>>,
@@ -52,12 +50,8 @@ impl Component for XmlComponent {
5250
&self.component_type
5351
}
5452

55-
fn name(&self) -> &str {
56-
&self.name
57-
}
58-
59-
fn version(&self) -> &str {
60-
&self.version
53+
fn version(&self) -> Option<&str> {
54+
self.version.as_deref()
6155
}
6256

6357
fn scope(&self) -> Option<&str> {
@@ -78,8 +72,7 @@ impl Component for XmlComponent {
7872
struct JsonComponent {
7973
#[serde(rename = "type")]
8074
component_type: String,
81-
name: String,
82-
version: String,
75+
version: Option<String>,
8376
scope: Option<String>,
8477
purl: Option<String>,
8578
#[serde(default)]
@@ -91,12 +84,8 @@ impl Component for JsonComponent {
9184
&self.component_type
9285
}
9386

94-
fn name(&self) -> &str {
95-
&self.name
96-
}
97-
98-
fn version(&self) -> &str {
99-
&self.version
87+
fn version(&self) -> Option<&str> {
88+
self.version.as_deref()
10089
}
10190

10291
fn scope(&self) -> Option<&str> {
@@ -140,11 +129,12 @@ fn filter_components<T: Component>(components: &[T]) -> impl Iterator<Item = &'_
140129
}
141130

142131
/// Convert a component's package URL (PURL) into a package object.
143-
fn from_purl<T: Component>(component: &T) -> anyhow::Result<Package> {
144-
let purl_str = component
145-
.purl()
146-
.ok_or_else(|| anyhow!("Missing purl for {}:{}", component.name(), component.version()))?;
147-
let purl = GenericPurl::<String>::from_str(purl_str)?;
132+
fn from_purl<T: Component>(component: &T) -> anyhow::Result<Option<Package>> {
133+
let purl = match component.purl() {
134+
Some(purl) => purl,
135+
None => return Ok(None),
136+
};
137+
let purl = GenericPurl::<String>::from_str(purl)?;
148138
let package_type = PackageType::from_str(purl.package_type()).map_err(|_| UnknownEcosystem)?;
149139

150140
// Determine the package name based on its type and namespace.
@@ -159,7 +149,7 @@ fn from_purl<T: Component>(component: &T) -> anyhow::Result<Package> {
159149
// Use the qualifiers from the PURL to determine the version details.
160150
let version = determine_package_version(pkg_version, &purl);
161151

162-
Ok(Package { name, version, package_type })
152+
Ok(Some(Package { name, version, package_type }))
163153
}
164154

165155
pub struct CycloneDX;
@@ -169,6 +159,7 @@ impl CycloneDX {
169159
let comp = components.unwrap_or_default();
170160
let packages = filter_components(comp)
171161
.map(from_purl)
162+
.flat_map(Result::transpose)
172163
.filter(|r| !r.as_ref().is_err_and(|e| e.is::<UnknownEcosystem>()))
173164
.collect::<anyhow::Result<Vec<_>>>()?;
174165
Ok(packages)
@@ -278,17 +269,15 @@ mod tests {
278269
fn test_ignore_unsupported_ecosystem() {
279270
let ignored_component = JsonComponent {
280271
component_type: "library".into(),
281-
name: "adduser".into(),
282-
version: "3.118ubuntu5".into(),
272+
version: Some("3.118ubuntu5".into()),
283273
scope: None,
284274
purl: Some("pkg:deb/ubuntu/[email protected]?arch=all&distro=ubuntu-22.04".into()),
285275
components: vec![],
286276
};
287277

288278
let component = JsonComponent {
289279
component_type: "library".into(),
290-
name: "abbrev".into(),
291-
version: "1.1.1".into(),
280+
version: Some("1.1.1".into()),
292281
scope: None,
293282
purl: Some("pkg:npm/[email protected]".into()),
294283
components: vec![],
@@ -308,4 +297,37 @@ mod tests {
308297
assert!(packages.len() == 1);
309298
assert_eq!(packages[0], expected_package);
310299
}
300+
301+
#[test]
302+
fn test_ignore_missing_purl() {
303+
let ignored_component = JsonComponent {
304+
component_type: "library".into(),
305+
version: Some("1.0.0".into()),
306+
scope: None,
307+
purl: None,
308+
components: vec![],
309+
};
310+
311+
let component = JsonComponent {
312+
component_type: "library".into(),
313+
version: Some("2.0.0".into()),
314+
scope: None,
315+
purl: Some("pkg:npm/[email protected]".into()),
316+
components: vec![],
317+
};
318+
319+
let expected_package = Package {
320+
name: "some-package-2".into(),
321+
version: PackageVersion::FirstParty("2.0.0".into()),
322+
package_type: PackageType::Npm,
323+
};
324+
325+
let bom: Bom<Vec<JsonComponent>> =
326+
Bom { components: Some(vec![component, ignored_component]) };
327+
328+
let packages = CycloneDX::process_components(bom.components.as_deref()).unwrap();
329+
330+
assert!(packages.len() == 1);
331+
assert_eq!(packages[0], expected_package);
332+
}
311333
}

0 commit comments

Comments
 (0)