Skip to content

Merging objects with non-unique bom_refs silently introduces data inconsistency #807

Closed

Description

@wkoot

NB: this is an extension of #540 and #677

Given the testdata in #677, set up a test for #540 but with clashing bom_refs and unique names instead:

>>> from json import load
>>> from cyclonedx.model.bom import Bom
>>> from cyclonedx.model.component import Component, ComponentType
>>> root_component = Component(
...     type=ComponentType.FRAMEWORK,
...     name="root 42",
...     version="42",
...     bom_ref="root",
... )
>>>
>>> bom = Bom()
>>> bom.metadata.component = root_component
>>> bom.validate()
True
>>> len(bom.components)
0
>>> len(bom.dependencies)
1
>>>
>>> with open('out.json', mode="r") as testfile:
...  json_bom = load(testfile)
...
>>> len(json_bom['components'])
4
>>> len(json_bom['dependencies'])
3
>>>
>>> loaded_bom = Bom.from_json(json_bom)
>>> loaded_bom.validate()
True
>>> len(loaded_bom.components)
4
>>> len(loaded_bom.dependencies)
5
>>>
>>> loaded_bom.metadata.component
<Component bom-ref=<BomRef 'root' id=139844009673936>, group=None, name=test, version=None, type=ComponentType.APPLICATION>
>>> bom.metadata.component
<Component bom-ref=<BomRef 'root' id=139844025859088>, group=None, name=root 42, version=42, type=ComponentType.FRAMEWORK>
>>>
>>> loaded_bom.metadata.component.bom_ref
<BomRef 'root' id=139844009673936>
>>> bom.metadata.component.bom_ref
<BomRef 'root' id=139844025859088>
>>>
>>> bom.metadata.component.bom_ref == loaded_bom.metadata.component.bom_ref
True
>>> loaded_bom.metadata.component == bom.metadata.component
False
>>>
>>> bom.components.add(loaded_bom.metadata.component)
>>> bom.register_dependency(root_component, [loaded_bom.metadata.component])
>>> bom.components |= loaded_bom.components
>>> bom.dependencies |= loaded_bom.dependencies
>>>
>>> bom.validate()
True
>>>
>>> len(bom.components)
5
>>> len(bom.dependencies)
6

Note that the Bom says it is valid, while it is actually not - although this is not apparent at a glance.
When outputting to json (e.g. with the reported JsonV1Dot5), dependencies is shown to be as follows:

        [
            {"ref": "root", "dependsOn": ["BomRef.23942051401152453.4412489734431171"]},
            {"ref": "root", "dependsOn": ["test11", "test21"]},
            {"ref": "test11", "dependsOn": ["test12"]},
            {"ref": "test12"},
            {"ref": "test21", "dependsOn": ["test22"]},
            {"ref": "test22"}
        ]

This is actually invalid and would not validate when dumped to file and re-read by the library

Activity

  1. wkoot commented on Apr 14, 2025

    @wkoot
    ContributorAuthor

    The problem is a lot more clear when merging multiple files, as it seems to compound in some manner.
    A second testfile with the same structure, but modified (non-clashing) bom-ref components:

    >>> with open('out2.json', mode="r") as testfile:
    ...  json_bom = load(testfile)
    ...
    >>> loaded_bom2 = Bom.from_json(json_bom)
    >>>
    >>> loaded_bom.metadata.component
    <Component bom-ref=<BomRef 'root' id=140246182363600>, group=None, name=test, version=None, type=ComponentType.APPLICATION>
    >>> loaded_bom2.metadata.component
    <Component bom-ref=<BomRef 'root' id=140246180662288>, group=None, name=test2, version=None, type=ComponentType.APPLICATION>
    >>>
    >>> loaded_bom.metadata.component == loaded_bom2.metadata.component
    False
    >>> loaded_bom.metadata.component.bom_ref == loaded_bom2.metadata.component.bom_ref
    True
    >>>
    >>> bom.components.add(loaded_bom2.metadata.component)
    >>> bom.register_dependency(root_component, [loaded_bom2.metadata.component])
    >>> bom.components |= loaded_bom2.components
    >>> bom.dependencies |= loaded_bom2.dependencies
    >>> bom.validate()
    True
    >>>
    >>> len(bom.components)
    10
    >>> len(bom.dependencies)
    12

    This provides the following dependencies json output:

            [
                {"ref": "root", "dependsOn": ["BomRef.19783006151644178.9868708899859131"]},
                {"ref": "root", "dependsOn": ["test11", "test21"]},
                {"ref": "root", "dependsOn": ["test411", "test421"]},
                {"ref": "test11", "dependsOn": ["test12"]},
                {"ref": "test12"},
                {"ref": "test21", "dependsOn": ["test22"]},
                {"ref": "test22"},
                {"ref": "test411", "dependsOn": ["test412"]},
                {"ref": "test412"},
                {"ref": "test421", "dependsOn": ["test422"]},
                {"ref": "test422"}
            ]

    Because the first dependency object is the root_component, the entire dependency tree is actually disconnected from root.
    It seems to happen when register_dependency is called on the root object, which isn't a member of the bom components.

    Note that the root BomRef string seems to be regenerated each time the json output is generated, this DependsOn is not stable.
    Inspecting the data object shows that dependencies were not correctly linked (see targets), although the bom_ref is correctly stored:

    >>> for dep in bom.dependencies:
    ...     print(dep)
    ...
    <Dependency ref=<BomRef 'root' id=140246198533136>, targets=1>
    <Dependency ref=<BomRef 'root' id=140246182437328>, targets=2>
    <Dependency ref=<BomRef 'root' id=140246180673744>, targets=2>
    <Dependency ref=<BomRef 'test11' id=140246182406736>, targets=1>
    <Dependency ref=<BomRef 'test12' id=140246182363664>, targets=0>
    <Dependency ref=<BomRef 'test21' id=140246182440080>, targets=1>
    <Dependency ref=<BomRef 'test22' id=140246182376016>, targets=0>
    <Dependency ref=<BomRef 'test411' id=140246180704336>, targets=1>
    <Dependency ref=<BomRef 'test412' id=140246180662352>, targets=0>
    <Dependency ref=<BomRef 'test421' id=140246180721232>, targets=1>
    <Dependency ref=<BomRef 'test422' id=140246180673616>, targets=0>
    >>> bom.dependencies[0]._dependencies[0]._ref._value
    'root'
  2. jkowalleck commented on Apr 14, 2025

    @jkowalleck
    Member

    Are you aware, that per CycloneDX specification, Bom-refs must be unique per document?
    This library IS aware of it, and it is actually enforced by assigning unique values when serializing Json/xml.

    Furthermore, since you are struggling with merging again, you maybe want to try out https://github.com/CycloneDX/cyclonedx-cli and read CycloneDX/specification#320

  3. wkoot commented on Apr 14, 2025

    @wkoot
    ContributorAuthor

    Yes, I agree. The problem arises with merging of multiple documents, which are all valid on their own, but may contain duplicates.
    I will have a look again at the cyclonedx-cli implementation, but CycloneDX/cyclonedx-cli#399 is still unaddressed.

    The examples above show that this library is not correctly assigning unique values, and doesn't throw warning/exception.
    For instance, the dependency "root" is mentioned three times in the json output and the backrefs are not remembered.
    The metadata component ref is probably the only value that does not need to be altered for the structure to remain valid.

    However, every call to .validate() is stating that the produced Bom is valid - while the library recognises that the data is not unique.
    Producing json output also runs an extra validation, which does not recognise that the produced output is wrong.

  4. locked and limited conversation to collaborators on Apr 14, 2025
  5. converted this issue into a discussion #808 on Apr 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions