From 55bab7bb13903ce6f9b1d84966846abc830ee900 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Mon, 23 Sep 2024 09:43:50 +0200 Subject: [PATCH 1/4] add service data classes Signed-off-by: luca.morgese@tno.nl --- cyclonedx/model/__init__.py | 2 +- cyclonedx/model/service.py | 265 +++++++++++++++++++++++++++++++++++- 2 files changed, 265 insertions(+), 2 deletions(-) diff --git a/cyclonedx/model/__init__.py b/cyclonedx/model/__init__.py index 213fdcf2..6c8926a4 100644 --- a/cyclonedx/model/__init__.py +++ b/cyclonedx/model/__init__.py @@ -60,7 +60,7 @@ class DataFlow(str, Enum): This is our internal representation of the dataFlowType simple type within the CycloneDX standard. .. note:: - See the CycloneDX Schema: https://cyclonedx.org/docs/1.4/xml/#type_dataFlowType + See the CycloneDX Schema: https://cyclonedx.org/docs/1.6/xml/#type_dataFlowType """ INBOUND = 'inbound' OUTBOUND = 'outbound' diff --git a/cyclonedx/model/service.py b/cyclonedx/model/service.py index 46ce6c29..3c7a2594 100644 --- a/cyclonedx/model/service.py +++ b/cyclonedx/model/service.py @@ -33,7 +33,7 @@ from .._internal.compare import ComparableTuple as _ComparableTuple from ..schema.schema import SchemaVersion1Dot3, SchemaVersion1Dot4, SchemaVersion1Dot5, SchemaVersion1Dot6 -from . import DataClassification, ExternalReference, Property, XsUri +from . import DataClassification, DataFlow, ExternalReference, Property, XsUri from .bom_ref import BomRef from .contact import OrganizationalEntity from .dependency import Dependable @@ -381,3 +381,266 @@ def __hash__(self) -> int: def __repr__(self) -> str: return f'' + + +@serializable.serializable_class +class OrganizationOrIndividualType: + """ + This is our internal representation of the organizationOrIndividualType complex type within the CycloneDX standard. + + .. note:: + See the CycloneDX Schema: https://cyclonedx.org/docs/1.6/xml/#type_organizationOrIndividualType + """ + + def __init__( + self, *, + organization: Optional[OrganizationalEntity] = None, + individual: Optional[OrganizationalEntity] = None, + ) -> None: + self.organization = organization + self.individual = individual + + # Property for organization + @property + @serializable.xml_sequence(1) + @serializable.xml_name('organization') + def organization(self) -> Optional[OrganizationalEntity]: + return self._organization + + @organization.setter + def organization(self, organization: Optional[OrganizationalEntity]) -> None: + self._organization = organization + + # Property for individual + @property + @serializable.xml_sequence(2) + @serializable.xml_name('individual') + def individual(self) -> Optional[OrganizationalEntity]: + return self._individual + + @individual.setter + def individual(self, individual: Optional[OrganizationalEntity]) -> None: + self._individual = individual + + +@serializable.serializable_class +class DataGovernance: + """ + This is our internal representation of the dataGovernance complex type within the CycloneDX standard. + + .. note:: + See the CycloneDX Schema: https://cyclonedx.org/docs/1.6/xml/#type_dataGovernance + """ + + def __init__( + self, *, + custodian: Optional[OrganizationOrIndividualType] = None, + steward: Optional[OrganizationOrIndividualType] = None, + owner: Optional[OrganizationOrIndividualType] = None, + ) -> None: + self.custodian = custodian + self.steward = steward + self.owner = owner + + # Property for custodian + @property + @serializable.xml_sequence(1) + @serializable.xml_name('custodian') + def custodian(self) -> Optional[OrganizationOrIndividualType]: + return self._custodian + + @custodian.setter + def custodian(self, custodian: Optional[OrganizationOrIndividualType]) -> None: + self._custodian = custodian + + # Property for steward + @property + @serializable.xml_sequence(2) + @serializable.xml_name('steward') + def steward(self) -> Optional[OrganizationOrIndividualType]: + return self._steward + + @steward.setter + def steward(self, steward: Optional[OrganizationOrIndividualType]) -> None: + self._steward = steward + + # Property for owner + @property + @serializable.xml_sequence(3) + @serializable.xml_name('owner') + def owner(self) -> Optional[OrganizationOrIndividualType]: + return self._owner + + @owner.setter + def owner(self, owner: Optional[OrganizationOrIndividualType]) -> None: + self._owner = owner + + +@serializable.serializable_class +class Data: + """ + This is our internal representation of the service.data complex type within the CycloneDX standard. + + .. note:: + See the CycloneDX Schema: https://cyclonedx.org/docs/1.6/xml/#type_service + """ + # @serializable.xml_string(serializable.XmlStringSerializationType.STRING) + + def __init__( + self, *, + flow: DataFlow, + classification: str, + name: Optional[str] = None, + description: Optional[str] = None, + governance: Optional[DataGovernance] = None, + source: Optional[Iterable[Union[BomRef, XsUri]]] = None, + destination: Optional[Iterable[Union[BomRef, XsUri]]] = None + ) -> None: + self.flow = flow + self.classification = classification + self.name = name + self.description = description + self.governance = governance + self.source = source + self.destination = destination + + @property + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + def name(self) -> Optional[str]: + """ + The name of the service data. + + Returns: + `str` if provided else None + """ + return self._name + + @name.setter + def name(self, name: Optional[str]) -> None: + self._name = name + + @property + @serializable.xml_attribute() + def flow(self) -> DataFlow: + """ + Specifies the flow direction of the data. + + Valid values are: inbound, outbound, bi-directional, and unknown. + + Direction is relative to the service. + + - Inbound flow states that data enters the service + - Outbound flow states that data leaves the service + - Bi-directional states that data flows both ways + - Unknown states that the direction is not known + + Returns: + `DataFlow` + """ + return self._flow + + @flow.setter + def flow(self, flow: DataFlow) -> None: + self._flow = flow + + @property + @serializable.xml_name('.') + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + def classification(self) -> str: + """ + Data classification tags data according to its type, sensitivity, and value if altered, stolen, or destroyed. + + Returns: + `str` + """ + return self._classification + + @classification.setter + def classification(self, classification: str) -> None: + self._classification = classification + + # description property + + @property + @serializable.xml_sequence(2) # Assuming order after name + @serializable.xml_string(serializable.XmlStringSerializationType.STRING) + def description(self) -> Optional[str]: + """ + The description of the service data. + + Returns: + `str` if provided else None + """ + return self._description + + @description.setter + def description(self, description: Optional[str]) -> None: + self._description = description + + # governance property + @property + @serializable.xml_sequence(3) # Assuming order after description + def governance(self) -> Optional[DataGovernance]: + """ + Governance information for the service data. + + Returns: + `DataGovernance` if provided else None + """ + return self._governance + + @governance.setter + def governance(self, governance: Optional[DataGovernance]) -> None: + self._governance = governance + + # source property + @property + @serializable.xml_sequence(4) # Assuming order after governance + def source(self) -> Optional[Iterable[Union[BomRef, XsUri]]]: + """ + The source(s) of the service data. + + Returns: + Iterable of `BomRef` or `XsUri` if provided else None + """ + return self._source + + @source.setter + def source(self, source: Optional[Iterable[Union[BomRef, XsUri]]]) -> None: + self._source = source + + # destination property + @property + @serializable.xml_sequence(5) # Assuming order after source + def destination(self) -> Optional[Iterable[Union[BomRef, XsUri]]]: + """ + The destination(s) of the service data. + + Returns: + Iterable of `BomRef` or `XsUri` if provided else None + """ + return self._destination + + @destination.setter + def destination(self, destination: Optional[Iterable[Union[BomRef, XsUri]]]) -> None: + self._destination = destination + + def __eq__(self, other: object) -> bool: + if isinstance(other, DataClassification): + return hash(other) == hash(self) + return False + + def __lt__(self, other: object) -> bool: + if isinstance(other, DataClassification): + return _ComparableTuple(( + self.flow, self.classification + )) < _ComparableTuple(( + other.flow, other.classification + )) + return NotImplemented + + def __hash__(self) -> int: + return hash((self.flow, self.classification)) + + def __repr__(self) -> str: + return f'' From 58a56345d8b67c8d97664a0dd1a4d2897ca8026a Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Mon, 23 Sep 2024 10:04:53 +0200 Subject: [PATCH 2/4] change dataclassification to data in enum test Signed-off-by: luca.morgese@tno.nl --- cyclonedx/model/service.py | 6 +++--- tests/test_enums.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cyclonedx/model/service.py b/cyclonedx/model/service.py index 3c7a2594..f0f7e8ff 100644 --- a/cyclonedx/model/service.py +++ b/cyclonedx/model/service.py @@ -61,7 +61,7 @@ def __init__( endpoints: Optional[Iterable[XsUri]] = None, authenticated: Optional[bool] = None, x_trust_boundary: Optional[bool] = None, - data: Optional[Iterable[DataClassification]] = None, + data: Optional[Iterable['Data']] = None, licenses: Optional[Iterable[License]] = None, external_references: Optional[Iterable[ExternalReference]] = None, properties: Optional[Iterable[Property]] = None, @@ -626,12 +626,12 @@ def destination(self, destination: Optional[Iterable[Union[BomRef, XsUri]]]) -> self._destination = destination def __eq__(self, other: object) -> bool: - if isinstance(other, DataClassification): + if isinstance(other, Data): return hash(other) == hash(self) return False def __lt__(self, other: object) -> bool: - if isinstance(other, DataClassification): + if isinstance(other, Data): return _ComparableTuple(( self.flow, self.classification )) < _ComparableTuple(( diff --git a/tests/test_enums.py b/tests/test_enums.py index c963c499..22a297df 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -34,7 +34,7 @@ from cyclonedx.model.component import Component, Patch, Pedigree from cyclonedx.model.issue import IssueType from cyclonedx.model.license import DisjunctiveLicense -from cyclonedx.model.service import DataClassification, Service +from cyclonedx.model.service import Data, Service from cyclonedx.model.vulnerability import ( BomTarget, BomTargetVersionRange, @@ -168,7 +168,8 @@ def test_knows_value(self, value: str) -> None: @patch('cyclonedx.model.ThisTool._version', 'TESTING') def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, **__: Any) -> None: bom = _make_bom(services=[Service(name='dummy', bom_ref='dummy', data=( - DataClassification(flow=df, classification=df.name) + Data(flow=df, classification=df.name) + # DataClassification(flow=df, classification=df.name) for df in DataFlow ))]) super()._test_cases_render(bom, of, sv) From 273932bfc8b50a149d477e4e1015feb85f8300cb Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Mon, 23 Sep 2024 10:15:55 +0200 Subject: [PATCH 3/4] use data instead of dataclassification for service in tests _data models Signed-off-by: luca.morgese@tno.nl --- cyclonedx/model/service.py | 12 +++++++----- tests/_data/models.py | 9 ++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/cyclonedx/model/service.py b/cyclonedx/model/service.py index f0f7e8ff..dbbd1fe0 100644 --- a/cyclonedx/model/service.py +++ b/cyclonedx/model/service.py @@ -33,7 +33,9 @@ from .._internal.compare import ComparableTuple as _ComparableTuple from ..schema.schema import SchemaVersion1Dot3, SchemaVersion1Dot4, SchemaVersion1Dot5, SchemaVersion1Dot6 -from . import DataClassification, DataFlow, ExternalReference, Property, XsUri +from . import DataFlow, ExternalReference, Property, XsUri + +# DataClassification, from .bom_ref import BomRef from .contact import OrganizationalEntity from .dependency import Dependable @@ -252,18 +254,18 @@ def x_trust_boundary(self, x_trust_boundary: Optional[bool]) -> None: @property @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'classification') @serializable.xml_sequence(10) - def data(self) -> 'SortedSet[DataClassification]': + def data(self) -> 'SortedSet[Data]': """ Specifies the data classification. Returns: - Set of `DataClassification` + Set of `Data` """ # TODO since CDX1.5 also supports `dataflow`, not only `DataClassification` return self._data @data.setter - def data(self, data: Iterable[DataClassification]) -> None: + def data(self, data: Iterable['Data']) -> None: self._data = SortedSet(data) @property @@ -643,4 +645,4 @@ def __hash__(self) -> int: return hash((self.flow, self.classification)) def __repr__(self) -> str: - return f'' + return f'' diff --git a/tests/_data/models.py b/tests/_data/models.py index c0c092f1..17034a0f 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -26,10 +26,9 @@ # See https://github.com/package-url/packageurl-python/issues/65 from packageurl import PackageURL -from cyclonedx.model import ( +from cyclonedx.model import ( # DataClassification, AttachedText, Copyright, - DataClassification, DataFlow, Encoding, ExternalReference, @@ -88,7 +87,7 @@ from cyclonedx.model.issue import IssueClassification, IssueType, IssueTypeSource from cyclonedx.model.license import DisjunctiveLicense, License, LicenseAcknowledgement, LicenseExpression from cyclonedx.model.release_note import ReleaseNotes -from cyclonedx.model.service import Service +from cyclonedx.model.service import Data, Service from cyclonedx.model.vulnerability import ( BomTarget, BomTargetVersionRange, @@ -566,7 +565,7 @@ def get_bom_with_services_complex() -> Bom: XsUri('/api/thing/2') ], authenticated=False, x_trust_boundary=True, data=[ - DataClassification(flow=DataFlow.OUTBOUND, classification='public') + Data(flow=DataFlow.OUTBOUND, classification='public') ], licenses=[DisjunctiveLicense(name='Commercial')], external_references=[ @@ -594,7 +593,7 @@ def get_bom_with_nested_services() -> Bom: XsUri('/api/thing/2') ], authenticated=False, x_trust_boundary=True, data=[ - DataClassification(flow=DataFlow.OUTBOUND, classification='public') + Data(flow=DataFlow.OUTBOUND, classification='public') ], licenses=[DisjunctiveLicense(name='Commercial')], external_references=[ From 58a93535eb1c4db12dbc71cef289f421b507b48c Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Mon, 23 Sep 2024 11:49:08 +0200 Subject: [PATCH 4/4] add todo note Signed-off-by: luca.morgese@tno.nl --- tests/_data/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/_data/models.py b/tests/_data/models.py index 17034a0f..00b3a8f1 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -557,6 +557,7 @@ def get_bom_with_services_simple() -> Bom: def get_bom_with_services_complex() -> Bom: bom = _make_bom(services=[ + # TODO: Add source and destination Service( name='my-first-service', bom_ref='my-specific-bom-ref-for-my-first-service', provider=get_org_entity_1(), group='a-group', version='1.2.3',