Skip to content

Commit 1f5fd7a

Browse files
jkuglerjkowalleck
andauthored
feat!: Add component and services for tools (#635)
CycloneDX spec 1.5 deprecated an array of tools in bom.metadata and instead prefers object with an array of components and an array of services. This PR implements that. This works de-serializing a Syft SBOM with a tool section like so: ``` "metadata": { "timestamp": "2024-06-10T13:06:52-08:00", "tools": { "components": [ { "type": "application", "author": "anchore", "name": "syft", "version": "1.4.1" } ] }, "component": { "bom-ref": "08329a07b4eb8eac", "type": "file", "name": "./" } }, ``` Next up: docs, XML (de)serialization code, and tests. fixes #561 --------- Signed-off-by: Joshua Kugler <[email protected]> Signed-off-by: Jan Kowalleck <[email protected]> Co-authored-by: Jan Kowalleck <[email protected]>
1 parent 9ba4b8e commit 1f5fd7a

File tree

74 files changed

+3167
-328
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+3167
-328
lines changed

cyclonedx/model/__init__.py

Lines changed: 5 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,139 +1127,6 @@ def __repr__(self) -> str:
11271127
return f'<Note id={id(self)}, locale={self.locale}>'
11281128

11291129

1130-
@serializable.serializable_class
1131-
class Tool:
1132-
"""
1133-
This is our internal representation of the `toolType` complex type within the CycloneDX standard.
1134-
1135-
Tool(s) are the things used in the creation of the CycloneDX document.
1136-
1137-
Tool might be deprecated since CycloneDX 1.5, but it is not deprecated in this library.
1138-
In fact, this library will try to provide a compatibility layer if needed.
1139-
1140-
.. note::
1141-
See the CycloneDX Schema for toolType: https://cyclonedx.org/docs/1.3/#type_toolType
1142-
"""
1143-
1144-
def __init__(
1145-
self, *,
1146-
vendor: Optional[str] = None,
1147-
name: Optional[str] = None,
1148-
version: Optional[str] = None,
1149-
hashes: Optional[Iterable[HashType]] = None,
1150-
external_references: Optional[Iterable[ExternalReference]] = None,
1151-
) -> None:
1152-
self.vendor = vendor
1153-
self.name = name
1154-
self.version = version
1155-
self.hashes = hashes or [] # type:ignore[assignment]
1156-
self.external_references = external_references or [] # type:ignore[assignment]
1157-
1158-
@property
1159-
@serializable.xml_sequence(1)
1160-
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
1161-
def vendor(self) -> Optional[str]:
1162-
"""
1163-
The name of the vendor who created the tool.
1164-
1165-
Returns:
1166-
`str` if set else `None`
1167-
"""
1168-
return self._vendor
1169-
1170-
@vendor.setter
1171-
def vendor(self, vendor: Optional[str]) -> None:
1172-
self._vendor = vendor
1173-
1174-
@property
1175-
@serializable.xml_sequence(2)
1176-
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
1177-
def name(self) -> Optional[str]:
1178-
"""
1179-
The name of the tool.
1180-
1181-
Returns:
1182-
`str` if set else `None`
1183-
"""
1184-
return self._name
1185-
1186-
@name.setter
1187-
def name(self, name: Optional[str]) -> None:
1188-
self._name = name
1189-
1190-
@property
1191-
@serializable.xml_sequence(3)
1192-
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
1193-
def version(self) -> Optional[str]:
1194-
"""
1195-
The version of the tool.
1196-
1197-
Returns:
1198-
`str` if set else `None`
1199-
"""
1200-
return self._version
1201-
1202-
@version.setter
1203-
def version(self, version: Optional[str]) -> None:
1204-
self._version = version
1205-
1206-
@property
1207-
@serializable.type_mapping(_HashTypeRepositorySerializationHelper)
1208-
@serializable.xml_sequence(4)
1209-
def hashes(self) -> 'SortedSet[HashType]':
1210-
"""
1211-
The hashes of the tool (if applicable).
1212-
1213-
Returns:
1214-
Set of `HashType`
1215-
"""
1216-
return self._hashes
1217-
1218-
@hashes.setter
1219-
def hashes(self, hashes: Iterable[HashType]) -> None:
1220-
self._hashes = SortedSet(hashes)
1221-
1222-
@property
1223-
@serializable.view(SchemaVersion1Dot4)
1224-
@serializable.view(SchemaVersion1Dot5)
1225-
@serializable.view(SchemaVersion1Dot6)
1226-
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference')
1227-
@serializable.xml_sequence(5)
1228-
def external_references(self) -> 'SortedSet[ExternalReference]':
1229-
"""
1230-
External References provides a way to document systems, sites, and information that may be relevant but which
1231-
are not included with the BOM.
1232-
1233-
Returns:
1234-
Set of `ExternalReference`
1235-
"""
1236-
return self._external_references
1237-
1238-
@external_references.setter
1239-
def external_references(self, external_references: Iterable[ExternalReference]) -> None:
1240-
self._external_references = SortedSet(external_references)
1241-
1242-
def __eq__(self, other: object) -> bool:
1243-
if isinstance(other, Tool):
1244-
return hash(other) == hash(self)
1245-
return False
1246-
1247-
def __lt__(self, other: Any) -> bool:
1248-
if isinstance(other, Tool):
1249-
return _ComparableTuple((
1250-
self.vendor, self.name, self.version
1251-
)) < _ComparableTuple((
1252-
other.vendor, other.name, other.version
1253-
))
1254-
return NotImplemented
1255-
1256-
def __hash__(self) -> int:
1257-
return hash((self.vendor, self.name, self.version, tuple(self.hashes), tuple(self.external_references)))
1258-
1259-
def __repr__(self) -> str:
1260-
return f'<Tool name={self.name}, version={self.version}, vendor={self.vendor}>'
1261-
1262-
12631130
@serializable.serializable_class
12641131
class IdentifiableAction:
12651132
"""
@@ -1397,6 +1264,9 @@ def __repr__(self) -> str:
13971264
return f'<Copyright text={self.text}>'
13981265

13991266

1267+
# Importing here to avoid a circular import
1268+
from .tool import Tool # pylint: disable=wrong-import-position # noqa: E402
1269+
14001270
ThisTool = Tool(
14011271
vendor='CycloneDX',
14021272
name='cyclonedx-python-lib',
@@ -1434,4 +1304,5 @@ def __repr__(self) -> str:
14341304
type=ExternalReferenceType.WEBSITE,
14351305
url=XsUri('https://github.com/CycloneDX/cyclonedx-python-lib/#readme')
14361306
)
1437-
])
1307+
]
1308+
)

cyclonedx/model/bom.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,14 @@
3737
SchemaVersion1Dot6,
3838
)
3939
from ..serialization import LicenseRepositoryHelper, UrnUuidHelper
40-
from . import ExternalReference, Property, ThisTool, Tool
40+
from . import ExternalReference, Property, ThisTool
4141
from .bom_ref import BomRef
4242
from .component import Component
4343
from .contact import OrganizationalContact, OrganizationalEntity
4444
from .dependency import Dependable, Dependency
4545
from .license import License, LicenseExpression, LicenseRepository
4646
from .service import Service
47+
from .tool import Tool, ToolsRepository, _ToolsRepositoryHelper
4748
from .vulnerability import Vulnerability
4849

4950
if TYPE_CHECKING: # pragma: no cover
@@ -61,7 +62,7 @@ class BomMetaData:
6162

6263
def __init__(
6364
self, *,
64-
tools: Optional[Iterable[Tool]] = None,
65+
tools: Optional[Union[Iterable[Tool], ToolsRepository]] = None,
6566
authors: Optional[Iterable[OrganizationalContact]] = None,
6667
component: Optional[Component] = None,
6768
supplier: Optional[OrganizationalEntity] = None,
@@ -89,7 +90,7 @@ def __init__(
8990
DeprecationWarning)
9091

9192
if not tools:
92-
self.tools.add(ThisTool)
93+
self.tools.tools.add(ThisTool)
9394

9495
@property
9596
@serializable.type_mapping(serializable.helpers.XsdDateTime)
@@ -119,22 +120,22 @@ def timestamp(self, timestamp: datetime) -> None:
119120
# ... # TODO since CDX1.5
120121

121122
@property
122-
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'tool')
123+
@serializable.type_mapping(_ToolsRepositoryHelper)
123124
@serializable.xml_sequence(3)
124-
def tools(self) -> 'SortedSet[Tool]':
125+
def tools(self) -> ToolsRepository:
125126
"""
126127
Tools used to create this BOM.
127128
128129
Returns:
129-
`Set` of `Tool` objects.
130+
`ToolsRepository` objects.
130131
"""
131-
# TODO since CDX1.5 also supports `Component` and `Services`, not only `Tool`
132132
return self._tools
133133

134134
@tools.setter
135-
def tools(self, tools: Iterable[Tool]) -> None:
136-
# TODO since CDX1.5 also supports `Component` and `Services`, not only `Tool`
137-
self._tools = SortedSet(tools)
135+
def tools(self, tools: Union[Iterable[Tool], ToolsRepository]) -> None:
136+
self._tools = tools \
137+
if isinstance(tools, ToolsRepository) \
138+
else ToolsRepository(tools=tools)
138139

139140
@property
140141
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'author')
@@ -292,7 +293,7 @@ def __eq__(self, other: object) -> bool:
292293
def __hash__(self) -> int:
293294
return hash((
294295
tuple(self.authors), self.component, tuple(self.licenses), self.manufacture, tuple(self.properties),
295-
self.supplier, self.timestamp, tuple(self.tools), self.manufacturer,
296+
self.supplier, self.timestamp, self.tools, self.manufacturer,
296297
))
297298

298299
def __repr__(self) -> str:

0 commit comments

Comments
 (0)