merging multiple CycloneDX documents #320
Replies: 16 comments 34 replies
This comment has been minimized.
This comment has been minimized.
-
re: #320 (comment)
|
Beta Was this translation helpful? Give feedback.
-
re: #320 (comment)
|
Beta Was this translation helpful? Give feedback.
-
re: #320 (comment)
|
Beta Was this translation helpful? Give feedback.
-
re: #320 (comment)
|
Beta Was this translation helpful? Give feedback.
-
re: #320 (comment)
Some precedents from experience in the field:
|
Beta Was this translation helpful? Give feedback.
-
re: #320 (comment)
|
Beta Was this translation helpful? Give feedback.
-
re: #320 (comment)
|
Beta Was this translation helpful? Give feedback.
-
re: #320 (comment)
|
Beta Was this translation helpful? Give feedback.
-
My humble request is that the specification be un-opinionated. Merging strategies require assumptions and positions, so best to leave it to the individual cli and tools for implementation. |
Beta Was this translation helpful? Give feedback.
This comment has been hidden.
This comment has been hidden.
-
In terms of use-cases, this is what I'm using BOM merges for: Hierarchical mergeI have BOMs for a couple of components which I'm aggregating, and thus, the new BOM describes the aggregate and has the other components as dependencies. Flat mergeI have a BOM for component A, but the BOM is incomplete, thus I want to expand it using information from other BOMs (i.e. BOM editing). Examples
In many cases, I used the flat merge, but then I actually had to modify the dependencies manually. (As written previously, for me the sematics for the hierarchical merge are quite clear, but for the flat merge this is not true.) What are your use cases? |
Beta Was this translation helpful? Give feedback.
-
❗ ❗ ❗ ❗ ❗ The following is a (probably outdated) unfinished and unpublished pamphlet "Standardization of BOM merging algorithms" - is still work in progress; the original authors agreed to publish the snapshot here. ❗ ❗ ❗ ❗ ❗ Meta
SummaryThis is a proposal for a set of standardized merging algorithms for CycloneDX Bill of Materials. MotivationWhy should we do this?With SBOM adoption ramping up, a lot of use cases have surfaced that require merging multiple BOMs into one. While it's trivial to merge BOM objects on a technical level, it may introduce inconsistencies or cause ambiguities. We need one or more merging algorithms that we consider to be correct and accurate. Another factor is that we offer tooling for SBOM generation and transformation, as well as libraries for multiple ecosystems. When it comes to functionalities like merging, it's important that our offering behaves the same across our portfolio, and produces consitent results. What use cases does this support?Use case 1I have a web app. It has NPM packages for client side JavaScript, and NuGet When I'm generating the SBOM for it, I need to use the CycloneDX Node implementation Use case 2I have a web app. It has NPM packages for a SPA frontend, NPM packages for the I want to generate a single SBOM that retains the individual system components. Each of these components has a different threat model. The same vulnerability for Additionally, each of these components is managed by a different team. Use case 3I've gone all in on micro-services. I now have so many micro-services I don't I want a view of my entire product and the dependency relationships (services) Each micro-service team generates a new SBOM for each build. I want an SBOM that represents all the individual micro-services in our What is the expected outcome?
What it isThis RFC proposes three merging algorithms, each of which targets a different use case.
How it WorksOverviewIn general, merging only affects top-level collections, not the elements within them. Merge functions adhere to the following signature: def merge(subject: Optional[Component], boms: Sequence[BOM]) -> BOM
Further, the following constraints apply:
BOM reference namespacingNamespacing is done to indicate the origin of a component or service, as well as to prevent reference collisions. See #7 for the namespace format definition. <!-- Not namespaced -->
<component bom-ref="pkg:maven/com.acme/acme-lib@1.2.3" type="library">
<!-- Namespaced -->
<component bom-ref="urn:cyclonedx:a48a8f21-afe9-4cde-bb43-9c61c6409927/3#pkg%3Amaven%2Fcom.acme%2Facme-lib%401.2.3" type="library"> The following constraints apply to namespacing:
AlgorithmsThe algorithms in this document are written in Python for readability's sake. ReproducabilityIn order to make results reproducible, merge algorithms are subject to the following additional constraints:
NamespacingTo accomodate for the namespacing requirements detailed above, merge algorithms make use def namespace(bom_ref: Optional[str], src: BOM) -> Optional[str]:
"""Namespace a BOM reference."""
pass
def namespace_component(component: Component, src: BOM) -> None:
"""Namespace a component's BOM reference (and that of its sub-components)."""
pass
def namespace_service(service: Service, src: BOM) -> None:
"""Namespace a component's BOM reference (and that of its sub-services)."""
pass
def namespace_dependency(dependency: Dependency, src: BOM) -> None:
"""Namespace a dependency's BOM reference (and that of its dependencies)."""
pass
def namespace_composition(composition: Composition, src: BOM) -> None:
"""Namespace a composition's BOM references."""
pass Flat mergeDescriptionThe flat merge algorithm creates an aggregate of all elements within the provided BOMs. Merged elements
Codedef flat_merge(subject: Optional[Component], boms: Sequence[BOM]) -> BOM:
if len(boms) < 2:
raise MergeError("merging requires at least two boms")
for bom in boms:
if not bom:
raise MergeError("bom must not be None")
if not bom.serial_number:
raise MergeError("bom must have a serial number")
tools: List[Tool] = []
metadata_properties: List[Property] = []
components: List[Component] = []
services: List[Service] = []
external_references: List[ExternalReference] = []
dependencies: List[Dependency] = []
compositions: List[Composition] = []
properties: List[Property] = []
subject_dependencies: List[Dependency] = []
for bom in boms:
if bom.metadata:
if bom.metadata.tools:
tools.extend(bom.metadata.tools)
if bom.metadata.component:
component = bom.metadata.component
namespace_component(component, bom)
components.append(component)
if subject.bom_ref and component.bom_ref:
subject_dependencies.append(Dependency(ref=component.bom_ref))
if bom.metadata.properties:
metadata_properties.extend(bom.metadata.properties)
if bom.components:
for component in bom.components:
namespace_component(component, bom)
components.append(component)
if bom.services:
for service in bom.services:
namespace_service(service, bom)
services.append(service)
if bom.external_references:
external_references.extend(bom.external_references)
if bom.dependencies:
for dependency in bom.dependencies:
namespace_dependency(dependency, bom)
dependencies.append(dependency)
if bom.compositions:
for composition in bom.compositions:
namespace_composition(composition, bom)
compositions.append(composition)
if bom.properties:
properties.extend(bom.properties)
if subject and subject_dependencies:
dependencies.append(Dependency(
ref=subject.bom_ref,
dependencies=subject_dependencies
))
result = BOM()
result.metadata = Metadata(
tools=tools,
component=subject,
properties=metadata_properties
)
result.components = components
result.services = services
result.external_references = external_references
result.dependencies = dependencies
result.compositions = compositions
result.properties = properties
return result Hierarchical mergeDescriptionThe hierarchical merge algorithm mostly behaves just like the flat merge, with the exception of components: Merged elements
Codedef hierarchical_merge(subject: Optional[Component], boms: Sequence[BOM]) -> BOM:
if len(boms) < 2:
raise MergeError("merging requires at least two boms")
for bom in boms:
if not bom:
raise MergeError("bom must not be None")
if not bom.serial_number:
raise MergeError("bom must have a serial number")
if not bom.metadata or not bom.metadata.component:
raise MergeError("bom must have a top-level component")
tools: List[Tool] = []
metadata_properties: List[Property] = []
components: List[Component] = []
services: List[Service] = []
externalReferences: List[ExternalReference] = []
dependencies: List[Dependency] = []
compositions: List[Composition] = []
properties: List[Property] = []
subject_dependencies: List[Dependency] = []
for bom in boms:
if bom.metadata.tools:
tools.extend(bom.metadata.tools)
if bom.metadata.properties:
metadata_properties.extend(bom.metadata.properties)
main = bom.metadata.component
if bom.components:
main.components.extend(component)
namespace_component(main, bom)
components.append(main)
if subject.bom_ref and main.bom_ref:
subject_dependencies.append(Dependency(ref=main.bom_ref))
if bom.services:
for service in bom.services:
namespace_service(service, bom)
services.append(service)
if bom.external_references:
external_references.extend(bom.external_references)
if bom.dependencies:
for dependency in bom.dependencies:
namespace_dependency(dependency, bom)
dependencies.append(dependency)
if bom.compositions:
for composition in bom.compositions:
namespace_composition(composition, bom)
compositions.append(composition)
if bom.properties:
properties.extend(bom.properties)
if subject and subject_dependencies:
dependencies.append(Dependency(
ref=subject.bom_ref,
dependencies=subject_dependencies
))
result = BOM()
result.metadata = Metadata(
tools=tools,
component=subject,
properties=metadata_properties
)
result.components = components
result.services = services
result.external_references = external_references
result.dependencies = dependencies
result.compositions = compositions
result.properties = properties
return result Reference mergeDescriptionThe reference merge extracts the top-level component of each provided BOM, An Fields of the components that are not essential to identifying them are stripped.
This is done to reduce redundancies, as well as ensuring consiceness of the resulting BOM. TODO: Should external references have BOM versions and hashes? Merged elements
Codedef reference_merge(subject: Optional[Component], boms: Sequence[BOM]) -> BOM:
if len(boms) < 2:
raise MergeError("merging requires at least two boms")
for bom in boms:
if not bom:
raise MergeError("bom must not be None")
if not bom.serial_number:
raise MergeError("bom must have a serial number")
if not bom.metadata or not bom.metadata.component:
raise MergeError("bom must have a top-level component")
components: List[Component] = []
subject_dependencies: List[Dependency] = []
for bom in boms:
component = Component(
bom_ref=namespace(bom.metadata.component.bom_ref, bom),
type=bom.metadata.component.type,
group=bom.metadata.component.group,
name=bom.metadata.component.name,
version=bom.metadata.component.version,
external_references = [
ExternalReference(type="bom", url=bom.serial_number)
]
)
components.append(component)
if subject.bom_ref and component.bom_ref:
subject_dependencies.append(Dependency(ref=component.bom_ref))
if subject and subject_dependencies:
dependencies.append(Dependency(
ref=subject.bom_ref,
dependencies=subject_dependencies
))
dependencies.extend(subject_dependencies)
result = BOM()
result.metadata = Metadata(component=subject)
result.components = components
result.dependencies = dependencies
return result ExamplesInputsTo keep this document manageable, we're assuming a fixed set of inputs for all merge operations. BOM AThis BOM describes the Java application <?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:c337a0f7-82b6-49bf-abd2-95e37c7476c8" version="1">
<metadata>
<tools>
<tool>
<name>cyclonedx-maven-plugin</name>
<version>2.5.3</version>
</tool>
</tools>
<component bom-ref="pkg:maven/com.acme/acme-backend@1.0.0" type="application">
<group>com.acme</group>
<name>acme-backend</name>
<version>1.0.0</version>
</component>
</metadata>
<components>
<component bom-ref="pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.12.4" type="library">
<group>com.fasterxml.jackson.core</group>
<name>jackson-databind</name>
<version>2.12.4</version>
</component>
</components>
<dependencies>
<dependency ref="pkg:maven/com.acme/acme-backend@1.0.0">
<dependency ref="pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.12.4"/>
</dependency>
<dependency ref="pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.12.4"/>
</dependencies>
</bom> BOM BThis BOM describes the JavaScript application <?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:12151f5b-4001-4835-a734-42fafde0a852" version="1">
<metadata>
<tools>
<tool>
<group>@cyclonedx</group>
<name>bom</name>
<version>v3.1.1</version>
</tool>
</tools>
<component bom-ref="pkg:npm/%40acme/acme-frontend@1.0.0" type="application">
<group>@acme</group>
<name>acme-frontend</name>
<version>1.0.0</version>
</component>
</metadata>
<components>
<component bom-ref="pkg:npm/vue@2.6.12" type="library">
<name>vue</name>
<version>2.6.12</version>
</component>
</components>
<dependencies>
<dependency ref="pkg:npm/%40acme/acme-frontend@1.0.0">
<dependency ref="pkg:npm/vue@2.6.12"/>
</dependency>
<dependency ref="pkg:npm/vue@2.6.12"/>
</dependencies>
</bom> SubjectThis component describes the top-level component <component bom-ref="com.acme/acme-app@1.0.0" type="application">
<group>com.acme</group>
<name>acme-app</name>
<version>1.0.0</version>
</component> Flat merge<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:f120a825-fb11-4fe6-a6cd-f8a6a3874756" version="1">
<metadata>
<tools>
<tool>
<name>cyclonedx-maven-plugin</name>
<version>2.5.3</version>
</tool>
<tool>
<group>@cyclonedx</group>
<name>bom</name>
<version>v3.1.1</version>
</tool>
</tools>
<component bom-ref="com.acme/acme-app@1.0.0" type="application">
<group>com.acme</group>
<name>acme-app</name>
<version>1.0.0</version>
</component>
</metadata>
<components>
<component bom-ref="urn:cyclonedx:c337a0f7-82b6-49bf-abd2-95e37c7476c8#pkg:maven%2Fcom.acme%2Facme-backend%401.0.0" type="application">
<group>com.acme</group>
<name>acme-backend</name>
<version>1.0.0</version>
</component>
<component bom-ref="urn:cyclonedx:c337a0f7-82b6-49bf-abd2-95e37c7476c8#pkg:maven%2Fcom.fasterxml.jackson.core%2Fjackson-databind%402.12.4" type="library">
<group>com.fasterxml.jackson.core</group>
<name>jackson-databind</name>
<version>2.12.4</version>
</component>
<component bom-ref="urn:cyclonedx:12151f5b-4001-4835-a734-42fafde0a852#pkg:npm%2F%40acme%2Facme-frontend%401.0.0" type="application">
<group>@acme</group>
<name>acme-frontend</name>
<version>1.0.0</version>
</component>
<component bom-ref="urn:cyclonedx:12151f5b-4001-4835-a734-42fafde0a852#pkg:npm%2Fvue%402.6.12" type="library">
<name>vue</name>
<version>2.6.12</version>
</component>
</components>
<dependencies>
<dependency ref="com.acme/acme-app@1.0.0">
<dependency ref="urn:cyclonedx:c337a0f7-82b6-49bf-abd2-95e37c7476c8#pkg:maven%2Fcom.acme%2Facme-backend%401.0.0"/>
<dependency ref="urn:cyclonedx:12151f5b-4001-4835-a734-42fafde0a852#pkg:npm%2F%40acme%2Facme-frontend%401.0.0"/>
</dependency>
<dependency ref="urn:cyclonedx:c337a0f7-82b6-49bf-abd2-95e37c7476c8#pkg:maven%2Fcom.acme%2Facme-backend%401.0.0">
<dependency ref="urn:cyclonedx:c337a0f7-82b6-49bf-abd2-95e37c7476c8#pkg:maven%2Fcom.fasterxml.jackson.core%2Fjackson-databind%402.12.4"/>
</dependency>
<dependency ref="urn:cyclonedx:c337a0f7-82b6-49bf-abd2-95e37c7476c8#pkg:maven%2Fcom.fasterxml.jackson.core%2Fjackson-databind%402.12.4"/>
<dependency ref="urn:cyclonedx:12151f5b-4001-4835-a734-42fafde0a852#pkg:npm%2F%40acme%2Facme-frontend%401.0.0">
<dependency ref="urn:cyclonedx:12151f5b-4001-4835-a734-42fafde0a852#pkg:npm%2Fvue%402.6.12"/>
</dependency>
<dependency ref="urn:cyclonedx:12151f5b-4001-4835-a734-42fafde0a852#pkg:npm%2Fvue%402.6.12"/>
</dependencies>
</bom> Hierarchical merge<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:f120a825-fb11-4fe6-a6cd-f8a6a3874756" version="1">
<metadata>
<tools>
<tool>
<name>cyclonedx-maven-plugin</name>
<version>2.5.3</version>
</tool>
<tool>
<group>@cyclonedx</group>
<name>bom</name>
<version>v3.1.1</version>
</tool>
</tools>
<component bom-ref="com.acme/acme-app@1.0.0" type="application">
<group>com.acme</group>
<name>acme-app</name>
<version>1.0.0</version>
</component>
</metadata>
<components>
<component bom-ref="urn:cyclonedx:c337a0f7-82b6-49bf-abd2-95e37c7476c8#pkg:maven%2Fcom.acme%2Facme-backend%401.0.0" type="application">
<group>com.acme</group>
<name>acme-backend</name>
<version>1.0.0</version>
<components>
<component bom-ref="urn:cyclonedx:c337a0f7-82b6-49bf-abd2-95e37c7476c8#pkg:maven%2Fcom.fasterxml.jackson.core%2Fjackson-databind%402.12.4" type="library">
<group>com.fasterxml.jackson.core</group>
<name>jackson-databind</name>
<version>2.12.4</version>
</component>
</components>
</component>
<component bom-ref="urn:cyclonedx:12151f5b-4001-4835-a734-42fafde0a852#pkg:npm%2F%40acme%2Facme-frontend%401.0.0" type="application">
<group>@acme</group>
<name>acme-frontend</name>
<version>1.0.0</version>
<components>
<component bom-ref="urn:cyclonedx:12151f5b-4001-4835-a734-42fafde0a852#pkg:npm%2Fvue%402.6.12" type="library">
<name>vue</name>
<version>2.6.12</version>
</component>
</components>
</component>
</components>
<dependencies>
<dependency ref="com.acme/acme-app@1.0.0">
<dependency ref="urn:cyclonedx:c337a0f7-82b6-49bf-abd2-95e37c7476c8#pkg:maven%2Fcom.acme%2Facme-backend%401.0.0"/>
<dependency ref="urn:cyclonedx:12151f5b-4001-4835-a734-42fafde0a852#pkg:npm%2F%40acme%2Facme-frontend%401.0.0"/>
</dependency>
<dependency ref="urn:cyclonedx:c337a0f7-82b6-49bf-abd2-95e37c7476c8#pkg:maven%2Fcom.acme%2Facme-backend%401.0.0">
<dependency ref="urn:cyclonedx:c337a0f7-82b6-49bf-abd2-95e37c7476c8#pkg:maven%2Fcom.fasterxml.jackson.core%2Fjackson-databind%402.12.4"/>
</dependency>
<dependency ref="urn:cyclonedx:c337a0f7-82b6-49bf-abd2-95e37c7476c8#pkg:maven%2Fcom.fasterxml.jackson.core%2Fjackson-databind%402.12.4"/>
<dependency ref="urn:cyclonedx:12151f5b-4001-4835-a734-42fafde0a852#pkg:npm%2F%40acme%2Facme-frontend%401.0.0">
<dependency ref="urn:cyclonedx:12151f5b-4001-4835-a734-42fafde0a852#pkg:npm%2Fvue%402.6.12"/>
</dependency>
<dependency ref="urn:cyclonedx:12151f5b-4001-4835-a734-42fafde0a852#pkg:npm%2Fvue%402.6.12"/>
</dependencies>
</bom> Reference merge<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:f120a825-fb11-4fe6-a6cd-f8a6a3874756" version="1">
<metadata>
<component bom-ref="com.acme/acme-app@1.0.0" type="application">
<group>com.acme</group>
<name>acme-app</name>
<version>1.0.0</version>
</component>
</metadata>
<components>
<component bom-ref="urn:cyclonedx:c337a0f7-82b6-49bf-abd2-95e37c7476c8#pkg:maven%2Fcom.acme%2Facme-backend%401.0.0" type="application">
<group>com.acme</group>
<name>acme-backend</name>
<version>1.0.0</version>
<externalReferences>
<reference type="bom">
<url>urn:uuid:c337a0f7-82b6-49bf-abd2-95e37c7476c8</url>
</reference>
</externalReferences>
</component>
<component bom-ref="urn:cyclonedx:12151f5b-4001-4835-a734-42fafde0a852#pkg:npm%2F%40acme%2Facme-frontend%401.0.0" type="application">
<group>@acme</group>
<name>acme-frontend</name>
<version>1.0.0</version>
<externalReferences>
<reference type="bom">
<url>urn:uuid:12151f5b-4001-4835-a734-42fafde0a852</url>
</reference>
</externalReferences>
</component>
</components>
<dependencies>
<dependency ref="com.acme/acme-app@1.0.0">
<dependency ref="urn:cyclonedx:c337a0f7-82b6-49bf-abd2-95e37c7476c8#pkg:maven%2Fcom.acme%2Facme-backend%401.0.0"/>
<dependency ref="urn:cyclonedx:12151f5b-4001-4835-a734-42fafde0a852#pkg:npm%2F%40acme%2Facme-frontend%401.0.0"/>
</dependency>
<dependency ref="urn:cyclonedx:c337a0f7-82b6-49bf-abd2-95e37c7476c8#pkg:maven%2Fcom.acme%2Facme-backend%401.0.0"/>
<dependency ref="urn:cyclonedx:12151f5b-4001-4835-a734-42fafde0a852#pkg:npm%2F%40acme%2Facme-frontend%401.0.0"/>
</dependencies>
</bom> Prior Art
|
Beta Was this translation helpful? Give feedback.
-
@jkowalleck it seems that the examples don't show scenario where you have different versions of same dependency in different BOMs, to me this is the biggest issue and I don't think any of the solutions will work without losing data or duplicating the information |
Beta Was this translation helpful? Give feedback.
-
Regarding the bit on dynamic data addition, in my PRs I've explored logic
(in helper methods so potentially optional) which:
* updates document timestamp - it is neither of the originals' but one of
the product;
* generates doc uuid (ditto);
* adds cyclonedx-cli and lib as tools which generated the merged document.
At least, these theses can be defended :)
|
Beta Was this translation helpful? Give feedback.
-
Regarding parallelism and ordering - it depends. One of described use-cases
is to plaster licensing info based on the audience of the sold product. If
the merge strategy option is to replace these (not to have both possibly
present original licenses in the result), surely only a specific one of two
should remain.
|
Beta Was this translation helpful? Give feedback.
-
This is an OPEN DISCUSSION on how to merge two CycloneDX documents.
This should help understand edge cases, expectations and constraints.
General and specific questions are welcome. Please refrain from discussing (language/framework) specific implementation details or runtime goals.
Previous work and existing art
documents
You want to add one? request @CycloneDX/core-team to to so
implementations
CycloneDX/cyclonedx-cli
merge-commandCycloneDX.Utils/Merge.cs
SBOMMerge
toolCycloneDX/cyclonedx-core-java
sbomasm
You want to add one? request @CycloneDX/core-team to to so
Beta Was this translation helpful? Give feedback.
All reactions