Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support services data object #683

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion cyclonedx/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
275 changes: 270 additions & 5 deletions cyclonedx/model/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@

from .._internal.compare import ComparableTuple as _ComparableTuple
from ..schema.schema import SchemaVersion1Dot3, SchemaVersion1Dot4, SchemaVersion1Dot5, SchemaVersion1Dot6
from . import DataClassification, ExternalReference, Property, XsUri
from . import DataFlow, ExternalReference, Property, XsUri

# DataClassification,
from .bom_ref import BomRef
from .contact import OrganizationalEntity
from .dependency import Dependable
Expand Down Expand Up @@ -61,7 +63,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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -381,3 +383,266 @@ def __hash__(self) -> int:

def __repr__(self) -> str:
return f'<Service bom-ref={self.bom_ref}, group={self.group}, name={self.name}, version={self.version}>'


@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, Data):
return hash(other) == hash(self)
return False

def __lt__(self, other: object) -> bool:
if isinstance(other, Data):
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'<Data flow={self.flow}>'
10 changes: 5 additions & 5 deletions tests/_data/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -558,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',
Expand All @@ -566,7 +566,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=[
Expand Down Expand Up @@ -594,7 +594,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=[
Expand Down
5 changes: 3 additions & 2 deletions tests/test_enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down