Skip to content

Commit 6993dea

Browse files
tonalqwencoderjkowalleck
authored
feat: add isExternal property to Component for CycloneDX v1.7 (#959)
Signed-off-by: Alexandr N Zamaraev (aka tonal) <tonal@promsoft.ru> Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: Jan Kowalleck <jan.kowalleck@gmail.com>
1 parent 27b2d43 commit 6993dea

18 files changed

Lines changed: 447 additions & 1 deletion

cyclonedx/model/component.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
SchemaVersion1Dot6,
5252
SchemaVersion1Dot7,
5353
)
54-
from ..serialization import PackageUrl as PackageUrlSH
54+
from ..serialization import PackageUrl as PackageUrlSH, XmlBoolAttribute as _XmlBoolAttributeSH
5555
from . import (
5656
AttachedText,
5757
ExternalReference,
@@ -993,6 +993,7 @@ def __init__(
993993
version: Optional[str] = None,
994994
description: Optional[str] = None,
995995
scope: Optional[ComponentScope] = None,
996+
is_external: Optional[bool] = None,
996997
hashes: Optional[Iterable[HashType]] = None,
997998
licenses: Optional[Iterable[License]] = None,
998999
copyright: Optional[str] = None,
@@ -1026,6 +1027,7 @@ def __init__(
10261027
self.name = name
10271028
self.description = description
10281029
self.scope = scope
1030+
self.is_external = is_external
10291031
self.hashes = hashes or []
10301032
self.licenses = licenses or []
10311033
self.copyright = copyright
@@ -1304,6 +1306,29 @@ def scope(self) -> Optional[ComponentScope]:
13041306
def scope(self, scope: Optional[ComponentScope]) -> None:
13051307
self._scope = scope
13061308

1309+
@property
1310+
@serializable.json_name('isExternal')
1311+
@serializable.xml_name('isExternal')
1312+
@serializable.xml_attribute()
1313+
@serializable.type_mapping(_XmlBoolAttributeSH)
1314+
@serializable.view(SchemaVersion1Dot7)
1315+
def is_external(self) -> Optional[bool]:
1316+
"""
1317+
Determine whether this component is external. An external component is one that is not part of an assembly,
1318+
but is expected to be provided by the environment, regardless of the component's scope. This setting can be
1319+
useful for distinguishing which components are bundled with the product and which can be relied upon to be
1320+
present in the deployment environment. This may be set to true for runtime components only. For
1321+
metadata.component, it must be set to false.
1322+
1323+
Returns:
1324+
`bool` if set else `None`
1325+
"""
1326+
return self._is_external
1327+
1328+
@is_external.setter
1329+
def is_external(self, is_external: Optional[bool]) -> None:
1330+
self._is_external = is_external
1331+
13071332
@property
13081333
@serializable.type_mapping(_HashTypeRepositorySerializationHelper)
13091334
@serializable.xml_sequence(11)
@@ -1690,6 +1715,7 @@ def __comparable_tuple(self) -> _ComparableTuple:
16901715
_ComparableTuple(self.components), self.evidence, self.release_notes, self.modified,
16911716
_ComparableTuple(self.authors), _ComparableTuple(self.omnibor_ids), self.manufacturer,
16921717
self.crypto_properties, _ComparableTuple(self.tags),
1718+
self.is_external,
16931719
))
16941720

16951721
def __eq__(self, other: object) -> bool:

cyclonedx/serialization/__init__.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,66 @@ def deserialize(cls, o: Any) -> UUID:
9595
) from err
9696

9797

98+
class XmlBoolAttribute(BaseHelper):
99+
"""Helper for serializing boolean values as XML attribute-compatible 'true'/'false' strings,
100+
while keeping native boolean values for JSON."""
101+
102+
@classmethod
103+
def json_serialize(cls, o: Any) -> Optional[bool]:
104+
if o is None:
105+
return None
106+
if isinstance(o, bool):
107+
return o
108+
raise SerializationOfUnexpectedValueException(
109+
f'Attempt to serialize a non-boolean: {o!r}')
110+
111+
@classmethod
112+
def json_deserialize(cls, o: Any) -> Optional[bool]:
113+
if o is None:
114+
return None
115+
if isinstance(o, bool):
116+
return o
117+
raise CycloneDxDeserializationException(
118+
f'Invalid boolean value: {o!r}'
119+
)
120+
121+
@classmethod
122+
def xml_serialize(cls, o: Any) -> Optional[str]:
123+
if o is None:
124+
return None
125+
if isinstance(o, bool):
126+
return 'true' if o else 'false'
127+
raise SerializationOfUnexpectedValueException(
128+
f'Attempt to serialize a non-boolean: {o!r}')
129+
130+
@classmethod
131+
def xml_deserialize(cls, o: Any) -> Optional[bool]:
132+
if o is None:
133+
return None
134+
if isinstance(o, bool):
135+
return o
136+
if isinstance(o, str):
137+
# Per spec https://www.w3.org/TR/xmlschema-2/#boolean
138+
# An instance of a datatype that is defined as boolean
139+
# can have the following legal literals {true, false, 1, 0}.
140+
o_lower = o.lower()
141+
if o_lower in ('1', 'true'):
142+
return True
143+
if o_lower in ('0', 'false'):
144+
return False
145+
raise CycloneDxDeserializationException(
146+
f'Invalid boolean value: {o!r}'
147+
)
148+
149+
@classmethod
150+
def serialize(cls, o: Any) -> Any:
151+
return cls.xml_serialize(o)
152+
153+
@classmethod
154+
def deserialize(cls, o: Any) -> Any:
155+
return cls.xml_deserialize(o)
156+
157+
98158
@deprecated('No public API planned for replacing this,')
99159
class LicenseRepositoryHelper(_LicenseRepositorySerializationHelper):
100160
"""**DEPRECATED**

tests/_data/models.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,11 @@ def get_bom_with_external_references() -> Bom:
599599
return bom
600600

601601

602+
def get_bom_with_external_component_1_7() -> Bom:
603+
bom = _make_bom(components=[get_component_external()])
604+
return bom
605+
606+
602607
def get_bom_with_services_simple() -> Bom:
603608
bom = _make_bom(services=[
604609
Service(name='my-first-service', bom_ref='my-specific-bom-ref-for-my-first-service'),
@@ -859,6 +864,16 @@ def get_component_setuptools_simple(
859864
)
860865

861866

867+
def get_component_external() -> Component:
868+
return Component(
869+
name='external-lib', version='1.0.0',
870+
type=ComponentType.LIBRARY,
871+
is_external=True,
872+
scope=ComponentScope.REQUIRED,
873+
bom_ref='external-lib-1.0.0',
874+
)
875+
876+
862877
def get_component_setuptools_simple_no_version(bom_ref: Optional[str] = None) -> Component:
863878
return Component(
864879
name='setuptools', bom_ref=bom_ref or 'pkg:pypi/setuptools?extension=tar.gz',
@@ -1703,6 +1718,7 @@ def get_bom_for_issue941_nested_dependencies_irreversible_migrate() -> Bom:
17031718
get_bom_with_licenses,
17041719
get_bom_with_multiple_licenses,
17051720
get_bom_for_issue_497_urls,
1721+
get_bom_with_external_component_1_7,
17061722
get_bom_for_issue_598_multiple_components_with_purl_qualifiers,
17071723
get_bom_with_component_setuptools_with_v16_fields,
17081724
get_bom_for_issue_630_empty_property,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" ?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.0" version="1">
3+
<components>
4+
<component type="library">
5+
<name>external-lib</name>
6+
<version>1.0.0</version>
7+
<scope>required</scope>
8+
<modified>false</modified>
9+
</component>
10+
</components>
11+
</bom>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" ?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.1" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1">
3+
<components>
4+
<component type="library" bom-ref="external-lib-1.0.0">
5+
<name>external-lib</name>
6+
<version>1.0.0</version>
7+
<scope>required</scope>
8+
</component>
9+
</components>
10+
</bom>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"components": [
3+
{
4+
"bom-ref": "external-lib-1.0.0",
5+
"name": "external-lib",
6+
"scope": "required",
7+
"type": "library",
8+
"version": "1.0.0"
9+
}
10+
],
11+
"dependencies": [
12+
{
13+
"ref": "external-lib-1.0.0"
14+
}
15+
],
16+
"metadata": {
17+
"timestamp": "2023-01-07T13:44:32.312678+00:00"
18+
},
19+
"serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac",
20+
"version": 1,
21+
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
22+
"bomFormat": "CycloneDX",
23+
"specVersion": "1.2"
24+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" ?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1">
3+
<metadata>
4+
<timestamp>2023-01-07T13:44:32.312678+00:00</timestamp>
5+
</metadata>
6+
<components>
7+
<component type="library" bom-ref="external-lib-1.0.0">
8+
<name>external-lib</name>
9+
<version>1.0.0</version>
10+
<scope>required</scope>
11+
</component>
12+
</components>
13+
<dependencies>
14+
<dependency ref="external-lib-1.0.0"/>
15+
</dependencies>
16+
</bom>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"components": [
3+
{
4+
"bom-ref": "external-lib-1.0.0",
5+
"name": "external-lib",
6+
"scope": "required",
7+
"type": "library",
8+
"version": "1.0.0"
9+
}
10+
],
11+
"dependencies": [
12+
{
13+
"ref": "external-lib-1.0.0"
14+
}
15+
],
16+
"metadata": {
17+
"timestamp": "2023-01-07T13:44:32.312678+00:00"
18+
},
19+
"serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac",
20+
"version": 1,
21+
"$schema": "http://cyclonedx.org/schema/bom-1.3a.schema.json",
22+
"bomFormat": "CycloneDX",
23+
"specVersion": "1.3"
24+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" ?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1">
3+
<metadata>
4+
<timestamp>2023-01-07T13:44:32.312678+00:00</timestamp>
5+
</metadata>
6+
<components>
7+
<component type="library" bom-ref="external-lib-1.0.0">
8+
<name>external-lib</name>
9+
<version>1.0.0</version>
10+
<scope>required</scope>
11+
</component>
12+
</components>
13+
<dependencies>
14+
<dependency ref="external-lib-1.0.0"/>
15+
</dependencies>
16+
</bom>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"components": [
3+
{
4+
"bom-ref": "external-lib-1.0.0",
5+
"name": "external-lib",
6+
"scope": "required",
7+
"type": "library",
8+
"version": "1.0.0"
9+
}
10+
],
11+
"dependencies": [
12+
{
13+
"ref": "external-lib-1.0.0"
14+
}
15+
],
16+
"metadata": {
17+
"timestamp": "2023-01-07T13:44:32.312678+00:00"
18+
},
19+
"serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac",
20+
"version": 1,
21+
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
22+
"bomFormat": "CycloneDX",
23+
"specVersion": "1.4"
24+
}

0 commit comments

Comments
 (0)