Skip to content

Commit 1f58b31

Browse files
authored
Merge pull request #263 from CycloneDX/feat/vex-without-components
feat: change model to put `Vulnerability` at `Bom` level, not `Component` level BREAKING CHANGE: `Vulnerability` now at `Bom` not `Component` level
2 parents 2d894b5 + fe38107 commit 1f58b31

File tree

14 files changed

+115
-106
lines changed

14 files changed

+115
-106
lines changed

.github/workflows/poetry.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
build-and-test:
8181
name: Test (${{ matrix.os }} py${{ matrix.python-version }} ${{ matrix.toxenv-factor }})
8282
runs-on: ${{ matrix.os }}
83-
timeout-minutes: 10
83+
timeout-minutes: 15
8484
env:
8585
REPORTS_ARTIFACT: tests-reports
8686
strategy:

.pre-commit-config.yaml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ repos:
44
hooks:
55
- id: system
66
name: mypy
7-
entry: poetry run tox -e mypy
7+
entry: poetry run tox -e mypy-locked
8+
pass_filenames: false
9+
language: system
10+
- repo: local
11+
hooks:
12+
- id: system
13+
name: isort
14+
entry: poetry run isort cyclonedx tests
815
pass_filenames: false
916
language: system
10-
# - repo: local
11-
# hooks:
12-
# - id: system
13-
# name: isort
14-
# entry: poetry run isort
15-
# pass_filenames: false
16-
# language: system
1717
- repo: local
1818
hooks:
1919
- id: system

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,21 @@
1313
----
1414

1515
This CycloneDX module for Python can generate valid CycloneDX bill-of-material document containing an aggregate of all
16-
project dependencies.
16+
project dependencies. CycloneDX is a lightweight BOM specification that is easily created, human-readable, and simple
17+
to parse.
1718

18-
This module is not designed for standalone use.
19+
**This module is not designed for standalone use.**
1920

20-
If you're looking for a CycloneDX tool to run to generate (SBOM) software bill-of-materials documents, why not checkout
21-
[CycloneDX Python][cyclonedx-python].
22-
23-
Additionally, the following tool can be used as well (and this library was written to help improve it) [Jake][jake].
21+
As of version `3.0.0`, the internal data model was adjusted to allow CycloneDX VEX documents to be produced as per
22+
[official examples](https://cyclonedx.org/capabilities/bomlink/#linking-external-vex-to-bom-inventory) linking a VEX
23+
documents to a separate BOM document.
2424

25-
Additionally, you can use this module yourself in your application to programmatically generate SBOMs.
25+
If you're looking for a CycloneDX tool to run to generate (SBOM) software bill-of-materials documents, why not checkout
26+
[CycloneDX Python][cyclonedx-python] or [Jake][jake].
2627

27-
CycloneDX is a lightweight BOM specification that is easily created, human-readable, and simple to parse.
28+
Alternatively, you can use this module yourself in your application to programmatically generate CycloneDX BOMs.
2829

29-
View our documentation [here](https://cyclonedx-python-library.readthedocs.io/).
30+
View the documentation [here](https://cyclonedx-python-library.readthedocs.io/).
3031

3132
## Python Support
3233

cyclonedx/model/bom.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#
1717
# SPDX-License-Identifier: Apache-2.0
1818
# Copyright (c) OWASP Foundation. All Rights Reserved.
19+
1920
import warnings
2021
from datetime import datetime, timezone
2122
from typing import Iterable, Optional
@@ -26,8 +27,10 @@
2627
from ..exception.model import UnknownComponentDependencyException
2728
from ..parser import BaseParser
2829
from . import ExternalReference, LicenseChoice, OrganizationalContact, OrganizationalEntity, Property, ThisTool, Tool
30+
from .bom_ref import BomRef
2931
from .component import Component
3032
from .service import Service
33+
from .vulnerability import Vulnerability
3134

3235

3336
class BomMetaData:
@@ -242,6 +245,7 @@ def __init__(self, *, components: Optional[Iterable[Component]] = None,
242245
self.components = components or [] # type: ignore
243246
self.services = services or [] # type: ignore
244247
self.external_references = external_references or [] # type: ignore
248+
self.vulnerabilities = SortedSet()
245249

246250
@property
247251
def uuid(self) -> UUID:
@@ -356,15 +360,46 @@ def external_references(self) -> "SortedSet[ExternalReference]":
356360
def external_references(self, external_references: Iterable[ExternalReference]) -> None:
357361
self._external_references = SortedSet(external_references)
358362

363+
def get_vulnerabilities_for_bom_ref(self, bom_ref: BomRef) -> "SortedSet[Vulnerability]":
364+
"""
365+
Get all known Vulnerabilities that affect the supplied bom_ref.
366+
367+
Args:
368+
bom_ref: `BomRef`
369+
370+
Returns:
371+
`SortedSet` of `Vulnerability`
372+
"""
373+
374+
vulnerabilities: SortedSet[Vulnerability] = SortedSet()
375+
for v in self.vulnerabilities:
376+
for target in v.affects:
377+
if target.ref == bom_ref.value:
378+
vulnerabilities.add(v)
379+
return vulnerabilities
380+
359381
def has_vulnerabilities(self) -> bool:
360382
"""
361383
Check whether this Bom has any declared vulnerabilities.
362384
363385
Returns:
364-
`bool` - `True` if at least one `cyclonedx.model.component.Component` has at least one Vulnerability,
365-
`False` otherwise.
386+
`bool` - `True` if this Bom has at least one Vulnerability, `False` otherwise.
387+
"""
388+
return bool(self.vulnerabilities)
389+
390+
@property
391+
def vulnerabilities(self) -> "SortedSet[Vulnerability]":
392+
"""
393+
Get all the Vulnerabilities in this BOM.
394+
395+
Returns:
396+
Set of `Vulnerability`
366397
"""
367-
return any(c.has_vulnerabilities() for c in self.components)
398+
return self._vulnerabilities
399+
400+
@vulnerabilities.setter
401+
def vulnerabilities(self, vulnerabilities: Iterable[Vulnerability]) -> None:
402+
self._vulnerabilities = SortedSet(vulnerabilities)
368403

369404
def validate(self) -> bool:
370405
"""
@@ -389,7 +424,7 @@ def validate(self) -> bool:
389424
# 2. Dependencies should exist for the Component this BOM is describing, if one is set
390425
if self.metadata.component and not self.metadata.component.dependencies:
391426
warnings.warn(
392-
f'The Component this BOM is describing {self.metadata.component.purl} has no defined dependencies'
427+
f'The Component this BOM is describing {self.metadata.component.purl} has no defined dependencies '
393428
f'which means the Dependency Graph is incomplete - you should add direct dependencies to this Component'
394429
f'to complete the Dependency Graph data.',
395430
UserWarning

cyclonedx/model/bom_ref.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def value(self, value: str) -> None:
4444

4545
def __eq__(self, other: object) -> bool:
4646
if isinstance(other, BomRef):
47-
return hash(other) == hash(self)
47+
return other.value == self.value
4848
return False
4949

5050
def __lt__(self, other: Any) -> bool:

cyclonedx/model/component.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
from .bom_ref import BomRef
4545
from .issue import IssueType
4646
from .release_note import ReleaseNotes
47-
from .vulnerability import Vulnerability
4847

4948

5049
class Commit:
@@ -763,7 +762,6 @@ def __init__(self, *, name: str, component_type: ComponentType = ComponentType.L
763762
self.licenses = [LicenseChoice(license_expression=license_str)] # type: ignore
764763

765764
self.__dependencies: "SortedSet[BomRef]" = SortedSet()
766-
self.__vulnerabilites: "SortedSet[Vulnerability]" = SortedSet()
767765

768766
@property
769767
def type(self) -> ComponentType:
@@ -1128,37 +1126,6 @@ def dependencies(self) -> "SortedSet[BomRef]":
11281126
def dependencies(self, dependencies: Iterable[BomRef]) -> None:
11291127
self.__dependencies = SortedSet(dependencies)
11301128

1131-
def add_vulnerability(self, vulnerability: Vulnerability) -> None:
1132-
"""
1133-
Add a Vulnerability to this Component.
1134-
1135-
Args:
1136-
vulnerability:
1137-
`cyclonedx.model.vulnerability.Vulnerability` instance to add to this Component.
1138-
1139-
Returns:
1140-
None
1141-
"""
1142-
self.__vulnerabilites.add(vulnerability)
1143-
1144-
def get_vulnerabilities(self) -> "SortedSet[Vulnerability]":
1145-
"""
1146-
Get all the Vulnerabilities for this Component.
1147-
1148-
Returns:
1149-
Set of `Vulnerability`
1150-
"""
1151-
return self.__vulnerabilites
1152-
1153-
def has_vulnerabilities(self) -> bool:
1154-
"""
1155-
Does this Component have any vulnerabilities?
1156-
1157-
Returns:
1158-
`True` if this Component has 1 or more vulnerabilities, `False` otherwise.
1159-
"""
1160-
return bool(self.get_vulnerabilities())
1161-
11621129
def get_pypi_url(self) -> str:
11631130
if self.version:
11641131
return f'https://pypi.org/project/{self.name}/{self.version}'

cyclonedx/output/json.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,6 @@ def generate(self, force_regeneration: bool = False) -> None:
7979
extras["dependencies"] = dependencies
8080
del dep_components
8181

82-
if self.bom_supports_vulnerabilities():
83-
vulnerabilities: List[Dict[Any, Any]] = []
84-
if bom.components:
85-
for component in bom.components:
86-
for vulnerability in component.get_vulnerabilities():
87-
vulnerabilities.append(
88-
json.loads(json.dumps(vulnerability, cls=CycloneDxJSONEncoder))
89-
)
90-
if vulnerabilities:
91-
extras["vulnerabilities"] = vulnerabilities
92-
9382
bom_json = json.loads(json.dumps(bom, cls=CycloneDxJSONEncoder))
9483
bom_json = json.loads(self._specialise_output_for_schema_version(bom_json=bom_json))
9584
self._json_output = json.dumps({**self._create_bom_element(), **bom_json, **extras})
@@ -133,6 +122,10 @@ def _specialise_output_for_schema_version(self, bom_json: Dict[Any, Any]) -> str
133122
and 'hashes' in bom_json['externalReferences'][i].keys():
134123
del bom_json['externalReferences'][i]['hashes']
135124

125+
# Remove Vulnerabilities if not supported
126+
if not self.bom_supports_vulnerabilities() and 'vulnerabilities' in bom_json.keys():
127+
del bom_json['vulnerabilities']
128+
136129
return json.dumps(bom_json)
137130

138131
def output_as_string(self) -> str:

cyclonedx/output/xml.py

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -75,29 +75,28 @@ def generate(self, force_regeneration: bool = False) -> None:
7575
if self.bom_supports_metadata():
7676
self._add_metadata_element()
7777

78-
has_vulnerabilities: bool = False
79-
8078
components_element = ElementTree.SubElement(self._root_bom_element, 'components')
8179
if bom.components:
8280
for component in bom.components:
8381
component_element = self._add_component_element(component=component)
8482
components_element.append(component_element)
85-
if self.bom_supports_vulnerabilities_via_extension() and component.has_vulnerabilities():
86-
# Vulnerabilities are only possible when bom-ref is supported by the main CycloneDX schema version
87-
vulnerabilities = ElementTree.SubElement(component_element, 'v:vulnerabilities')
88-
for vulnerability in component.get_vulnerabilities():
89-
if component.bom_ref:
90-
vulnerabilities.append(
91-
Xml._get_vulnerability_as_xml_element_pre_1_3(bom_ref=component.bom_ref,
92-
vulnerability=vulnerability)
93-
)
94-
else:
95-
warnings.warn(
96-
f'Unable to include Vulnerability {str(vulnerability)} in generated BOM as the '
97-
f'Component it relates to ({str(component)}) but it has no bom-ref.'
98-
)
99-
elif component.has_vulnerabilities():
100-
has_vulnerabilities = True
83+
if self.bom_supports_vulnerabilities_via_extension():
84+
component_vulnerabilities = bom.get_vulnerabilities_for_bom_ref(bom_ref=component.bom_ref)
85+
if component_vulnerabilities:
86+
# Vulnerabilities are only possible when bom-ref is supported by the main CycloneDX schema
87+
# version
88+
vulnerabilities = ElementTree.SubElement(component_element, 'v:vulnerabilities')
89+
for vulnerability in component_vulnerabilities:
90+
if component.bom_ref:
91+
vulnerabilities.append(
92+
Xml._get_vulnerability_as_xml_element_pre_1_3(bom_ref=component.bom_ref,
93+
vulnerability=vulnerability)
94+
)
95+
else:
96+
warnings.warn(
97+
f'Unable to include Vulnerability {str(vulnerability)} in generated BOM as the '
98+
f'Component it relates to ({str(component)}) but it has no bom-ref.'
99+
)
101100

102101
if self.bom_supports_services() and bom.services:
103102
services_element = ElementTree.SubElement(self._root_bom_element, 'services')
@@ -125,13 +124,12 @@ def generate(self, force_regeneration: bool = False) -> None:
125124
})
126125
del dep_components
127126

128-
if self.bom_supports_vulnerabilities() and has_vulnerabilities:
127+
if self.bom_supports_vulnerabilities() and bom.vulnerabilities:
129128
vulnerabilities_element = ElementTree.SubElement(self._root_bom_element, 'vulnerabilities')
130-
for component in bom.components:
131-
for vulnerability in component.get_vulnerabilities():
132-
vulnerabilities_element.append(
133-
self._get_vulnerability_as_xml_element_post_1_4(vulnerability=vulnerability)
134-
)
129+
for vulnerability in bom.vulnerabilities:
130+
vulnerabilities_element.append(
131+
self._get_vulnerability_as_xml_element_post_1_4(vulnerability=vulnerability)
132+
)
135133

136134
self.generated = True
137135

docs/index.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ CycloneDX is a lightweight BOM specification that is easily created, human-reada
2020
This CycloneDX module for Python can generate valid CycloneDX bill-of-material document containing an aggregate of all
2121
project dependencies.
2222

23+
As of version ``3.0.0``, the internal data model was adjusted to allow CycloneDX VEX documents to be produced as per
24+
`official examples`_ linking VEX to a separate BOM.
25+
2326
This module is not designed for standalone use (i.e. it is not executable on it’s own). If you’re looking for a
2427
CycloneDX tool to run to generate (SBOM) software bill-of-materials documents, why not checkout:
2528

@@ -44,4 +47,5 @@ programmatically generate SBOMs.
4447

4548
.. _CycloneDX Python: https://pypi.org/project/cyclonedx-bom/
4649
.. _Jake: https://pypi.org/project/jake
47-
.. _CycloneDX Tool Center: https://cyclonedx.org/tool-center/
50+
.. _CycloneDX Tool Center: https://cyclonedx.org/tool-center/
51+
.. _official examples: https://cyclonedx.org/capabilities/bomlink/#linking-external-vex-to-bom-inventory

docs/schema-support.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ supported in prior versions of the CycloneDX schema.
4343
| ``bom.properties`` | No | See `schema specification bug 130`_ |
4444
+----------------------------+---------------+---------------------------------------------------------------------------------------------------+
4545
| ``bom.vulnerabilities`` | Yes | Note: Prior to CycloneDX 1.4, these were present under ``bom.components`` via a schema extension. |
46+
| | | Note: As of ``cyclonedx-python-lib`` ``>3.0.0``, Vulnerability are modelled differently |
4647
+----------------------------+---------------+---------------------------------------------------------------------------------------------------+
4748
| ``bom.signature`` | No | |
4849
+----------------------------+---------------+---------------------------------------------------------------------------------------------------+

0 commit comments

Comments
 (0)