diff --git a/cyclonedx/model/service.py b/cyclonedx/model/service.py
index 10597370..ee34aaa2 100644
--- a/cyclonedx/model/service.py
+++ b/cyclonedx/model/service.py
@@ -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,
@@ -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 []
@@ -239,16 +241,26 @@ 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)
+ @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
+ def trust_zone(self) -> Optional[str]:
+ """
+ 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')
@@ -369,7 +381,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:
diff --git a/tests/_data/models.py b/tests/_data/models.py
index 55a5cdb9..7f732836 100644
--- a/tests/_data/models.py
+++ b/tests/_data/models.py
@@ -614,7 +614,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-vpc', data=[
DataClassification(flow=DataFlow.OUTBOUND, classification='public')
],
licenses=[DisjunctiveLicense(name='Commercial')],
diff --git a/tests/_data/snapshots/get_bom_with_services_complex-1.5.json.bin b/tests/_data/snapshots/get_bom_with_services_complex-1.5.json.bin
index 7672db57..dd798d1c 100644
--- a/tests/_data/snapshots/get_bom_with_services_complex-1.5.json.bin
+++ b/tests/_data/snapshots/get_bom_with_services_complex-1.5.json.bin
@@ -154,6 +154,7 @@
"title": "Release Notes Title",
"type": "major"
},
+ "trustZone": "internal-vpc",
"version": "1.2.3",
"x-trust-boundary": true
},
diff --git a/tests/_data/snapshots/get_bom_with_services_complex-1.5.xml.bin b/tests/_data/snapshots/get_bom_with_services_complex-1.5.xml.bin
index 7fb7fc50..65e78a5d 100644
--- a/tests/_data/snapshots/get_bom_with_services_complex-1.5.xml.bin
+++ b/tests/_data/snapshots/get_bom_with_services_complex-1.5.xml.bin
@@ -33,6 +33,7 @@
false
true
+ internal-vpc
public
diff --git a/tests/_data/snapshots/get_bom_with_services_complex-1.6.json.bin b/tests/_data/snapshots/get_bom_with_services_complex-1.6.json.bin
index 45b78218..9ea9865a 100644
--- a/tests/_data/snapshots/get_bom_with_services_complex-1.6.json.bin
+++ b/tests/_data/snapshots/get_bom_with_services_complex-1.6.json.bin
@@ -160,6 +160,7 @@
"title": "Release Notes Title",
"type": "major"
},
+ "trustZone": "internal-vpc",
"version": "1.2.3",
"x-trust-boundary": true
},
diff --git a/tests/_data/snapshots/get_bom_with_services_complex-1.6.xml.bin b/tests/_data/snapshots/get_bom_with_services_complex-1.6.xml.bin
index 7a054cfa..ccac1c2d 100644
--- a/tests/_data/snapshots/get_bom_with_services_complex-1.6.xml.bin
+++ b/tests/_data/snapshots/get_bom_with_services_complex-1.6.xml.bin
@@ -39,6 +39,7 @@
false
true
+ internal-vpc
public
diff --git a/tests/_data/snapshots/get_bom_with_services_complex-1.7.json.bin b/tests/_data/snapshots/get_bom_with_services_complex-1.7.json.bin
index 9aa33fa2..54206c3c 100644
--- a/tests/_data/snapshots/get_bom_with_services_complex-1.7.json.bin
+++ b/tests/_data/snapshots/get_bom_with_services_complex-1.7.json.bin
@@ -160,6 +160,7 @@
"title": "Release Notes Title",
"type": "major"
},
+ "trustZone": "internal-vpc",
"version": "1.2.3",
"x-trust-boundary": true
},
diff --git a/tests/_data/snapshots/get_bom_with_services_complex-1.7.xml.bin b/tests/_data/snapshots/get_bom_with_services_complex-1.7.xml.bin
index 770f7a84..760dcfe1 100644
--- a/tests/_data/snapshots/get_bom_with_services_complex-1.7.xml.bin
+++ b/tests/_data/snapshots/get_bom_with_services_complex-1.7.xml.bin
@@ -39,6 +39,7 @@
false
true
+ internal-vpc
public
diff --git a/tests/test_deserialize_json.py b/tests/test_deserialize_json.py
index 85f8b11a..9aa53183 100644
--- a/tests/test_deserialize_json.py
+++ b/tests/test_deserialize_json.py
@@ -17,6 +17,7 @@
from collections.abc import Callable
+from io import StringIO
from json import loads as json_loads
from os.path import join
from typing import Any
@@ -27,6 +28,9 @@
from cyclonedx.model.bom import Bom
from cyclonedx.model.license import DisjunctiveLicense, LicenseExpression, LicenseRepository
+from cyclonedx.model.service import Service
+from cyclonedx.output.json import BY_SCHEMA_VERSION as JSON_BY_SCHEMA_VERSION
+from cyclonedx.output.xml import BY_SCHEMA_VERSION as XML_BY_SCHEMA_VERSION
from cyclonedx.schema import OutputFormat, SchemaVersion
from tests import OWN_DATA_DIRECTORY, DeepCompareMixin, SnapshotMixin, mksname
from tests._data.models import (
@@ -130,6 +134,31 @@ def test_regression_issue690(self) -> None:
bom: Bom = Bom.from_json(json) # <<< is expected to not crash
self.assertIsNotNone(bom)
+ def test_service_trust_zone_from_json(self) -> None:
+ bom = Bom.from_json({
+ 'bomFormat': 'CycloneDX',
+ 'specVersion': '1.7',
+ 'version': 1,
+ 'services': [
+ {
+ 'bom-ref': 'svc-ref',
+ 'name': 'svc',
+ 'trustZone': 'internal-vpc',
+ }
+ ],
+ })
+ self.assertEqual('internal-vpc', next(iter(bom.services)).trust_zone)
+
+ def test_service_trust_zone_json_to_xml_roundtrip(self) -> None:
+ bom = Bom(services=[
+ Service(name='svc', bom_ref='svc-ref', trust_zone='internal-vpc')
+ ])
+ json_text = JSON_BY_SCHEMA_VERSION[SchemaVersion.V1_7](bom).output_as_string()
+ json_bom = Bom.from_json(json_loads(json_text))
+ xml_text = XML_BY_SCHEMA_VERSION[SchemaVersion.V1_7](json_bom).output_as_string()
+ xml_bom = Bom.from_xml(StringIO(xml_text))
+ self.assertEqual('internal-vpc', next(iter(xml_bom.services)).trust_zone)
+
def test_component_evidence_identity(self) -> None:
json_file = join(OWN_DATA_DIRECTORY, 'json',
SchemaVersion.V1_6.to_version(),
diff --git a/tests/test_deserialize_xml.py b/tests/test_deserialize_xml.py
index 8ad0f31c..2115ce5b 100644
--- a/tests/test_deserialize_xml.py
+++ b/tests/test_deserialize_xml.py
@@ -16,6 +16,7 @@
# Copyright (c) OWASP Foundation. All Rights Reserved.
from collections.abc import Callable
+from io import StringIO
from os.path import join
from typing import Any
from unittest import TestCase
@@ -50,6 +51,19 @@ def test_prepared(self, get_bom: Callable[[], Bom], *_: Any, **__: Any) -> None:
self.assertBomDeepEqual(expected, bom,
fuzzy_deps=get_bom in all_get_bom_funct_with_incomplete_deps)
+ def test_service_trust_zone_from_xml(self) -> None:
+ bom = Bom.from_xml(StringIO("""
+
+
+
+ svc
+ internal-vpc
+
+
+
+"""))
+ self.assertEqual('internal-vpc', next(iter(bom.services)).trust_zone)
+
def test_component_evidence_identity(self) -> None:
xml_file = join(OWN_DATA_DIRECTORY, 'xml',
SchemaVersion.V1_6.to_version(),
diff --git a/tests/test_model_service.py b/tests/test_model_service.py
index c66c2521..94f87da5 100644
--- a/tests/test_model_service.py
+++ b/tests/test_model_service.py
@@ -18,6 +18,8 @@
from unittest import TestCase
+from sortedcontainers import SortedSet
+
from cyclonedx.model.service import Service
from tests import reorder
@@ -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)
@@ -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)
@@ -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))))
diff --git a/tests/test_output_json.py b/tests/test_output_json.py
index b9340a4e..8ccd1cd4 100644
--- a/tests/test_output_json.py
+++ b/tests/test_output_json.py
@@ -18,6 +18,7 @@
import re
from collections.abc import Callable
+from json import dumps, loads
from typing import Any
from unittest import TestCase
from unittest.mock import Mock, patch
@@ -34,6 +35,7 @@
)
from cyclonedx.exception.output import FormatNotSupportedException
from cyclonedx.model.bom import Bom
+from cyclonedx.model.service import Service
from cyclonedx.output.json import BY_SCHEMA_VERSION, Json
from cyclonedx.schema import OutputFormat, SchemaVersion
from cyclonedx.validation.json import JsonStrictValidator
@@ -105,6 +107,57 @@ def test_bomref_not_duplicate(self) -> None:
self.assertEqual(nr_bomrefs, len(found))
self.assertCountEqual(set(found), found, 'expected unique items')
+ def test_service_trust_zone_by_schema_version(self) -> None:
+ bom = Bom(services=[
+ Service(name='svc', bom_ref='svc-ref', trust_zone='internal-vpc')
+ ])
+ supported_schema_versions = (
+ SchemaVersion.V1_5,
+ SchemaVersion.V1_6,
+ SchemaVersion.V1_7,
+ )
+ unsupported_schema_versions = (
+ SchemaVersion.V1_2,
+ SchemaVersion.V1_3,
+ SchemaVersion.V1_4,
+ )
+ for sv in supported_schema_versions:
+ with self.subTest(schema_version=sv):
+ output = BY_SCHEMA_VERSION[sv](bom).output_as_string()
+ service = loads(output)['services'][0]
+ self.assertEqual('internal-vpc', service['trustZone'])
+ self.assertNotIn('trust_zone', service)
+ try:
+ errors = JsonStrictValidator(sv).validate_str(output)
+ except MissingOptionalDependencyException:
+ self.skipTest('MissingOptionalDependencyException')
+ self.assertIsNone(errors, output)
+ for sv in unsupported_schema_versions:
+ with self.subTest(schema_version=sv):
+ output = BY_SCHEMA_VERSION[sv](bom).output_as_string()
+ service = loads(output)['services'][0]
+ self.assertNotIn('trustZone', service)
+ self.assertNotIn('trust_zone', service)
+ try:
+ errors = JsonStrictValidator(sv).validate_str(output)
+ except MissingOptionalDependencyException:
+ self.skipTest('MissingOptionalDependencyException')
+ self.assertIsNone(errors, output)
+
+ def test_service_trust_zone_rejected_before_15(self) -> None:
+ bom = Bom(services=[
+ Service(name='svc', bom_ref='svc-ref', trust_zone='internal-vpc')
+ ])
+ output = BY_SCHEMA_VERSION[SchemaVersion.V1_4](bom).output_as_string()
+ data = loads(output)
+ data['services'][0]['trustZone'] = 'internal-vpc'
+ mutated_output = dumps(data)
+ try:
+ errors = JsonStrictValidator(SchemaVersion.V1_4).validate_str(mutated_output)
+ except MissingOptionalDependencyException:
+ self.skipTest('MissingOptionalDependencyException')
+ self.assertIsNotNone(errors)
+
@ddt
class TestFunctionalBySchemaVersion(TestCase):
diff --git a/tests/test_output_xml.py b/tests/test_output_xml.py
index 6e887ded..4d00f1d8 100644
--- a/tests/test_output_xml.py
+++ b/tests/test_output_xml.py
@@ -32,6 +32,7 @@
UnknownComponentDependencyException,
)
from cyclonedx.model.bom import Bom
+from cyclonedx.model.service import Service
from cyclonedx.output.xml import BY_SCHEMA_VERSION, Xml
from cyclonedx.schema import OutputFormat, SchemaVersion
from cyclonedx.validation.xml import XmlValidator
@@ -93,6 +94,65 @@ def test_bomref_not_duplicate(self) -> None:
self.assertEqual(nr_bomrefs, len(found))
self.assertCountEqual(set(found), found, 'expected unique items')
+ def test_service_trust_zone_by_schema_version(self) -> None:
+ bom = Bom(services=[
+ Service(name='svc', bom_ref='svc-ref', trust_zone='internal-vpc')
+ ])
+ supported_schema_versions = (
+ SchemaVersion.V1_5,
+ SchemaVersion.V1_6,
+ SchemaVersion.V1_7,
+ )
+ unsupported_schema_versions = (
+ SchemaVersion.V1_2,
+ SchemaVersion.V1_3,
+ SchemaVersion.V1_4,
+ )
+ for sv in supported_schema_versions:
+ with self.subTest(schema_version=sv):
+ output = BY_SCHEMA_VERSION[sv](bom).output_as_string()
+ self.assertIn('internal-vpc', output)
+ try:
+ errors = XmlValidator(sv).validate_str(output)
+ except MissingOptionalDependencyException:
+ self.skipTest('MissingOptionalDependencyException')
+ self.assertIsNone(errors, output)
+ for sv in unsupported_schema_versions:
+ with self.subTest(schema_version=sv):
+ output = BY_SCHEMA_VERSION[sv](bom).output_as_string()
+ self.assertNotIn('', output)
+ try:
+ errors = XmlValidator(sv).validate_str(output)
+ except MissingOptionalDependencyException:
+ self.skipTest('MissingOptionalDependencyException')
+ self.assertIsNone(errors, output)
+
+ def test_service_trust_zone_xml_string_is_normalized(self) -> None:
+ bom = Bom(services=[
+ Service(name='svc', bom_ref='svc-ref', trust_zone='internal\tvpc\nzone')
+ ])
+ output = BY_SCHEMA_VERSION[SchemaVersion.V1_7](bom).output_as_string()
+ self.assertIn('internal vpc zone', output)
+
+ def test_service_trust_zone_rejected_before_15(self) -> None:
+ bom = Bom(services=[
+ Service(name='svc', bom_ref='svc-ref', trust_zone='internal-vpc')
+ ])
+ output = BY_SCHEMA_VERSION[SchemaVersion.V1_4](bom).output_as_string()
+ self.assertEqual(1, output.count(''))
+ mutated_output = output.replace(
+ '',
+ ' internal-vpc\n ',
+ 1
+ )
+ self.assertNotEqual(output, mutated_output)
+ try:
+ errors = XmlValidator(SchemaVersion.V1_4).validate_str(mutated_output)
+ except MissingOptionalDependencyException:
+ self.skipTest('MissingOptionalDependencyException')
+ self.assertIsNotNone(errors)
+ self.assertIn('trustZone', str(errors))
+
@ddt
class TestFunctionalBySchemaVersion(TestCase):