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

RELEASE 2.0.0 #148

Merged
merged 30 commits into from
Feb 21, 2022
Merged
Changes from 14 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b45ff18
WIP on `bom.services`
DarthHater Jan 27, 2022
df43a9b
test: refactored fixtures for tests which has uncovered #150, #151 an…
madpah Jan 27, 2022
15b081b
fix: `expression` not supported in Component Licsnes for version 1.0
madpah Jan 27, 2022
70d25c8
fix: Components with no version (optional since 1.4) produce invalid …
madpah Jan 27, 2022
c09e396
fix: regression introduced by first fix for #150
madpah Jan 27, 2022
1f55f3e
fix: further fix for #150
madpah Jan 27, 2022
a35d540
removed unused imports
madpah Jan 27, 2022
9edf6c9
feat: support services in XML BOMs
madpah Jan 31, 2022
2090c08
attempt to resolve Lift finding
madpah Jan 31, 2022
a51766d
fix: temporary fix for `__hash__` of Component with `properties` #153
madpah Jan 31, 2022
0ce5de6
test: refactor to work on PY < 3.10
madpah Jan 31, 2022
32c0139
feat: Complete support for `bom.components` (#155)
madpah Feb 2, 2022
1b733d7
feat: support for `bom.externalReferences` in JSON and XML #124
madpah Feb 2, 2022
6c280e7
chore: bump dependencies
madpah Feb 2, 2022
41a4be0
doc: added page to docs to call out which parts of the specification …
madpah Feb 2, 2022
b3c8d9a
BREAKING CHANGE: adopted PEP-3102 for model classes (#158)
madpah Feb 3, 2022
0f1fd6d
removed unnecessary calls to `hash()` in `__hash__()` methods as poin…
madpah Feb 3, 2022
142b8bf
BREAKING CHANGE: update models to use `Set` rather than `List` (#160)
madpah Feb 8, 2022
2938a6c
feat: support complete model for `bom.metadata` (#162)
madpah Feb 8, 2022
9b6ce4b
BREAKING CHANGE: Updated default schema version to 1.4 from 1.3 (#164)
madpah Feb 8, 2022
5c954d1
fix: `Component.bom_ref` is not Optional in our model implementation …
madpah Feb 15, 2022
a926b34
feat: completed work on #155 (#172)
madpah Feb 16, 2022
020fcf0
BREAKING CHANGE: replaced concept of default schema version with late…
madpah Feb 16, 2022
d189f2c
BREAKING CHANGE: added new model `BomRef` unlocking logic later to en…
madpah Feb 17, 2022
0d82c01
Continuation of #170 - missed updating Vulnerability to use `BomRef` …
madpah Feb 17, 2022
670bde4
implemented `__str__` for `BomRef`
madpah Feb 21, 2022
f014d7c
fix: `license_url` not serialised in XML output #179 (#180)
madpah Feb 21, 2022
b20d9d1
doc: added RTD badge to README
madpah Feb 21, 2022
da3f0ca
feat: bump dependencies
madpah Feb 21, 2022
9a32351
Merge branch 'main' into feat/add-bom-services
madpah Feb 21, 2022
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
7 changes: 4 additions & 3 deletions cyclonedx/exception/output.py
Original file line number Diff line number Diff line change
@@ -22,9 +22,10 @@
from . import CycloneDxException


class ComponentVersionRequiredException(CycloneDxException):
class FormatNotSupportedException(CycloneDxException):
"""
Exception raised when attempting to output to an SBOM version that mandates a Component has a version,
but one is not available/present.
Exception raised when attempting to output a BOM to a format not supported in the requested version.

For example, JSON is not supported prior to 1.2.
"""
pass
340 changes: 334 additions & 6 deletions cyclonedx/model/__init__.py

Large diffs are not rendered by default.

172 changes: 156 additions & 16 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
@@ -18,11 +18,12 @@
# Copyright (c) OWASP Foundation. All Rights Reserved.

from datetime import datetime, timezone
from typing import List, Optional
from typing import cast, List, Optional
from uuid import uuid4, UUID

from . import ThisTool, Tool
from . import ExternalReference, ThisTool, Tool
from .component import Component
from .service import Service
from ..parser import BaseParser


@@ -106,6 +107,21 @@ def component(self, component: Component) -> None:
"""
self._component = component

def __eq__(self, other: object) -> bool:
if isinstance(other, BomMetaData):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((
self.timestamp,
tuple([hash(tool) for tool in set(sorted(self.tools, key=hash))]) if self.tools else None,
hash(self.component)
))

def __repr__(self) -> str:
return f'<BomMetaData timestamp={self.timestamp.utcnow()}>'


class Bom:
"""
@@ -133,7 +149,8 @@ def from_parser(parser: BaseParser) -> 'Bom':
bom.add_components(parser.get_components())
return bom

def __init__(self) -> None:
def __init__(self, components: Optional[List[Component]] = None, services: Optional[List[Service]] = None,
external_references: Optional[List[ExternalReference]] = None) -> None:
"""
Create a new Bom that you can manually/programmatically add data to later.
@@ -142,7 +159,9 @@ def __init__(self) -> None:
"""
self.uuid = uuid4()
self.metadata = BomMetaData()
self._components: List[Component] = []
self.components = components
self.services = services
self.external_references = external_references

@property
def uuid(self) -> UUID:
@@ -176,17 +195,17 @@ def metadata(self, metadata: BomMetaData) -> None:
self._metadata = metadata

@property
def components(self) -> List[Component]:
def components(self) -> Optional[List[Component]]:
"""
Get all the Components currently in this Bom.
Returns:
List of all Components in this Bom.
List of all Components in this Bom or `None`
"""
return self._components

@components.setter
def components(self, components: List[Component]) -> None:
def components(self, components: Optional[List[Component]]) -> None:
self._components = components

def add_component(self, component: Component) -> None:
@@ -200,8 +219,10 @@ def add_component(self, component: Component) -> None:
Returns:
None
"""
if not self.has_component(component=component):
self._components.append(component)
if not self.components:
self.components = [component]
elif not self.has_component(component=component):
self.components.append(component)

def add_components(self, components: List[Component]) -> None:
"""
@@ -214,7 +235,7 @@ def add_components(self, components: List[Component]) -> None:
Returns:
None
"""
self.components = self._components + components
self.components = (self._components or []) + components

def component_count(self) -> int:
"""
@@ -223,7 +244,7 @@ def component_count(self) -> int:
Returns:
The number of Components in this Bom as `int`.
"""
return len(self._components)
return len(self._components) if self._components else 0

def get_component_by_purl(self, purl: Optional[str]) -> Optional[Component]:
"""
@@ -236,8 +257,11 @@ def get_component_by_purl(self, purl: Optional[str]) -> Optional[Component]:
Returns:
`Component` or `None`
"""
if not self._components:
return None

if purl:
found = list(filter(lambda x: x.purl == purl, self.components))
found = list(filter(lambda x: x.purl == purl, cast(List[Component], self.components)))
if len(found) == 1:
return found[0]

@@ -263,7 +287,107 @@ def has_component(self, component: Component) -> bool:
Returns:
`bool` - `True` if the supplied Component is part of this Bom, `False` otherwise.
"""
return component in self._components
if not self.components:
return False
return component in self.components

@property
def services(self) -> Optional[List[Service]]:
"""
Get all the Services currently in this Bom.
Returns:
List of `Service` in this Bom or `None`
"""
return self._services

@services.setter
def services(self, services: Optional[List[Service]]) -> None:
self._services = services

def add_service(self, service: Service) -> None:
"""
Add a Service to this Bom instance.
Args:
service:
`cyclonedx.model.service.Service` instance to add to this Bom.
Returns:
None
"""
if not self.services:
self.services = [service]
elif not self.has_service(service=service):
self.services.append(service)

def add_services(self, services: List[Service]) -> None:
"""
Add multiple Services at once to this Bom instance.
Args:
services:
List of `cyclonedx.model.service.Service` instances to add to this Bom.
Returns:
None
"""
self.services = (self.services or []) + services

def has_service(self, service: Service) -> bool:
"""
Check whether this Bom contains the provided Service.
Args:
service:
The instance of `cyclonedx.model.service.Service` to check if this Bom contains.
Returns:
`bool` - `True` if the supplied Service is part of this Bom, `False` otherwise.
"""
if not self.services:
return False

return service in self.services

def service_count(self) -> int:
"""
Returns the current count of Services within this Bom.
Returns:
The number of Services in this Bom as `int`.
"""
if not self.services:
return 0

return len(self.services)

@property
def external_references(self) -> Optional[List[ExternalReference]]:
"""
Provides the ability to document external references related to the BOM or to the project the BOM describes.
Returns:
List of `ExternalReference` else `None`
"""
return self._external_references

@external_references.setter
def external_references(self, external_references: Optional[List[ExternalReference]]) -> None:
self._external_references = external_references

def add_external_reference(self, external_reference: ExternalReference) -> None:
"""
Add an external reference to this Bom.
Args:
external_reference:
`ExternalReference` to add to this Bom.
Returns:
None
"""
self.external_references = (self.external_references or []) + [external_reference]

def has_vulnerabilities(self) -> bool:
"""
@@ -273,8 +397,24 @@ def has_vulnerabilities(self) -> bool:
`bool` - `True` if at least one `cyclonedx.model.component.Component` has at least one Vulnerability,
`False` otherwise.
"""
for c in self.components:
if c.has_vulnerabilities():
return True
if self.components:
for c in self.components:
if c.has_vulnerabilities():
return True

return False

def __eq__(self, other: object) -> bool:
if isinstance(other, Bom):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((
self.uuid, hash(self.metadata),
tuple([hash(c) for c in set(sorted(self.components, key=hash))]) if self.components else None,
tuple([hash(s) for s in set(sorted(self.services, key=hash))]) if self.services else None
))

def __repr__(self) -> str:
return f'<Bom uuid={self.uuid}>'
770 changes: 751 additions & 19 deletions cyclonedx/model/component.py

Large diffs are not rendered by default.

27 changes: 26 additions & 1 deletion cyclonedx/model/issue.py
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ class IssueClassification(Enum):
This is out internal representation of the enum `issueClassification`.
.. note::
See the CycloneDX Schema definition: hhttps://cyclonedx.org/docs/1.4/xml/#type_issueClassification
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_issueClassification
"""
DEFECT = 'defect'
ENHANCEMENT = 'enhancement'
@@ -78,6 +78,17 @@ def url(self) -> Optional[XsUri]:
def url(self, url: Optional[XsUri]) -> None:
self._url = url

def __eq__(self, other: object) -> bool:
if isinstance(other, IssueTypeSource):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((self.name, self.url))

def __repr__(self) -> str:
return f'<IssueTypeSource name={self._name}, url={self.url}>'


class IssueType:
"""
@@ -263,3 +274,17 @@ def set_source_url(self, source_url: XsUri) -> None:
self._source.url = source_url
else:
self._source = IssueTypeSource(url=source_url)

def __eq__(self, other: object) -> bool:
if isinstance(other, IssueType):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((
self._type, self._id, self._name, self._description, self._source,
tuple([hash(ref) for ref in set(sorted(self._references, key=hash))]) if self._references else None
))

def __repr__(self) -> str:
return f'<IssueType type={self._type}, id={self._id}, name={self._name}>'
18 changes: 18 additions & 0 deletions cyclonedx/model/release_note.py
Original file line number Diff line number Diff line change
@@ -238,3 +238,21 @@ def properties(self) -> Optional[List[Property]]:
@properties.setter
def properties(self, properties: Optional[List[Property]]) -> None:
self._properties = properties

def __eq__(self, other: object) -> bool:
if isinstance(other, ReleaseNotes):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((
self.type, self.title, self.featured_image, self.social_image, self.description, self.timestamp,
tuple([hash(alias) for alias in set(sorted(self.aliases, key=hash))]) if self.aliases else None,
tuple([hash(tag) for tag in set(sorted(self.tags, key=hash))]) if self.tags else None,
tuple([hash(issue) for issue in set(sorted(self.resolves, key=hash))]) if self.resolves else None,
tuple([hash(note) for note in set(sorted(self.notes, key=hash))]) if self.notes else None,
tuple([hash(prop) for prop in set(sorted(self._properties, key=hash))]) if self._properties else None
))

def __repr__(self) -> str:
return f'<ReleaseNotes type={self.type}, title={self.title}>'
347 changes: 347 additions & 0 deletions cyclonedx/model/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,347 @@
# encoding: utf-8

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0

from typing import List, Optional
from uuid import uuid4

from . import ExternalReference, DataClassification, LicenseChoice, OrganizationalEntity, Property, XsUri # , Signature
from .release_note import ReleaseNotes

"""
This set of classes represents the data that is possible about known Services.
.. note::
See the CycloneDX Schema extension definition https://cyclonedx.org/docs/1.4/xml/#type_servicesType
"""


class Service:
"""
Class that models the `service` complex type in the CycloneDX schema.
.. note::
See the CycloneDX schema: https://cyclonedx.org/docs/1.4/xml/#type_service
"""

def __init__(self, name: str, bom_ref: Optional[str] = None, provider: Optional[OrganizationalEntity] = None,
group: Optional[str] = None, version: Optional[str] = None, description: Optional[str] = None,
endpoints: Optional[List[XsUri]] = None, authenticated: Optional[bool] = None,
x_trust_boundary: Optional[bool] = None, data: Optional[List[DataClassification]] = None,
licenses: Optional[List[LicenseChoice]] = None,
external_references: Optional[List[ExternalReference]] = None,
properties: Optional[List[Property]] = None,
services: Optional[List['Service']] = None,
release_notes: Optional[ReleaseNotes] = None,
) -> None:
self.bom_ref = bom_ref or str(uuid4())
self.provider = provider
self.group = group
self.name = name
self.version = version
self.description = description
self.endpoints = endpoints
self.authenticated = authenticated
self.x_trust_boundary = x_trust_boundary
self.data = data
self.licenses = licenses or []
self.external_references = external_references or []
self.services = services
self.release_notes = release_notes
self.properties = properties

@property
def bom_ref(self) -> Optional[str]:
"""
An optional identifier which can be used to reference the service elsewhere in the BOM. Uniqueness is enforced
within all elements and children of the root-level bom element.
If a value was not provided in the constructor, a UUIDv4 will have been assigned.
Returns:
`str` unique identifier for this Service
"""
return self._bom_ref

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

@property
def provider(self) -> Optional[OrganizationalEntity]:
"""
Get the The organization that provides the service.
Returns:
`OrganizationalEntity` if set else `None`
"""
return self._provider

@provider.setter
def provider(self, provider: Optional[OrganizationalEntity]) -> None:
self._provider = provider

@property
def group(self) -> Optional[str]:
"""
The grouping name, namespace, or identifier. This will often be a shortened, single name of the company or
project that produced the service or domain name. Whitespace and special characters should be avoided.
Returns:
`str` if provided else `None`
"""
return self._group

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

@property
def name(self) -> str:
"""
The name of the service. This will often be a shortened, single name of the service.
Returns:
`str`
"""
return self._name

@name.setter
def name(self, name: str) -> None:
self._name = name

@property
def version(self) -> Optional[str]:
"""
The service version.
Returns:
`str` if set else `None`
"""
return self._version

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

@property
def description(self) -> Optional[str]:
"""
Specifies a description for the service.
Returns:
`str` if set else `None`
"""
return self._description

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

@property
def endpoints(self) -> Optional[List[XsUri]]:
"""
A list of endpoints URI's this service provides.
Returns:
List of `XsUri` else `None`
"""
return self._endpoints

@endpoints.setter
def endpoints(self, endpoints: Optional[List[XsUri]]) -> None:
self._endpoints = endpoints

def add_endpoint(self, endpoint: XsUri) -> None:
"""
Add an endpoint URI for this Service.
Args:
endpoint:
`XsUri` instance to add
Returns:
None
"""
self.endpoints = (self._endpoints or []) + [endpoint]

@property
def authenticated(self) -> Optional[bool]:
"""
A boolean value indicating if the service requires authentication. A value of true indicates the service
requires authentication prior to use.
A value of false indicates the service does not require authentication.
Returns:
`bool` if set else `None`
"""
return self._authenticated

@authenticated.setter
def authenticated(self, authenticated: Optional[bool]) -> None:
self._authenticated = authenticated

@property
def x_trust_boundary(self) -> Optional[bool]:
"""
A boolean value indicating if use of the service crosses a trust zone or boundary. A value of true indicates
that by using the service, a trust boundary is crossed.
A value of false indicates that by using the service, a trust boundary is not crossed.
Returns:
`bool` if set else `None`
"""
return self._x_trust_boundary

@x_trust_boundary.setter
def x_trust_boundary(self, x_trust_boundary: Optional[bool]) -> None:
self._x_trust_boundary = x_trust_boundary

@property
def data(self) -> Optional[List[DataClassification]]:
"""
Specifies the data classification.
Returns:
List of `DataClassificiation` or `None`
"""
return self._data

@data.setter
def data(self, data: Optional[List[DataClassification]]) -> None:
self._data = data

@property
def licenses(self) -> List[LicenseChoice]:
"""
A optional list of statements about how this Service is licensed.
Returns:
List of `LicenseChoice` else `None`
"""
return self._licenses

@licenses.setter
def licenses(self, licenses: List[LicenseChoice]) -> None:
self._licenses = licenses

@property
def external_references(self) -> List[ExternalReference]:
"""
Provides the ability to document external references related to the Service.
Returns:
List of `ExternalReference`s
"""
return self._external_references

@external_references.setter
def external_references(self, external_references: List[ExternalReference]) -> None:
self._external_references = external_references

def add_external_reference(self, reference: ExternalReference) -> None:
"""
Add an `ExternalReference` to this `Service`.
Args:
reference:
`ExternalReference` instance to add.
"""
self.external_references = self._external_references + [reference]

@property
def services(self) -> Optional[List['Service']]:
"""
A list of services included or deployed behind the parent service.
This is not a dependency tree.
It provides a way to specify a hierarchical representation of service assemblies.
Returns:
List of `Service`s or `None`
"""
return self._services

@services.setter
def services(self, services: Optional[List['Service']]) -> None:
self._services = services

def has_service(self, service: 'Service') -> bool:
"""
Check whether this Service contains the given Service.
Args:
service:
The instance of `cyclonedx.model.service.Service` to check if this Service contains.
Returns:
`bool` - `True` if the supplied Service is part of this Service, `False` otherwise.
"""
if not self.services:
return False

return service in self.services

@property
def release_notes(self) -> Optional[ReleaseNotes]:
"""
Specifies optional release notes.
Returns:
`ReleaseNotes` or `None`
"""
return self._release_notes

@release_notes.setter
def release_notes(self, release_notes: Optional[ReleaseNotes]) -> None:
self._release_notes = release_notes

@property
def properties(self) -> Optional[List[Property]]:
"""
Provides the ability to document properties in a key/value store. This provides flexibility to include data not
officially supported in the standard without having to use additional namespaces or create extensions.
Return:
List of `Property` or `None`
"""
return self._properties

@properties.setter
def properties(self, properties: Optional[List[Property]]) -> None:
self._properties = properties

def __eq__(self, other: object) -> bool:
if isinstance(other, Service):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((
self.authenticated, self.data, self.description,
tuple([hash(uri) for uri in set(sorted(self.endpoints, key=hash))]) if self.endpoints else None,
tuple([hash(ref) for ref in
set(sorted(self.external_references, key=hash))]) if self.external_references else None,
self.group, str(self.licenses), self.name, self.properties, self.provider,
self.release_notes,
tuple([hash(service) for service in set(sorted(self.services, key=hash))]) if self.services else None,
self.version, self.x_trust_boundary
))

def __repr__(self) -> str:
return f'<Service name={self.name}, version={self.version}, bom-ref={self.bom_ref}>'
122 changes: 122 additions & 0 deletions cyclonedx/model/vulnerability.py
Original file line number Diff line number Diff line change
@@ -106,6 +106,17 @@ def status(self) -> Optional[ImpactAnalysisAffectedStatus]:
def status(self, status: Optional[ImpactAnalysisAffectedStatus]) -> None:
self._status = status

def __eq__(self, other: object) -> bool:
if isinstance(other, BomTargetVersionRange):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((self.version, self.range, hash(self.status)))

def __repr__(self) -> str:
return f'<BomTargetVersionRange version={self.version}, version_range={self.range}, status={self.status}>'


class BomTarget:
"""
@@ -146,6 +157,20 @@ def versions(self) -> Optional[List[BomTargetVersionRange]]:
def versions(self, versions: Optional[List[BomTargetVersionRange]]) -> None:
self._versions = versions

def __eq__(self, other: object) -> bool:
if isinstance(other, BomTarget):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((
self.ref,
tuple([hash(version) for version in set(sorted(self.versions, key=hash))]) if self.versions else None
))

def __repr__(self) -> str:
return f'<BomTarget ref={self.ref}>'


class VulnerabilityAnalysis:
"""
@@ -218,6 +243,21 @@ def detail(self) -> Optional[str]:
def detail(self, detail: Optional[str]) -> None:
self._detail = detail

def __eq__(self, other: object) -> bool:
if isinstance(other, VulnerabilityAnalysis):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((
self.state, self.justification,
tuple([hash(r) for r in set(sorted(self.response, key=hash))]) if self.response else None,
self.detail
))

def __repr__(self) -> str:
return f'<VulnerabilityAnalysis state={self.state}, justification={self.justification}>'


class VulnerabilityAdvisory:
"""
@@ -253,6 +293,17 @@ def url(self) -> XsUri:
def url(self, url: XsUri) -> None:
self._url = url

def __eq__(self, other: object) -> bool:
if isinstance(other, VulnerabilityAdvisory):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((self.title, self.url))

def __repr__(self) -> str:
return f'<VulnerabilityAdvisory url={self.url}, title={self.title}>'


class VulnerabilitySource:
"""
@@ -294,6 +345,17 @@ def url(self) -> Optional[XsUri]:
def url(self, url: Optional[XsUri]) -> None:
self._url = url

def __eq__(self, other: object) -> bool:
if isinstance(other, VulnerabilitySource):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((self.name, self.url))

def __repr__(self) -> str:
return f'<VulnerabilityAdvisory name={self.name}, url={self.url}>'


class VulnerabilityReference:
"""
@@ -338,6 +400,17 @@ def source(self) -> Optional[VulnerabilitySource]:
def source(self, source: Optional[VulnerabilitySource]) -> None:
self._source = source

def __eq__(self, other: object) -> bool:
if isinstance(other, VulnerabilityReference):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((self.id, hash(self.source)))

def __repr__(self) -> str:
return f'<VulnerabilityReference id={self.id}, source={self.source}>'


class VulnerabilityScoreSource(Enum):
"""
@@ -571,6 +644,19 @@ def justification(self) -> Optional[str]:
def justification(self, justification: Optional[str]) -> None:
self._justification = justification

def __eq__(self, other: object) -> bool:
if isinstance(other, VulnerabilityRating):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((
hash(self.source), self.score, self.severity, self.method, self.vector, self.justification
))

def __repr__(self) -> str:
return f'<VulnerabilityRating score={self.score}, source={self.source}>'


class VulnerabilityCredits:
"""
@@ -620,6 +706,20 @@ def individuals(self) -> Optional[List[OrganizationalContact]]:
def individuals(self, individuals: Optional[List[OrganizationalContact]]) -> None:
self._individuals = individuals

def __eq__(self, other: object) -> bool:
if isinstance(other, VulnerabilityCredits):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((
tuple([hash(org) for org in set(sorted(self.organizations, key=hash))]) if self.organizations else None,
tuple([hash(person) for person in set(sorted(self.individuals, key=hash))]) if self.individuals else None
))

def __repr__(self) -> str:
return f'<VulnerabilityCredits id={id(self)}>'


class Vulnerability:
"""
@@ -975,3 +1075,25 @@ def get_recommendations(self) -> List[str]:
DeprecationWarning
)
return [self.recommendation] if self.recommendation else []

def __eq__(self, other: object) -> bool:
if isinstance(other, Vulnerability):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((
self.id, hash(self.source),
tuple([hash(ref) for ref in set(sorted(self.references, key=hash))]) if self.references else None,
tuple([hash(rating) for rating in set(sorted(self.ratings, key=hash))]) if self.ratings else None,
tuple([hash(cwe) for cwe in set(sorted(self.cwes, key=hash))]) if self.cwes else None,
self.description, self.detail, self.recommendation,
tuple([hash(advisory) for advisory in set(sorted(self.advisories, key=hash))]) if self.advisories else None,
self.created, self.published, self.updated, hash(self.credits),
tuple([hash(tool) for tool in set(sorted(self.tools, key=hash))]) if self.tools else None,
hash(self.analysis),
tuple([hash(affected) for affected in set(sorted(self.affects, key=hash))]) if self.affects else None
))

def __repr__(self) -> str:
return f'<Vulnerability bom-ref={self.bom_ref}, id={self.id}>'
14 changes: 14 additions & 0 deletions cyclonedx/output/__init__.py
Original file line number Diff line number Diff line change
@@ -40,6 +40,15 @@ class SchemaVersion(Enum):
V1_3: str = 'V1Dot3'
V1_4: str = 'V1Dot4'

def to_version(self) -> str:
"""
Return as a version string - e.g. `1.4`
Returns:
`str` version
"""
return f'{self.value[1]}.{self.value[5]}'


DEFAULT_SCHEMA_VERSION = SchemaVersion.V1_3

@@ -51,6 +60,11 @@ def __init__(self, bom: Bom, **kwargs: int) -> None:
self._bom = bom
self._generated: bool = False

@property
@abstractmethod
def schema_version(self) -> SchemaVersion:
pass

@property
def generated(self) -> bool:
return self._generated
46 changes: 36 additions & 10 deletions cyclonedx/output/json.py
Original file line number Diff line number Diff line change
@@ -19,14 +19,15 @@

import json
from abc import abstractmethod
from typing import Any, Dict, List, Optional, Union
from typing import cast, Any, Dict, List, Optional, Union

from . import BaseOutput
from . import BaseOutput, SchemaVersion
from .schema import BaseSchemaVersion, SchemaVersion1Dot0, SchemaVersion1Dot1, SchemaVersion1Dot2, SchemaVersion1Dot3, \
SchemaVersion1Dot4
from .serializer.json import CycloneDxJSONEncoder
from ..exception.output import FormatNotSupportedException
from ..model.bom import Bom

from ..model.component import Component

ComponentDict = Dict[str, Union[
str,
@@ -41,21 +42,27 @@ def __init__(self, bom: Bom) -> None:
super().__init__(bom=bom)
self._json_output: str = ''

@property
def schema_version(self) -> SchemaVersion:
return self.schema_version_enum

def generate(self, force_regeneration: bool = False) -> None:
if self.generated and not force_regeneration:
return

schema_uri: Optional[str] = self._get_schema_uri()
if not schema_uri:
# JSON not supported!
return
raise FormatNotSupportedException(
f'JSON is not supported by CycloneDX in schema version {self.schema_version.to_version()}'
)

vulnerabilities: Dict[str, List[Dict[Any, Any]]] = {"vulnerabilities": []}
for component in self.get_bom().components:
for vulnerability in component.get_vulnerabilities():
vulnerabilities['vulnerabilities'].append(
json.loads(json.dumps(vulnerability, cls=CycloneDxJSONEncoder))
)
if self.get_bom().components:
for component in cast(List[Component], self.get_bom().components):
for vulnerability in component.get_vulnerabilities():
vulnerabilities['vulnerabilities'].append(
json.loads(json.dumps(vulnerability, cls=CycloneDxJSONEncoder))
)

bom_json = json.loads(json.dumps(self.get_bom(), cls=CycloneDxJSONEncoder))
bom_json = json.loads(self._specialise_output_for_schema_version(bom_json=bom_json))
@@ -82,6 +89,9 @@ def _specialise_output_for_schema_version(self, bom_json: Dict[Any, Any]) -> str
# Iterate Components
if 'components' in bom_json.keys():
for i in range(len(bom_json['components'])):
if self.component_version_optional() and bom_json['components'][i]['version'] == "":
del bom_json['components'][i]['version']

if not self.component_supports_author() and 'author' in bom_json['components'][i].keys():
del bom_json['components'][i]['author']

@@ -94,6 +104,22 @@ def _specialise_output_for_schema_version(self, bom_json: Dict[Any, Any]) -> str
else:
bom_json['components'] = []

# Iterate Services
if 'services' in bom_json.keys():
for i in range(len(bom_json['services'])):
if not self.services_supports_properties() and 'properties' in bom_json['services'][i].keys():
del bom_json['services'][i]['properties']

if not self.services_supports_release_notes() and 'releaseNotes' in bom_json['services'][i].keys():
del bom_json['services'][i]['releaseNotes']

# Iterate externalReferences
if 'externalReferences' in bom_json.keys():
for i in range(len(bom_json['externalReferences'])):
if not self.external_references_supports_hashes() \
and 'hashes' in bom_json['externalReferences'][i].keys():
del bom_json['externalReferences'][i]['hashes']

# Iterate Vulnerabilities
if 'vulnerabilities' in bom_json.keys():
for i in range(len(bom_json['vulnerabilities'])):
108 changes: 108 additions & 0 deletions cyclonedx/output/schema.py
Original file line number Diff line number Diff line change
@@ -19,9 +19,16 @@

from abc import ABC, abstractmethod

from . import SchemaVersion


class BaseSchemaVersion(ABC):

@property
@abstractmethod
def schema_version_enum(self) -> SchemaVersion:
pass

def bom_supports_metadata(self) -> bool:
return True

@@ -31,6 +38,18 @@ def bom_metadata_supports_tools(self) -> bool:
def bom_metadata_supports_tools_external_references(self) -> bool:
return True

def bom_supports_services(self) -> bool:
return True

def bom_supports_external_references(self) -> bool:
return True

def services_supports_properties(self) -> bool:
return True

def services_supports_release_notes(self) -> bool:
return True

def bom_supports_vulnerabilities(self) -> bool:
return True

@@ -49,22 +68,41 @@ def component_supports_bom_ref_attribute(self) -> bool:
def component_supports_mime_type_attribute(self) -> bool:
return True

def component_supports_licenses_expression(self) -> bool:
return True

def component_version_optional(self) -> bool:
return False

def component_supports_swid(self) -> bool:
return True

def component_supports_pedigree(self) -> bool:
return True

def pedigree_supports_patches(self) -> bool:
return True

def component_supports_external_references(self) -> bool:
return True

def component_supports_release_notes(self) -> bool:
return True

def external_references_supports_hashes(self) -> bool:
return True

@abstractmethod
def get_schema_version(self) -> str:
raise NotImplementedError


class SchemaVersion1Dot4(BaseSchemaVersion):

@property
def schema_version_enum(self) -> SchemaVersion:
return SchemaVersion.V1_4

def get_schema_version(self) -> str:
return '1.4'

@@ -74,9 +112,16 @@ def component_version_optional(self) -> bool:

class SchemaVersion1Dot3(BaseSchemaVersion):

@property
def schema_version_enum(self) -> SchemaVersion:
return SchemaVersion.V1_3

def bom_metadata_supports_tools_external_references(self) -> bool:
return False

def services_supports_release_notes(self) -> bool:
return False

def bom_supports_vulnerabilities(self) -> bool:
return False

@@ -95,9 +140,19 @@ def get_schema_version(self) -> str:

class SchemaVersion1Dot2(BaseSchemaVersion):

@property
def schema_version_enum(self) -> SchemaVersion:
return SchemaVersion.V1_2

def bom_metadata_supports_tools_external_references(self) -> bool:
return False

def services_supports_properties(self) -> bool:
return False

def services_supports_release_notes(self) -> bool:
return False

def bom_supports_vulnerabilities(self) -> bool:
return False

@@ -110,18 +165,37 @@ def component_supports_mime_type_attribute(self) -> bool:
def component_supports_release_notes(self) -> bool:
return False

def external_references_supports_hashes(self) -> bool:
return False

def get_schema_version(self) -> str:
return '1.2'


class SchemaVersion1Dot1(BaseSchemaVersion):

@property
def schema_version_enum(self) -> SchemaVersion:
return SchemaVersion.V1_1

def bom_metadata_supports_tools(self) -> bool:
return False

def bom_metadata_supports_tools_external_references(self) -> bool:
return False

def bom_supports_services(self) -> bool:
return False

def services_supports_properties(self) -> bool:
return False

def pedigree_supports_patches(self) -> bool:
return False

def services_supports_release_notes(self) -> bool:
return False

def bom_supports_vulnerabilities(self) -> bool:
return False

@@ -137,21 +211,43 @@ def component_supports_mime_type_attribute(self) -> bool:
def component_supports_author(self) -> bool:
return False

def component_supports_swid(self) -> bool:
return False

def component_supports_release_notes(self) -> bool:
return False

def external_references_supports_hashes(self) -> bool:
return False

def get_schema_version(self) -> str:
return '1.1'


class SchemaVersion1Dot0(BaseSchemaVersion):

@property
def schema_version_enum(self) -> SchemaVersion:
return SchemaVersion.V1_0

def bom_metadata_supports_tools(self) -> bool:
return False

def bom_metadata_supports_tools_external_references(self) -> bool:
return False

def bom_supports_services(self) -> bool:
return False

def bom_supports_external_references(self) -> bool:
return False

def services_supports_properties(self) -> bool:
return False

def services_supports_release_notes(self) -> bool:
return False

def bom_supports_vulnerabilities(self) -> bool:
return False

@@ -167,14 +263,26 @@ def component_supports_author(self) -> bool:
def component_supports_bom_ref_attribute(self) -> bool:
return False

def component_supports_licenses_expression(self) -> bool:
return False

def component_supports_mime_type_attribute(self) -> bool:
return False

def component_supports_swid(self) -> bool:
return False

def component_supports_pedigree(self) -> bool:
return False

def component_supports_external_references(self) -> bool:
return False

def component_supports_release_notes(self) -> bool:
return False

def external_references_supports_hashes(self) -> bool:
return False

def get_schema_version(self) -> str:
return '1.0'
18 changes: 18 additions & 0 deletions cyclonedx/output/serializer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# encoding: utf-8

# This file is part of CycloneDX Python Lib
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.
10 changes: 7 additions & 3 deletions cyclonedx/output/serializer/json.py
Original file line number Diff line number Diff line change
@@ -29,9 +29,10 @@
from packageurl import PackageURL # type: ignore

from cyclonedx.model import XsUri
from cyclonedx.model.component import Component

HYPHENATED_ATTRIBUTES = [
'bom_ref', 'mime_type'
'bom_ref', 'mime_type', 'x_trust_boundary'
]
PYTHON_TO_JSON_NAME = compile(r'_([a-z])')

@@ -77,8 +78,11 @@ def default(self, o: Any) -> Any:
elif '_' in new_key:
new_key = PYTHON_TO_JSON_NAME.sub(lambda x: x.group(1).upper(), new_key)

# Skip any None values
if v:
# Inject '' for Component.version if it's None
if isinstance(o, Component) and new_key == 'version' and v is None:
d[new_key] = ""
elif v or v is False:
# Skip any None values (exception 'version')
if isinstance(v, PackageURL):
# Special handling of PackageURL instances which JSON would otherwise automatically encode to
# an Array
426 changes: 315 additions & 111 deletions cyclonedx/output/xml.py

Large diffs are not rendered by default.

57 changes: 31 additions & 26 deletions poetry.lock
3 changes: 2 additions & 1 deletion tests/base.py
Original file line number Diff line number Diff line change
@@ -117,7 +117,8 @@ def assertValidAgainstSchema(self, bom_xml: str, schema_version: SchemaVersion)

if not schema_validates:
print(xml_schema.error_log.last_error)
self.assertTrue(schema_validates, 'Failed to validate Generated SBOM against XSD Schema')
self.assertTrue(schema_validates, f'Failed to validate Generated SBOM against XSD Schema:'
f'{bom_xml}')

def assertEqualXml(self, a: str, b: str) -> None:
da, db = minidom.parseString(a), minidom.parseString(b)
428 changes: 428 additions & 0 deletions tests/data.py

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions tests/fixtures/json/1.2/bom_external_references.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.2a.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.2",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2021-09-01T10:50:42.051979+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "VERSION"
}
]
},
"components": [],
"externalReferences": [
{
"url": "https://cyclonedx.org",
"comment": "No comment",
"type": "distribution"
},
{
"url": "https://cyclonedx.org",
"type": "website"
}
]
}
84 changes: 84 additions & 0 deletions tests/fixtures/json/1.2/bom_services_complex.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.2a.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.2",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2021-09-01T10:50:42.051979+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "VERSION"
}
],
"component": {
"type": "library",
"bom-ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
"name": "cyclonedx-python-lib",
"version": "1.0.0"
}
},
"components": [],
"services": [
{
"bom-ref": "my-specific-bom-ref-for-my-first-service",
"provider": {
"contact": [
{
"email": "paul.horton@owasp.org",
"name": "Paul Horton"
},
{
"email": "someone@somewhere.tld",
"name": "A N Other",
"phone": "+44 (0)1234 567890"
}
],
"name": "CycloneDX",
"url": [
"https://cyclonedx.org"
]
},
"group": "a-group",
"name": "my-first-service",
"version": "1.2.3",
"description": "Description goes here",
"endpoints": [
"/api/thing/1",
"/api/thing/2"
],
"authenticated": false,
"x-trust-boundary": true,
"data": [
{
"classification": "public",
"flow": "outbound"
}
],
"licenses": [
{
"expression": "Commercial"
}
],
"externalReferences": [
{
"comment": "No comment",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
],
"type": "distribution",
"url": "https://cyclonedx.org"
}
]
},
{
"bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda",
"name": "my-second-service"
}
]
}
145 changes: 145 additions & 0 deletions tests/fixtures/json/1.2/bom_services_nested.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.2a.schema.json",
"bomFormat": "CycloneDX",
"components": [
],
"metadata": {
"component": {
"bom-ref": "bb5911d6-1a1d-41c9-b6e0-46e848d16655",
"name": "cyclonedx-python-lib",
"type": "library",
"version": "1.0.0"
},
"timestamp": "2022-01-27T16:16:35.622354+00:00",
"tools": [
{
"name": "cyclonedx-python-lib",
"vendor": "CycloneDX",
"version": "0.11.0"
}
]
},
"serialNumber": "urn:uuid:1d2c4529-8cf8-447d-b2a1-e4ebb610adb9",
"services": [
{
"authenticated": false,
"bom-ref": "my-specific-bom-ref-for-my-first-service",
"data": [
{
"classification": "public",
"flow": "outbound"
}
],
"description": "Description goes here",
"endpoints": [
"/api/thing/1",
"/api/thing/2"
],
"externalReferences": [
{
"comment": "No comment",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
],
"type": "distribution",
"url": "https://cyclonedx.org"
}
],
"group": "a-group",
"licenses": [
{
"expression": "Commercial"
}
],
"name": "my-first-service",
"provider": {
"contact": [
{
"email": "paul.horton@owasp.org",
"name": "Paul Horton"
},
{
"email": "someone@somewhere.tld",
"name": "A N Other",
"phone": "+44 (0)1234 567890"
}
],
"name": "CycloneDX",
"url": [
"https://cyclonedx.org"
]
},
"services": [
{
"bom-ref": "df70b5f1-8f53-47a4-be48-669ae78795e6",
"name": "first-nested-service"
},
{
"authenticated": true,
"bom-ref": "my-specific-bom-ref-for-second-nested-service",
"group": "no-group",
"name": "second-nested-service",
"provider": {
"contact": [
{
"email": "paul.horton@owasp.org",
"name": "Paul Horton"
},
{
"email": "someone@somewhere.tld",
"name": "A N Other",
"phone": "+44 (0)1234 567890"
}
],
"name": "CycloneDX",
"url": [
"https://cyclonedx.org"
]
},
"version": "3.2.1",
"x-trust-boundary": false
}
],
"version": "1.2.3",
"x-trust-boundary": true
},
{
"bom-ref": "df70b5f1-8f53-47a4-be48-669ae78795e6",
"name": "my-second-service",
"services": [
{
"bom-ref": "df70b5f1-8f53-47a4-be48-669ae78795e6",
"group": "what-group",
"name": "yet-another-nested-service",
"provider": {
"contact": [
{
"email": "paul.horton@owasp.org",
"name": "Paul Horton"
},
{
"email": "someone@somewhere.tld",
"name": "A N Other",
"phone": "+44 (0)1234 567890"
}
],
"name": "CycloneDX",
"url": [
"https://cyclonedx.org"
]
},
"version": "6.5.4"
},
{
"bom-ref": "my-specific-bom-ref-for-another-nested-service",
"name": "another-nested-service"
}
]
}
],
"specVersion": "1.2",
"version": 1
}
34 changes: 34 additions & 0 deletions tests/fixtures/json/1.2/bom_services_simple.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.2a.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.2",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2021-09-01T10:50:42.051979+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "VERSION"
}
],
"component": {
"type": "library",
"bom-ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
"name": "cyclonedx-python-lib",
"version": "1.0.0"
}
},
"components": [],
"services": [
{
"bom-ref": "bb5911d6-1a1d-41c9-b6e0-46e848d16655",
"name": "my-first-service"
},
{
"bom-ref": "bb5911d6-1a1d-41c9-b6e0-46e848d16655",
"name": "my-second-service"
}
]
}
Original file line number Diff line number Diff line change
@@ -21,6 +21,11 @@
"author": "Test Author",
"name": "setuptools",
"version": "50.3.2",
"licenses": [
{
"expression": "MIT License"
}
],
"purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz"
}
]
218 changes: 218 additions & 0 deletions tests/fixtures/json/1.2/bom_setuptools_complete.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.2a.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.2",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2021-09-01T10:50:42.051979+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "VERSION"
}
]
},
"components": [
{
"type": "library",
"bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz",
"author": "Test Author",
"name": "setuptools",
"version": "50.3.2",
"licenses": [
{
"expression": "MIT License"
}
],
"cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*",
"purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz",
"swid": {
"tagId": "swidgen-242eb18a-503e-ca37-393b-cf156ef09691_9.1.1",
"name": "Test Application",
"version": "3.4.5",
"text": {
"contentType": "text/xml",
"encoding": "base64",
"content": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg=="
}
},
"pedigree": {
"ancestors": [
{
"type": "library",
"bom-ref": "ccc8d7ee-4b9c-4750-aee0-a72585152291",
"author": "Test Author",
"name": "setuptools",
"version": "50.3.2",
"licenses": [
{
"expression": "MIT License"
}
],
"purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz"
},
{
"type": "library",
"bom-ref": "8a3893b3-9923-4adb-a1d3-47456636ba0a",
"author": "Test Author",
"name": "setuptools",
"version": "",
"licenses": [
{
"expression": "MIT License"
}
],
"purl": "pkg:pypi/setuptools?extension=tar.gz"
}
],
"descendants": [
{
"type": "library",
"bom-ref": "28b2d8ce-def0-446f-a221-58dee0b44acc",
"author": "Test Author",
"name": "setuptools",
"version": "",
"licenses": [
{
"expression": "MIT License"
}
],
"purl": "pkg:pypi/setuptools?extension=tar.gz"
},
{
"type": "library",
"bom-ref": "555ca729-93c6-48f3-956e-bdaa4a2f0bfa",
"name": "toml",
"version": "0.10.2",
"purl": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
],
"externalReferences": [
{
"type": "distribution",
"url": "https://cyclonedx.org",
"comment": "No comment",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
]
}
]
}
],
"variants": [
{
"type": "library",
"bom-ref": "e7abdcca-5ba2-4f29-b2cf-b1e1ef788e66",
"name": "toml",
"version": "0.10.2",
"purl": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
],
"externalReferences": [
{
"type": "distribution",
"url": "https://cyclonedx.org",
"comment": "No comment",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
]
}
]
},
{
"type": "library",
"bom-ref": "ded1d73e-1fca-4302-b520-f1bc53979958",
"author": "Test Author",
"name": "setuptools",
"version": "50.3.2",
"licenses": [
{
"expression": "MIT License"
}
],
"purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz"
}
],
"commits": [
{
"uid": "a-random-uid",
"message": "A commit message"
}
],
"patches": [
{
"type": "backport"
}
],
"notes": "Some notes here please"
},
"components": [
{
"type": "library",
"bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz",
"author": "Test Author",
"name": "setuptools",
"version": "50.3.2",
"licenses": [
{
"expression": "MIT License"
}
],
"purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz"
},
{
"type": "library",
"bom-ref": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"name": "toml",
"version": "0.10.2",
"purl": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
],
"externalReferences": [
{
"type": "distribution",
"url": "https://cyclonedx.org",
"comment": "No comment",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
]
}
]
}
],
"evidence": {
"copyright": [
{
"text": "Commercial"
},
{
"text": "Commercial 2"
}
]
}
}
]
}
Original file line number Diff line number Diff line change
@@ -21,6 +21,11 @@
"author": "Test Author",
"name": "setuptools",
"version": "50.3.2",
"licenses": [
{
"expression": "MIT License"
}
],
"cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*",
"purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz"
}
45 changes: 45 additions & 0 deletions tests/fixtures/json/1.2/bom_toml_1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.2a.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.2",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2021-09-01T10:50:42.051979+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "VERSION"
}
]
},
"components": [
{
"type": "library",
"bom-ref": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"name": "toml",
"version": "0.10.2",
"purl": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
],
"externalReferences": [
{
"type": "distribution",
"url": "https://cyclonedx.org",
"comment": "No comment",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
]
}
]
}
]
}
24 changes: 24 additions & 0 deletions tests/fixtures/json/1.2/bom_with_full_metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.2a.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.2",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2021-09-01T10:50:42.051979+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "VERSION"
}
],
"component": {
"bom-ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857",
"type": "library",
"name": "cyclonedx-python-lib",
"version": "1.0.0"
}
},
"components": []
}
Original file line number Diff line number Diff line change
@@ -14,19 +14,22 @@
}
]
},
"components": [
"components": [],
"externalReferences": [
{
"type": "library",
"bom-ref": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"name": "toml",
"version": "0.10.2",
"purl": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"url": "https://cyclonedx.org",
"comment": "No comment",
"type": "distribution",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
]
},
{
"url": "https://cyclonedx.org",
"type": "website"
}
]
}
94 changes: 94 additions & 0 deletions tests/fixtures/json/1.3/bom_services_complex.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.3.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2021-09-01T10:50:42.051979+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "VERSION"
}
],
"component": {
"type": "library",
"bom-ref": "bb5911d6-1a1d-41c9-b6e0-46e848d16655",
"name": "cyclonedx-python-lib",
"version": "1.0.0"
}
},
"components": [],
"services": [
{
"bom-ref": "my-specific-bom-ref-for-my-first-service",
"provider": {
"contact": [
{
"email": "paul.horton@owasp.org",
"name": "Paul Horton"
},
{
"email": "someone@somewhere.tld",
"name": "A N Other",
"phone": "+44 (0)1234 567890"
}
],
"name": "CycloneDX",
"url": [
"https://cyclonedx.org"
]
},
"group": "a-group",
"name": "my-first-service",
"version": "1.2.3",
"description": "Description goes here",
"endpoints": [
"/api/thing/1",
"/api/thing/2"
],
"authenticated": false,
"x-trust-boundary": true,
"data": [
{
"classification": "public",
"flow": "outbound"
}
],
"licenses": [
{
"expression": "Commercial"
}
],
"externalReferences": [
{
"comment": "No comment",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
],
"type": "distribution",
"url": "https://cyclonedx.org"
}
],
"properties": [
{
"name": "key1",
"value": "val1"
},
{
"name": "key2",
"value": "val2"
}
]
},
{
"bom-ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857",
"name": "my-second-service"
}
]
}
155 changes: 155 additions & 0 deletions tests/fixtures/json/1.3/bom_services_nested.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.3.schema.json",
"bomFormat": "CycloneDX",
"components": [
],
"metadata": {
"component": {
"bom-ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857",
"name": "cyclonedx-python-lib",
"type": "library",
"version": "1.0.0"
},
"timestamp": "2022-01-27T16:16:35.622354+00:00",
"tools": [
{
"name": "cyclonedx-python-lib",
"vendor": "CycloneDX",
"version": "0.11.0"
}
]
},
"serialNumber": "urn:uuid:1d2c4529-8cf8-447d-b2a1-e4ebb610adb9",
"services": [
{
"authenticated": false,
"bom-ref": "my-specific-bom-ref-for-my-first-service",
"data": [
{
"classification": "public",
"flow": "outbound"
}
],
"description": "Description goes here",
"endpoints": [
"/api/thing/1",
"/api/thing/2"
],
"externalReferences": [
{
"comment": "No comment",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
],
"type": "distribution",
"url": "https://cyclonedx.org"
}
],
"group": "a-group",
"licenses": [
{
"expression": "Commercial"
}
],
"name": "my-first-service",
"properties": [
{
"name": "key1",
"value": "val1"
},
{
"name": "key2",
"value": "val2"
}
],
"provider": {
"contact": [
{
"email": "paul.horton@owasp.org",
"name": "Paul Horton"
},
{
"email": "someone@somewhere.tld",
"name": "A N Other",
"phone": "+44 (0)1234 567890"
}
],
"name": "CycloneDX",
"url": [
"https://cyclonedx.org"
]
},
"services": [
{
"bom-ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
"name": "first-nested-service"
},
{
"authenticated": true,
"bom-ref": "my-specific-bom-ref-for-second-nested-service",
"group": "no-group",
"name": "second-nested-service",
"provider": {
"contact": [
{
"email": "paul.horton@owasp.org",
"name": "Paul Horton"
},
{
"email": "someone@somewhere.tld",
"name": "A N Other",
"phone": "+44 (0)1234 567890"
}
],
"name": "CycloneDX",
"url": [
"https://cyclonedx.org"
]
},
"version": "3.2.1",
"x-trust-boundary": false
}
],
"version": "1.2.3",
"x-trust-boundary": true
},
{
"bom-ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
"name": "my-second-service",
"services": [
{
"bom-ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
"group": "what-group",
"name": "yet-another-nested-service",
"provider": {
"contact": [
{
"email": "paul.horton@owasp.org",
"name": "Paul Horton"
},
{
"email": "someone@somewhere.tld",
"name": "A N Other",
"phone": "+44 (0)1234 567890"
}
],
"name": "CycloneDX",
"url": [
"https://cyclonedx.org"
]
},
"version": "6.5.4"
},
{
"bom-ref": "my-specific-bom-ref-for-another-nested-service",
"name": "another-nested-service"
}
]
}
],
"specVersion": "1.3",
"version": 1
}
34 changes: 34 additions & 0 deletions tests/fixtures/json/1.3/bom_services_simple.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.3.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2021-09-01T10:50:42.051979+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "VERSION"
}
],
"component": {
"type": "library",
"bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda",
"name": "cyclonedx-python-lib",
"version": "1.0.0"
}
},
"components": [],
"services": [
{
"bom-ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
"name": "my-first-service"
},
{
"bom-ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
"name": "my-second-service"
}
]
}
Original file line number Diff line number Diff line change
@@ -17,15 +17,16 @@
"components": [
{
"type": "library",
"bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz",
"author": "Test Author",
"name": "setuptools",
"version": "50.3.2",
"purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz",
"bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz",
"licenses": [
{
"expression": "MIT License"
}
]
],
"purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz"
}
]
}
218 changes: 218 additions & 0 deletions tests/fixtures/json/1.3/bom_setuptools_complete.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.3.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2021-09-01T10:50:42.051979+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "VERSION"
}
]
},
"components": [
{
"type": "library",
"bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz",
"author": "Test Author",
"name": "setuptools",
"version": "50.3.2",
"licenses": [
{
"expression": "MIT License"
}
],
"cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*",
"purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz",
"swid": {
"tagId": "swidgen-242eb18a-503e-ca37-393b-cf156ef09691_9.1.1",
"name": "Test Application",
"version": "3.4.5",
"text": {
"contentType": "text/xml",
"encoding": "base64",
"content": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg=="
}
},
"pedigree": {
"ancestors": [
{
"type": "library",
"bom-ref": "ccc8d7ee-4b9c-4750-aee0-a72585152291",
"author": "Test Author",
"name": "setuptools",
"version": "50.3.2",
"licenses": [
{
"expression": "MIT License"
}
],
"purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz"
},
{
"type": "library",
"bom-ref": "8a3893b3-9923-4adb-a1d3-47456636ba0a",
"author": "Test Author",
"name": "setuptools",
"version": "",
"licenses": [
{
"expression": "MIT License"
}
],
"purl": "pkg:pypi/setuptools?extension=tar.gz"
}
],
"descendants": [
{
"type": "library",
"bom-ref": "28b2d8ce-def0-446f-a221-58dee0b44acc",
"author": "Test Author",
"name": "setuptools",
"version": "",
"licenses": [
{
"expression": "MIT License"
}
],
"purl": "pkg:pypi/setuptools?extension=tar.gz"
},
{
"type": "library",
"bom-ref": "555ca729-93c6-48f3-956e-bdaa4a2f0bfa",
"name": "toml",
"version": "0.10.2",
"purl": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
],
"externalReferences": [
{
"type": "distribution",
"url": "https://cyclonedx.org",
"comment": "No comment",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
]
}
]
}
],
"variants": [
{
"type": "library",
"bom-ref": "e7abdcca-5ba2-4f29-b2cf-b1e1ef788e66",
"name": "toml",
"version": "0.10.2",
"purl": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
],
"externalReferences": [
{
"type": "distribution",
"url": "https://cyclonedx.org",
"comment": "No comment",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
]
}
]
},
{
"type": "library",
"bom-ref": "ded1d73e-1fca-4302-b520-f1bc53979958",
"author": "Test Author",
"name": "setuptools",
"version": "50.3.2",
"licenses": [
{
"expression": "MIT License"
}
],
"purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz"
}
],
"commits": [
{
"uid": "a-random-uid",
"message": "A commit message"
}
],
"patches": [
{
"type": "backport"
}
],
"notes": "Some notes here please"
},
"components": [
{
"type": "library",
"bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz",
"author": "Test Author",
"name": "setuptools",
"version": "50.3.2",
"licenses": [
{
"expression": "MIT License"
}
],
"purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz"
},
{
"type": "library",
"bom-ref": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"name": "toml",
"version": "0.10.2",
"purl": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
],
"externalReferences": [
{
"type": "distribution",
"url": "https://cyclonedx.org",
"comment": "No comment",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
]
}
]
}
],
"evidence": {
"copyright": [
{
"text": "Commercial"
},
{
"text": "Commercial 2"
}
]
}
}
]
}
Original file line number Diff line number Diff line change
@@ -17,15 +17,16 @@
"components": [
{
"type": "library",
"bom-ref": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"name": "toml",
"version": "0.10.2",
"purl": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"bom-ref": "pkg:pypi/setuptools?extension=tar.gz",
"author": "Test Author",
"name": "setuptools",
"version": "",
"licenses": [
{
"expression": "MIT License"
}
]
],
"purl": "pkg:pypi/setuptools?extension=tar.gz"
}
]
}
Original file line number Diff line number Diff line change
@@ -17,16 +17,17 @@
"components": [
{
"type": "library",
"bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz",
"author": "Test Author",
"name": "setuptools",
"version": "50.3.2",
"cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*",
"purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz",
"bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz",
"licenses": [
{
"expression": "MIT License"
}
]
],
"cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*",
"purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz"
}
]
}
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@
}
],
"component": {
"bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3",
"bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda",
"type": "library",
"name": "cyclonedx-python-lib",
"version": "1.0.0"
69 changes: 69 additions & 0 deletions tests/fixtures/json/1.4/bom_external_references.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2021-09-01T10:50:42.051979+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "VERSION",
"externalReferences": [
{
"type": "build-system",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions"
},
{
"type": "distribution",
"url": "https://pypi.org/project/cyclonedx-python-lib/"
},
{
"type": "documentation",
"url": "https://cyclonedx.github.io/cyclonedx-python-lib/"
},
{
"type": "issue-tracker",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues"
},
{
"type": "license",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE"
},
{
"type": "release-notes",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md"
},
{
"type": "vcs",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib"
},
{
"type": "website",
"url": "https://cyclonedx.org"
}
]
}
]
},
"components": [],
"externalReferences": [
{
"url": "https://cyclonedx.org",
"comment": "No comment",
"type": "distribution",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
]
},
{
"url": "https://cyclonedx.org",
"type": "website"
}
]
}
187 changes: 187 additions & 0 deletions tests/fixtures/json/1.4/bom_services_complex.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2021-09-01T10:50:42.051979+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "VERSION",
"externalReferences": [
{
"type": "build-system",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions"
},
{
"type": "distribution",
"url": "https://pypi.org/project/cyclonedx-python-lib/"
},
{
"type": "documentation",
"url": "https://cyclonedx.github.io/cyclonedx-python-lib/"
},
{
"type": "issue-tracker",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues"
},
{
"type": "license",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE"
},
{
"type": "release-notes",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md"
},
{
"type": "vcs",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib"
},
{
"type": "website",
"url": "https://cyclonedx.org"
}
]
}
],
"component": {
"type": "library",
"bom-ref": "df70b5f1-8f53-47a4-be48-669ae78795e6",
"name": "cyclonedx-python-lib",
"version": "1.0.0"
}
},
"components": [],
"services": [
{
"bom-ref": "my-specific-bom-ref-for-my-first-service",
"provider": {
"contact": [
{
"email": "paul.horton@owasp.org",
"name": "Paul Horton"
},
{
"email": "someone@somewhere.tld",
"name": "A N Other",
"phone": "+44 (0)1234 567890"
}
],
"name": "CycloneDX",
"url": [
"https://cyclonedx.org"
]
},
"group": "a-group",
"name": "my-first-service",
"version": "1.2.3",
"description": "Description goes here",
"endpoints": [
"/api/thing/1",
"/api/thing/2"
],
"authenticated": false,
"x-trust-boundary": true,
"data": [
{
"classification": "public",
"flow": "outbound"
}
],
"licenses": [
{
"expression": "Commercial"
}
],
"externalReferences": [
{
"comment": "No comment",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
],
"type": "distribution",
"url": "https://cyclonedx.org"
}
],
"releaseNotes": {
"aliases": [
"First Test Release"
],
"description": "This release is a test release",
"featuredImage": "https://cyclonedx.org/theme/assets/images/CycloneDX-Twitter-Card.png",
"notes": [
{
"locale": "en-GB",
"text": {
"content": "U29tZSBzaW1wbGUgcGxhaW4gdGV4dA==",
"contentType": "text/plain; charset=UTF-8",
"encoding": "base64"
}
},
{
"locale": "en-US",
"text": {
"content": "U29tZSBzaW1wbGUgcGxhaW4gdGV4dA==",
"contentType": "text/plain; charset=UTF-8",
"encoding": "base64"
}
}
],
"properties": [
{
"name": "key1",
"value": "val1"
},
{
"name": "key2",
"value": "val2"
}
],
"resolves": [
{
"description": "Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features...",
"id": "CVE-2021-44228",
"name": "Apache Log3Shell",
"references": [
"https://logging.apache.org/log4j/2.x/security.html",
"https://central.sonatype.org/news/20211213_log4shell_help"
],
"source": {
"name": "NVD",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228"
},
"type": "security"
}
],
"socialImage": "https://cyclonedx.org/cyclonedx-icon.png",
"tags": [
"test",
"alpha"
],
"timestamp": "2021-12-31T10:00:00+00:00",
"title": "Release Notes Title",
"type": "major"
},
"properties": [
{
"name": "key1",
"value": "val1"
},
{
"name": "key2",
"value": "val2"
}
]
},
{
"bom-ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
"name": "my-second-service"
}
]
}
Loading