Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 22 additions & 11 deletions cyclonedx/model/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def __init__(
endpoints: Optional[Iterable[XsUri]] = None,
authenticated: Optional[bool] = None,
x_trust_boundary: Optional[bool] = None,
trust_zone: Optional[str] = None,
data: Optional[Iterable[DataClassification]] = None,
licenses: Optional[Iterable[License]] = None,
external_references: Optional[Iterable[ExternalReference]] = None,
Expand All @@ -83,6 +84,7 @@ def __init__(
self.endpoints = endpoints or []
self.authenticated = authenticated
self.x_trust_boundary = x_trust_boundary
self.trust_zone = trust_zone
self.data = data or []
self.licenses = licenses or []
self.external_references = external_references or []
Expand Down Expand Up @@ -239,16 +241,25 @@ def x_trust_boundary(self) -> Optional[bool]:
def x_trust_boundary(self, x_trust_boundary: Optional[bool]) -> None:
self._x_trust_boundary = x_trust_boundary

# @property
# ...
# @serializable.view(SchemaVersion1Dot5)
# @serializable.xml_sequence(9)
# def trust_zone(self) -> ...:
# ... # since CDX1.5
#
# @trust_zone.setter
# def trust_zone(self, ...) -> None:
# ... # since CDX1.5
@property
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.view(SchemaVersion1Dot7)
@serializable.xml_sequence(9)

@saquibsaifee saquibsaifee Aug 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@serializable.xml_sequence(9)
@serializable.xml_sequence(9)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)

def trust_zone(self) -> Optional[str]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trust_zone property is missing the @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) decorator that all other Optional[str] fields in this class carry (e.g. group, description, version, name).

The CycloneDX schema defines trustZone as xs:normalizedString, which requires tab/newline characters to be collapsed to spaces. Without this decorator, a value like 'internal\tvpc' will be written as a literal tab into the XML output, producing an invalid document.

@property
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.view(SchemaVersion1Dot7)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)  # ← add this
@serializable.xml_sequence(9)
def trust_zone(self) -> Optional[str]:

After adding the decorator, the XML snapshot fixtures (*.xml.bin) need to be regenerated — the tab in internal\tvpc should be normalized to a space (internal vpc) in the XML output.

"""
The name of the trust zone the service resides in.

Supported from CycloneDX v1.5 onwards.

Returns:
`str` if set else `None`
"""
return self._trust_zone

@trust_zone.setter
def trust_zone(self, trust_zone: Optional[str]) -> None:
self._trust_zone = trust_zone

@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'classification')
Expand Down Expand Up @@ -369,7 +380,7 @@ def __comparable_tuple(self) -> _ComparableTuple:
self.authenticated, _ComparableTuple(self.data), _ComparableTuple(self.endpoints),
_ComparableTuple(self.external_references), _ComparableTuple(self.licenses),
_ComparableTuple(self.properties), self.release_notes, _ComparableTuple(self.services),
self.x_trust_boundary
self.x_trust_boundary, self.trust_zone
))

def __eq__(self, other: object) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion tests/_data/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,7 @@ def get_bom_with_services_complex() -> Bom:
XsUri('/api/thing/1'),
XsUri('/api/thing/2')
],
authenticated=False, x_trust_boundary=True, data=[
authenticated=False, x_trust_boundary=True, trust_zone='internal\tvpc', data=[
DataClassification(flow=DataFlow.OUTBOUND, classification='public')
],
licenses=[DisjunctiveLicense(name='Commercial')],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
"title": "Release Notes Title",
"type": "major"
},
"trustZone": "internal\tvpc",
"version": "1.2.3",
"x-trust-boundary": true
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
</endpoints>
<authenticated>false</authenticated>
<x-trust-boundary>true</x-trust-boundary>
<trustZone>internal vpc</trustZone>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML snapshot contains a raw tab character (internal\tvpc) rather than the normalized form internal vpc.

Since trustZone is typed as xs:normalizedString in the CycloneDX XML schema, tab/newline/carriage-return characters must be normalized to spaces before output. Once the @serializable.xml_string(NORMALIZED_STRING) decorator is added to the trust_zone property in service.py, please regenerate this snapshot — it should read <trustZone>internal vpc</trustZone> instead.

<data>
<classification flow="outbound">public</classification>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@
"title": "Release Notes Title",
"type": "major"
},
"trustZone": "internal\tvpc",
"version": "1.2.3",
"x-trust-boundary": true
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
</endpoints>
<authenticated>false</authenticated>
<x-trust-boundary>true</x-trust-boundary>
<trustZone>internal vpc</trustZone>
<data>
<classification flow="outbound">public</classification>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@
"title": "Release Notes Title",
"type": "major"
},
"trustZone": "internal\tvpc",
"version": "1.2.3",
"x-trust-boundary": true
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
</endpoints>
<authenticated>false</authenticated>
<x-trust-boundary>true</x-trust-boundary>
<trustZone>internal vpc</trustZone>
<data>
<classification flow="outbound">public</classification>
</data>
Expand Down
20 changes: 20 additions & 0 deletions tests/test_model_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

from unittest import TestCase

from sortedcontainers import SortedSet

from cyclonedx.model.service import Service
from tests import reorder

Expand All @@ -35,6 +37,7 @@ def test_minimal_service(self) -> None:
self.assertFalse(s.endpoints)
self.assertIsNone(s.authenticated)
self.assertIsNone(s.x_trust_boundary)
self.assertIsNone(s.trust_zone)
self.assertFalse(s.data)
self.assertFalse(s.licenses)
self.assertFalse(s.external_references)
Expand All @@ -57,6 +60,7 @@ def test_service_with_services(self) -> None:
self.assertFalse(parent_service.endpoints)
self.assertIsNone(parent_service.authenticated)
self.assertIsNone(parent_service.x_trust_boundary)
self.assertIsNone(parent_service.trust_zone)
self.assertFalse(parent_service.data)
self.assertFalse(parent_service.licenses)
self.assertFalse(parent_service.external_references)
Expand All @@ -80,3 +84,19 @@ def test_sort(self) -> None:
sorted_services = sorted(services)
expected_services = reorder(services, expected_order)
self.assertListEqual(sorted_services, expected_services)

def test_trust_zone_default(self) -> None:
s = Service(name='my-test-service')
self.assertIsNone(s.trust_zone)

def test_trust_zone_setter(self) -> None:
s = Service(name='my-test-service', trust_zone='internal-vpc')
self.assertEqual('internal-vpc', s.trust_zone)
s.trust_zone = 'public-internet'
self.assertEqual('public-internet', s.trust_zone)

def test_trust_zone_affects_equality_and_sorted_set_membership(self) -> None:
s1 = Service(name='my-test-service', trust_zone='internal-vpc')
s2 = Service(name='my-test-service', trust_zone='public-internet')
self.assertNotEqual(s1, s2)
self.assertEqual(2, len(SortedSet((s1, s2))))