Skip to content

Commit 1480a09

Browse files
tonalqwencoder
andcommitted
feat: add isExternal property to Component for CycloneDX v1.7
Implement the isExternal boolean property on Component as specified in CycloneDX v1.7 schema. An external component is one that is not part of an assembly, but is expected to be provided by the environment. - Add is_external property to Component class with XML attribute serialization - Create XmlBoolAttribute serialization helper for proper bool handling - Add unit tests for is_external (default value, set/get, equality, sorting) - Add test fixture and snapshots for JSON/XML output - Supports v1.7+ schemas only Implements: #903 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Signed-off-by: Alexandr N Zamaraev (aka tonal) <tonal@promsoft.ru>
1 parent d04d043 commit 1480a09

18 files changed

Lines changed: 443 additions & 2 deletions

cyclonedx/model/component.py

Lines changed: 27 additions & 2 deletions
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)
@@ -1683,7 +1708,7 @@ def __comparable_tuple(self) -> _ComparableTuple:
16831708
self.swid, self.cpe, _ComparableTuple(self.swhids),
16841709
self.supplier, self.author, self.publisher,
16851710
self.description,
1686-
self.mime_type, self.scope, _ComparableTuple(self.hashes),
1711+
self.mime_type, self.scope, self.is_external, _ComparableTuple(self.hashes),
16871712
_ComparableTuple(self.licenses), self.copyright,
16881713
self.pedigree,
16891714
_ComparableTuple(self.external_references), _ComparableTuple(self.properties),

cyclonedx/serialization/__init__.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,63 @@ 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+
o_lower = o.lower()
138+
if o_lower in ('1', 'true'):
139+
return True
140+
if o_lower in ('0', 'false'):
141+
return False
142+
raise CycloneDxDeserializationException(
143+
f'Invalid boolean value: {o!r}'
144+
)
145+
146+
@classmethod
147+
def serialize(cls, o: Any) -> Any:
148+
return cls.xml_serialize(o)
149+
150+
@classmethod
151+
def deserialize(cls, o: Any) -> Any:
152+
return cls.xml_deserialize(o)
153+
154+
98155
@deprecated('No public API planned for replacing this,')
99156
class LicenseRepositoryHelper(_LicenseRepositorySerializationHelper):
100157
"""**DEPRECATED**

tests/_data/models.py

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

595595

596+
def get_bom_with_external_component_1_7() -> Bom:
597+
bom = _make_bom(components=[get_component_external()])
598+
return bom
599+
600+
596601
def get_bom_with_services_simple() -> Bom:
597602
bom = _make_bom(services=[
598603
Service(name='my-first-service', bom_ref='my-specific-bom-ref-for-my-first-service'),
@@ -853,6 +858,16 @@ def get_component_setuptools_simple(
853858
)
854859

855860

861+
def get_component_external() -> Component:
862+
return Component(
863+
name='external-lib', version='1.0.0',
864+
type=ComponentType.LIBRARY,
865+
is_external=True,
866+
scope=ComponentScope.REQUIRED,
867+
bom_ref='external-lib-1.0.0',
868+
)
869+
870+
856871
def get_component_setuptools_simple_no_version(bom_ref: Optional[str] = None) -> Component:
857872
return Component(
858873
name='setuptools', bom_ref=bom_ref or 'pkg:pypi/setuptools?extension=tar.gz',
@@ -1611,6 +1626,7 @@ def get_bom_for_issue540_duplicate_components() -> Bom:
16111626
get_bom_with_licenses,
16121627
get_bom_with_multiple_licenses,
16131628
get_bom_for_issue_497_urls,
1629+
get_bom_with_external_component_1_7,
16141630
get_bom_for_issue_598_multiple_components_with_purl_qualifiers,
16151631
get_bom_with_component_setuptools_with_v16_fields,
16161632
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)