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
34 changes: 23 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,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')
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion tests/_data/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')],
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-vpc",
"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>
<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-vpc",
"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-vpc",
"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
29 changes: 29 additions & 0 deletions tests/test_deserialize_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 (
Expand Down Expand Up @@ -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(),
Expand Down
14 changes: 14 additions & 0 deletions tests/test_deserialize_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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("""<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.7" version="1">
<services>
<service bom-ref="svc-ref">
<name>svc</name>
<trustZone>internal-vpc</trustZone>
</service>
</services>
</bom>
"""))
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(),
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))))
53 changes: 53 additions & 0 deletions tests/test_output_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down
60 changes: 60 additions & 0 deletions tests/test_output_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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('<trustZone>internal-vpc</trustZone>', 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('<trustZone>', 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('<trustZone>internal vpc zone</trustZone>', 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('</service>'))
mutated_output = output.replace(
'</service>',
' <trustZone>internal-vpc</trustZone>\n </service>',
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):
Expand Down