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 4.0.0 work #341

Merged
merged 29 commits into from
Mar 20, 2023
Merged
Changes from 25 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2d894b5
doc: typo in README.
madpah Jul 6, 2022
d1d140c
chore: resolved issue in pre-commit-hooks
madpah Jul 6, 2022
403011c
feat: support VEX without Components in the same BOM
madpah Jul 6, 2022
a043437
feat: support VEX without Components in the same BOM
madpah Jul 6, 2022
2c50eb8
doc: updated to reflect breaking changes in model for `3.0.0`
madpah Jul 6, 2022
7d8a1f1
chore: removed duplicate code
madpah Jul 6, 2022
6a3d9e6
remove deprecated methods - no easy way to provide backwards compatab…
madpah Jul 7, 2022
e709d19
removed unused import
madpah Jul 7, 2022
3d71a5b
updated unit tests
madpah Jul 7, 2022
bd40d8b
ci: extend CI timeout to 15 minutes - py3.10 on OSX failing to comple…
madpah Jul 7, 2022
fe38107
chore: removed commented code
madpah Jul 8, 2022
1f58b31
Merge pull request #263 from CycloneDX/feat/vex-without-components
madpah Jul 11, 2022
b56c5c3
ci: pin `semantic-release==7.28.1` for manual deploy - see #234
madpah Jul 12, 2022
17341d6
feat: allow `version` of BOM to be defined
madpah Jul 12, 2022
068da83
Merge pull request #267 from CycloneDX/feat/bomlink-urn-helper-methods
madpah Jul 12, 2022
3dd1861
chore: merged main into dev/3.0.0 branch
madpah Aug 11, 2022
29e6c8c
Merge branch 'main' into dev/3.0.0
madpah Aug 11, 2022
7df1b7e
chore: fix release workflow
madpah Sep 15, 2022
03ac713
chore: editorconfig
jkowalleck Jan 6, 2023
695afe9
Merge branch 'main' into dev/4.0.0
madpah Jan 20, 2023
676c941
feat: support for deserialization from JSON and XML (#290)
madpah Mar 3, 2023
40145ce
fix: update `serializable` to include XML safety changes
madpah Mar 3, 2023
3f3c8ff
feat: Support for Python 3.11 (#349)
madpah Mar 6, 2023
5a99137
Merge branch 'main' into dev/4.0.0
madpah Mar 6, 2023
1b32d4b
fix: removed `autopep8` in favour of `flake8` as both have conflictin…
madpah Mar 7, 2023
4d0d920
chore: bump dev dependencies
madpah Mar 7, 2023
aa3189e
tests: compoennt versions optional (#350)
jkowalleck Mar 13, 2023
2ef396f
doc: doc updates for new deserialization feature
madpah Mar 17, 2023
ccb7a7f
doc: doc updates for contribution
madpah Mar 17, 2023
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
14 changes: 7 additions & 7 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -4,9 +4,9 @@ on:
push:
branches: [ 'main' ]
workflow_dispatch:

env:
PYTHON_VERSION_DEFAULT: "3.10"
PYTHON_VERSION_DEFAULT: "3.11"
POETRY_VERSION: "1.1.12"

jobs:
@@ -22,14 +22,14 @@ jobs:
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Setup python
# see https://github.com/actions/setup-python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION_DEFAULT }}
architecture: 'x64'

- name: Install and configure Poetry
# See https://github.com/marketplace/actions/install-poetry-action
uses: snok/install-poetry@v1
@@ -38,17 +38,17 @@ jobs:
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true

- name: Install dependencies
run: poetry install --no-root

- name: View poetry version
run: poetry --version

- name: Python Semantic Release
# see https://python-semantic-release.readthedocs.io/en/latest/automatic-releases/github-actions.html
# see https://github.com/relekang/python-semantic-release
uses: relekang/python-semantic-release@v7.33.1
uses: relekang/python-semantic-release@v7.33.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
repository_username: __token__
2 changes: 1 addition & 1 deletion .github/workflows/manual-release-candidate.yml
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ jobs:
python -m pip install poetry --upgrade pip
poetry config virtualenvs.create false
poetry install
python -m pip install python-semantic-release
python -m pip install python-semantic-release==7.28.1
- name: Apply Pre Release Version
run: |
RC_VERSION="$(semantic-release --noop --major print-version)-${{ github.event.inputs.release_candidate_suffix }}"
52 changes: 31 additions & 21 deletions .github/workflows/poetry.yml
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ name: Python CI

on:
push:
branches: ["master", "main"]
branches: ["main"]
pull_request:
branches-ignore: ['dependabot/**']
workflow_dispatch:
@@ -15,8 +15,8 @@ on:

env:
REPORTS_DIR: CI_reports
PYTHON_VERISON_DEFAULT: "3.10"
POETRY_VERSION: "1.1.11"
PYTHON_VERSION_DEFAULT: "3.11"
POETRY_VERSION: "1.1.12"

jobs:
coding-standards:
@@ -27,19 +27,23 @@ jobs:
- name: Checkout
# see https://github.com/actions/checkout
uses: actions/checkout@v3

- name: Setup Python Environment
# see https://github.com/actions/setup-python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERISON_DEFAULT }}
python-version: ${{ env.PYTHON_VERSION_DEFAULT }}
architecture: 'x64'

- name: Install poetry
# see https://github.com/marketplace/actions/setup-poetry
uses: Gr1N/setup-poetry@v8
with:
poetry-version: ${{ env.POETRY_VERSION }}

- name: Install dependencies
run: poetry install --no-root

- name: Run tox
run: poetry run tox -e flake8 -s false

@@ -53,96 +57,102 @@ jobs:
include:
- # test with the locked dependencies
os: ubuntu-latest
python-version: '3.10'
python-version: '3.11'
toxenv-factor: 'locked'
- # test with the lowest dependencies
os: ubuntu-20.04
python-version: '3.6'
os: ubuntu-latest
python-version: '3.7'
toxenv-factor: 'lowest'
steps:
- name: Checkout
# see https://github.com/actions/checkout
uses: actions/checkout@v3

- name: Setup Python Environment
# see https://github.com/actions/setup-python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
architecture: 'x64'

- name: Install poetry
# see https://github.com/marketplace/actions/setup-poetry
uses: Gr1N/setup-poetry@v8
with:
poetry-version: ${{ env.POETRY_VERSION }}

- name: Install dependencies
run: poetry install --no-root

- name: Run tox
run: poetry run tox -e mypy-${{ matrix.toxenv-factor }} -s false

build-and-test:
name: Test (${{ matrix.os }} py${{ matrix.python-version }} ${{ matrix.toxenv-factor }})
runs-on: ${{ matrix.os }}
timeout-minutes: 10
timeout-minutes: 15
env:
REPORTS_ARTIFACT: tests-reports
strategy:
fail-fast: false
matrix:
os: ['ubuntu-latest', 'windows-latest', 'macos-latest']
python-version:
- "3.10" # highest supported
- "3.11" # highest supported
- "3.10"
- "3.9"
- "3.8"
- "3.7"
- "3.6" # lowest supported
- "3.7" # lowest supported
toxenv-factor: ['locked']
include:
- # test with py36 ubuntu20
os: ubuntu-20.04
python-version: '3.6'
toxenv-factor: 'locked'
- # test with the lowest dependencies
os: ubuntu-20.04
python-version: '3.6'
toxenv-factor: 'lowest'
exclude:
- # no py36 with latest ubuntu - see https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json
os: ubuntu-latest
python-version: '3.6'
python-version: '3.7'
toxenv-factor: 'lowest'
steps:
- name: Disabled Git auto EOL CRLF transforms
run: |
git config --global core.autocrlf false
git config --global core.eol lf

- name: Checkout
# see https://github.com/actions/checkout
uses: actions/checkout@v3

- name: Create reports directory
run: mkdir ${{ env.REPORTS_DIR }}

- name: Setup Python Environment
# see https://github.com/actions/setup-python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
architecture: 'x64'

- name: Validate Python Environment
run: echo "import sys; print('Python %s on %s in %s' % (sys.version, sys.platform, sys.getdefaultencoding()))" | python

- name: Install poetry
# see https://github.com/marketplace/actions/setup-poetry
uses: Gr1N/setup-poetry@v8
with:
poetry-version: ${{ env.POETRY_VERSION }}

- name: Install dependencies
run: poetry install --no-root

- name: Ensure build successful
run: poetry build

- name: Run tox
run: poetry run tox -e py-${{ matrix.toxenv-factor }} -s false

- name: Generate coverage reports
run: >
poetry run coverage report &&
poetry run coverage xml -o ${{ env.REPORTS_DIR }}/coverage-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.toxenv-factor }}.xml &&
poetry run coverage html -d ${{ env.REPORTS_DIR }}

- name: Artifact reports
if: ${{ ! cancelled() }}
# see https://github.com/actions/upload-artifact
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ repos:
hooks:
- id: system
name: mypy
entry: poetry run tox -e mypy
entry: poetry run tox -e mypy-locked
pass_filenames: false
language: system
- repo: local
@@ -17,7 +17,7 @@ repos:
- repo: local
hooks:
- id: system
name: autopep8
entry: poetry run autopep8 --in-place -r cyclonedx tests
name: flake8
entry: poetry run flake8 cyclonedx/ tests/
pass_filenames: false
language: system
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@ Get it all applied via:

```shell
poetry run isort .
poetry run autopep8 --in-place -r cyclonedx tests
poetry run flake8 cyclonedx/ tests/
```

## Documentation
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -13,19 +13,21 @@
----

This CycloneDX module for Python can generate valid CycloneDX bill-of-material document containing an aggregate of all
project dependencies.
project dependencies. CycloneDX is a lightweight BOM specification that is easily created, human-readable, and simple
to parse.

This module is not designed for standalone use.
**This module is not designed for standalone use.**

If you're looking for a CycloneDX tool to run to generate (SBOM) software bill-of-materials documents, why not checkout: [CycloneDX Python][cyclonedx-python]
As of version `3.0.0`, the internal data model was adjusted to allow CycloneDX VEX documents to be produced as per
[official examples](https://cyclonedx.org/capabilities/bomlink/#linking-external-vex-to-bom-inventory) linking a VEX
documents to a separate BOM document.

Additionally, the following tool can be used as well (and this library was written to help improve it) [Jake][jake].
If you're looking for a CycloneDX tool to run to generate (SBOM) software bill-of-materials documents, why not checkout
[CycloneDX Python][cyclonedx-python] or [Jake][jake].

Additionally, you can use this module yourself in your application to programmatically generate SBOMs.
Alternatively, you can use this module yourself in your application to programmatically generate CycloneDX BOMs.

CycloneDX is a lightweight BOM specification that is easily created, human-readable, and simple to parse.

View our documentation [here](https://cyclonedx-python-library.readthedocs.io/).
View the documentation [here](https://cyclonedx-python-library.readthedocs.io/).

## Python Support

3 changes: 3 additions & 0 deletions cyclonedx/exception/__init__.py
Original file line number Diff line number Diff line change
@@ -21,4 +21,7 @@


class CycloneDxException(Exception):
"""
Root exception thrown by this library.
"""
pass
12 changes: 12 additions & 0 deletions cyclonedx/exception/factory.py
Original file line number Diff line number Diff line change
@@ -30,16 +30,28 @@ class CycloneDxFactoryException(CycloneDxException):


class LicenseChoiceFactoryException(CycloneDxFactoryException):
"""
Base exception that covers all LicenseChoiceFactory exceptions.
"""
pass


class InvalidSpdxLicenseException(LicenseChoiceFactoryException):
"""
Thrown when an invalid SPDX License is provided.
"""
pass


class LicenseFactoryException(CycloneDxFactoryException):
"""
Base exception that covers all LicenseFactory exceptions.
"""
pass


class InvalidLicenseExpressionException(LicenseFactoryException):
"""
Thrown when an invalid License expressions is provided.
"""
pass
7 changes: 7 additions & 0 deletions cyclonedx/exception/output.py
Original file line number Diff line number Diff line change
@@ -22,6 +22,13 @@
from . import CycloneDxException


class BomGenerationErrorException(CycloneDxException):
"""
Raised if there is an unknown error.
"""
pass


class FormatNotSupportedException(CycloneDxException):
"""
Exception raised when attempting to output a BOM to a format not supported in the requested version.
4 changes: 4 additions & 0 deletions cyclonedx/factory/__init__.py
Original file line number Diff line number Diff line change
@@ -14,3 +14,7 @@
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.

"""
Factories used in this library.
"""
21 changes: 9 additions & 12 deletions cyclonedx/factory/license.py
Original file line number Diff line number Diff line change
@@ -30,27 +30,24 @@ def make_from_string(self, name_or_spdx: str, *,
license_url: Optional[XsUri] = None) -> License:
"""Make a :class:`cyclonedx.model.License` from a string."""
try:
return self.make_with_id(name_or_spdx, license_text=license_text, license_url=license_url)
return self.make_with_id(name_or_spdx, text=license_text, url=license_url)
except InvalidSpdxLicenseException:
return self.make_with_name(name_or_spdx, license_text=license_text, license_url=license_url)
return self.make_with_name(name_or_spdx, text=license_text, url=license_url)

def make_with_id(self, spdx_id: str, *,
license_text: Optional[AttachedText] = None,
license_url: Optional[XsUri] = None) -> License:
def make_with_id(self, spdx_id: str, *, text: Optional[AttachedText] = None,
url: Optional[XsUri] = None) -> License:
"""Make a :class:`cyclonedx.model.License` from an SPDX-ID.

:raises InvalidSpdxLicenseException: if `spdx_id` was not known/supported SPDX-ID
"""
spdx_license_id = spdx_fixup(spdx_id)
if spdx_license_id is None:
raise InvalidSpdxLicenseException(spdx_id)
return License(spdx_license_id=spdx_license_id, license_text=license_text, license_url=license_url)
return License(id=spdx_license_id, text=text, url=url)

def make_with_name(self, name: str, *,
license_text: Optional[AttachedText] = None,
license_url: Optional[XsUri] = None) -> License:
def make_with_name(self, name: str, *, text: Optional[AttachedText] = None, url: Optional[XsUri] = None) -> License:
"""Make a :class:`cyclonedx.model.License` with a name."""
return License(license_name=name, license_text=license_text, license_url=license_url)
return License(name=name, text=text, url=url)


class LicenseChoiceFactory:
@@ -74,12 +71,12 @@ def make_with_compound_expression(self, compound_expression: str) -> LicenseChoi
:raises InvalidLicenseExpressionException: if `expression` is not known/supported license expression
"""
if is_spdx_compound_expression(compound_expression):
return LicenseChoice(license_expression=compound_expression)
return LicenseChoice(expression=compound_expression)
raise InvalidLicenseExpressionException(compound_expression)

def make_with_license(self, name_or_spdx: str, *,
license_text: Optional[AttachedText] = None,
license_url: Optional[XsUri] = None) -> LicenseChoice:
"""Make a :class:`cyclonedx.model.LicenseChoice` with a license (name or SPDX-ID)."""
return LicenseChoice(license_=self.license_factory.make_from_string(
return LicenseChoice(license=self.license_factory.make_from_string(
name_or_spdx, license_text=license_text, license_url=license_url))
265 changes: 175 additions & 90 deletions cyclonedx/model/__init__.py

Large diffs are not rendered by default.

246 changes: 206 additions & 40 deletions cyclonedx/model/bom.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion cyclonedx/model/bom_ref.py
Original file line number Diff line number Diff line change
@@ -44,7 +44,7 @@ def value(self, value: str) -> None:

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

def __lt__(self, other: Any) -> bool:
325 changes: 202 additions & 123 deletions cyclonedx/model/component.py

Large diffs are not rendered by default.

90 changes: 77 additions & 13 deletions cyclonedx/model/dependency.py
Original file line number Diff line number Diff line change
@@ -17,33 +17,97 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.

from typing import Iterable, Optional
from abc import ABC, abstractmethod
from typing import Any, Iterable, List, Optional, Set

import serializable
from sortedcontainers import SortedSet

from cyclonedx.model import ComparableTuple
from cyclonedx.serialization import BomRefHelper

from .bom_ref import BomRef


class DependencyDependencies(serializable.BaseHelper): # type: ignore

@classmethod
def serialize(cls, o: object) -> List[str]:
if isinstance(o, SortedSet):
return list(map(lambda i: str(i.ref), o))

raise ValueError(f'Attempt to serialize a non-Dependency: {o.__class__}')

@classmethod
def deserialize(cls, o: object) -> Set["Dependency"]:
dependencies: Set["Dependency"] = set()
if isinstance(o, list):
for v in o:
dependencies.add(Dependency(ref=BomRef(value=v)))
return dependencies


@serializable.serializable_class
class Dependency:
"""
This is our internal representation of a Dependency for a Component.
Models a Dependency within a BOM.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_dependencyType
See https://cyclonedx.org/docs/1.4/xml/#type_dependencyType
"""

def __init__(self, *, ref: BomRef, depends_on: Optional[Iterable[BomRef]] = None) -> None:
self._ref = ref
self.depends_on = depends_on or [] # type: ignore
def __init__(self, ref: BomRef, dependencies: Optional[Iterable["Dependency"]] = None) -> None:
self.ref = ref
self.dependencies = SortedSet(dependencies) or SortedSet()

@property
@property # type: ignore[misc]
@serializable.type_mapping(BomRefHelper)
@serializable.xml_attribute()
def ref(self) -> BomRef:
return self._ref

@property
def depends_on(self) -> "SortedSet[BomRef]":
return self._depends_on
@ref.setter
def ref(self, ref: BomRef) -> None:
self._ref = ref

@property # type: ignore[misc]
@serializable.json_name('dependsOn')
@serializable.type_mapping(DependencyDependencies)
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'dependency')
def dependencies(self) -> "SortedSet[Dependency]":
return self._dependencies

@depends_on.setter
def depends_on(self, depends_on: Iterable[BomRef]) -> None:
self._depends_on = SortedSet(depends_on)
@dependencies.setter
def dependencies(self, dependencies: Iterable["Dependency"]) -> None:
self._dependencies = SortedSet(dependencies)

def dependencies_as_bom_refs(self) -> Set[BomRef]:
return set(map(lambda d: d.ref, self.dependencies))

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

def __lt__(self, other: Any) -> bool:
if isinstance(other, Dependency):
return ComparableTuple((self.ref, tuple(self.dependencies))) < ComparableTuple(
(other.ref, tuple(other.dependencies)))
return NotImplemented

def __hash__(self) -> int:
return hash((self.ref, tuple(self.dependencies)))

def __repr__(self) -> str:
return f'<Dependency ref={self.ref}, targets={len(self.dependencies)}>'


class Dependable(ABC):
"""
Dependable objects can be part of the Dependency Graph
"""

@property
@abstractmethod
def bom_ref(self) -> BomRef:
pass
6 changes: 6 additions & 0 deletions cyclonedx/model/impact_analysis.py
Original file line number Diff line number Diff line change
@@ -19,6 +19,8 @@

from enum import Enum

import serializable

"""
This set of classes represents the data about Impact Analysis.
@@ -29,6 +31,7 @@
"""


@serializable.serializable_enum
class ImpactAnalysisAffectedStatus(str, Enum):
"""
Enum object that defines the permissible impact analysis affected states.
@@ -50,6 +53,7 @@ class ImpactAnalysisAffectedStatus(str, Enum):
UNKNOWN = 'unknown'


@serializable.serializable_enum
class ImpactAnalysisJustification(str, Enum):
"""
Enum object that defines the rationale of why the impact analysis state was asserted.
@@ -69,6 +73,7 @@ class ImpactAnalysisJustification(str, Enum):
REQUIRES_ENVIRONMENT = 'requires_environment'


@serializable.serializable_enum
class ImpactAnalysisResponse(str, Enum):
"""
Enum object that defines the valid rationales as to why the impact analysis state was asserted.
@@ -84,6 +89,7 @@ class ImpactAnalysisResponse(str, Enum):
WORKAROUND_AVAILABLE = 'workaround_available'


@serializable.serializable_enum
class ImpactAnalysisState(str, Enum):
"""
Enum object that defines the permissible impact analysis states.
36 changes: 23 additions & 13 deletions cyclonedx/model/issue.py
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@
from enum import Enum
from typing import Any, Iterable, Optional

import serializable
from sortedcontainers import SortedSet

from ..exception.model import NoPropertiesProvidedException
@@ -36,6 +37,7 @@ class IssueClassification(str, Enum):
SECURITY = 'security'


@serializable.serializable_class
class IssueTypeSource:
"""
This is our internal representation ofa source within the IssueType complex type that can be used in multiple
@@ -98,6 +100,7 @@ def __repr__(self) -> str:
return f'<IssueTypeSource name={self._name}, url={self.url}>'


@serializable.serializable_class
class IssueType:
"""
This is our internal representation of an IssueType complex type that can be used in multiple places within
@@ -107,17 +110,18 @@ class IssueType:
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_issueType
"""

def __init__(self, *, classification: IssueClassification, id_: Optional[str] = None, name: Optional[str] = None,
def __init__(self, *, type: IssueClassification, id: Optional[str] = None, name: Optional[str] = None,
description: Optional[str] = None, source: Optional[IssueTypeSource] = None,
references: Optional[Iterable[XsUri]] = None) -> None:
self.type = classification
self.id = id_
self.type = type
self.id = id
self.name = name
self.description = description
self.source = source
self.references = references or [] # type: ignore

@property
@property # type: ignore[misc]
@serializable.xml_attribute()
def type(self) -> IssueClassification:
"""
Specifies the type of issue.
@@ -128,10 +132,11 @@ def type(self) -> IssueClassification:
return self._type

@type.setter
def type(self, classification: IssueClassification) -> None:
self._type = classification
def type(self, type: IssueClassification) -> None:
self._type = type

@property
@property # type: ignore[misc]
@serializable.xml_sequence(1)
def id(self) -> Optional[str]:
"""
The identifier of the issue assigned by the source of the issue.
@@ -142,10 +147,11 @@ def id(self) -> Optional[str]:
return self._id

@id.setter
def id(self, id_: Optional[str]) -> None:
self._id = id_
def id(self, id: Optional[str]) -> None:
self._id = id

@property
@property # type: ignore[misc]
@serializable.xml_sequence(2)
def name(self) -> Optional[str]:
"""
The name of the issue.
@@ -159,7 +165,8 @@ def name(self) -> Optional[str]:
def name(self, name: Optional[str]) -> None:
self._name = name

@property
@property # type: ignore[misc]
@serializable.xml_sequence(3)
def description(self) -> Optional[str]:
"""
A description of the issue.
@@ -173,7 +180,8 @@ def description(self) -> Optional[str]:
def description(self, description: Optional[str]) -> None:
self._description = description

@property
@property # type: ignore[misc]
@serializable.xml_sequence(4)
def source(self) -> Optional[IssueTypeSource]:
"""
The source of this issue.
@@ -187,7 +195,9 @@ def source(self) -> Optional[IssueTypeSource]:
def source(self, source: Optional[IssueTypeSource]) -> None:
self._source = source

@property
@property # type: ignore[misc]
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'url')
@serializable.xml_sequence(5)
def references(self) -> "SortedSet[XsUri]":
"""
Any reference URLs related to this issue.
49 changes: 34 additions & 15 deletions cyclonedx/model/release_note.py
Original file line number Diff line number Diff line change
@@ -20,12 +20,14 @@
from datetime import datetime
from typing import Iterable, Optional

import serializable
from sortedcontainers import SortedSet

from ..model import Note, Property, XsUri
from ..model.issue import IssueType


@serializable.serializable_class
class ReleaseNotes:
"""
This is our internal representation of a `releaseNotesType` for a Component in a BOM.
@@ -34,12 +36,12 @@ class ReleaseNotes:
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/#type_releaseNotesType
"""

def __init__(self, *, type_: str, title: Optional[str] = None, featured_image: Optional[XsUri] = None,
def __init__(self, *, type: str, title: Optional[str] = None, featured_image: Optional[XsUri] = None,
social_image: Optional[XsUri] = None, description: Optional[str] = None,
timestamp: Optional[datetime] = None, aliases: Optional[Iterable[str]] = None,
tags: Optional[Iterable[str]] = None, resolves: Optional[Iterable[IssueType]] = None,
notes: Optional[Iterable[Note]] = None, properties: Optional[Iterable[Property]] = None) -> None:
self.type = type_
self.type = type
self.title = title
self.featured_image = featured_image
self.social_image = social_image
@@ -51,7 +53,8 @@ def __init__(self, *, type_: str, title: Optional[str] = None, featured_image: O
self.notes = notes or [] # type: ignore
self.properties = properties or [] # type: ignore

@property
@property # type: ignore[misc]
@serializable.xml_sequence(1)
def type(self) -> str:
"""
The software versioning type.
@@ -73,10 +76,11 @@ def type(self) -> str:
return self._type

@type.setter
def type(self, type_: str) -> None:
self._type = type_
def type(self, type: str) -> None:
self._type = type

@property
@property # type: ignore[misc]
@serializable.xml_sequence(2)
def title(self) -> Optional[str]:
"""
The title of the release.
@@ -87,7 +91,8 @@ def title(self) -> Optional[str]:
def title(self, title: Optional[str]) -> None:
self._title = title

@property
@property # type: ignore[misc]
@serializable.xml_sequence(3)
def featured_image(self) -> Optional[XsUri]:
"""
The URL to an image that may be prominently displayed with the release note.
@@ -98,7 +103,8 @@ def featured_image(self) -> Optional[XsUri]:
def featured_image(self, featured_image: Optional[XsUri]) -> None:
self._featured_image = featured_image

@property
@property # type: ignore[misc]
@serializable.xml_sequence(4)
def social_image(self) -> Optional[XsUri]:
"""
The URL to an image that may be used in messaging on social media platforms.
@@ -109,7 +115,8 @@ def social_image(self) -> Optional[XsUri]:
def social_image(self, social_image: Optional[XsUri]) -> None:
self._social_image = social_image

@property
@property # type: ignore[misc]
@serializable.xml_sequence(5)
def description(self) -> Optional[str]:
"""
A short description of the release.
@@ -120,7 +127,9 @@ def description(self) -> Optional[str]:
def description(self, description: Optional[str]) -> None:
self._description = description

@property
@property # type: ignore[misc]
@serializable.type_mapping(serializable.helpers.XsdDateTime)
@serializable.xml_sequence(6)
def timestamp(self) -> Optional[datetime]:
"""
The date and time (timestamp) when the release note was created.
@@ -131,7 +140,9 @@ def timestamp(self) -> Optional[datetime]:
def timestamp(self, timestamp: Optional[datetime]) -> None:
self._timestamp = timestamp

@property
@property # type: ignore[misc]
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'alias')
@serializable.xml_sequence(7)
def aliases(self) -> "SortedSet[str]":
"""
One or more alternate names the release may be referred to. This may include unofficial terms used by
@@ -146,7 +157,9 @@ def aliases(self) -> "SortedSet[str]":
def aliases(self, aliases: Iterable[str]) -> None:
self._aliases = SortedSet(aliases)

@property
@property # type: ignore[misc]
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'tag')
@serializable.xml_sequence(8)
def tags(self) -> "SortedSet[str]":
"""
One or more tags that may aid in search or retrieval of the release note.
@@ -160,7 +173,9 @@ def tags(self) -> "SortedSet[str]":
def tags(self, tags: Iterable[str]) -> None:
self._tags = SortedSet(tags)

@property
@property # type: ignore[misc]
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'issue')
@serializable.xml_sequence(9)
def resolves(self) -> "SortedSet[IssueType]":
"""
A collection of issues that have been resolved.
@@ -174,7 +189,9 @@ def resolves(self) -> "SortedSet[IssueType]":
def resolves(self, resolves: Iterable[IssueType]) -> None:
self._resolves = SortedSet(resolves)

@property
@property # type: ignore[misc]
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'note')
@serializable.xml_sequence(10)
def notes(self) -> "SortedSet[Note]":
"""
Zero or more release notes containing the locale and content. Multiple note elements may be specified to support
@@ -189,7 +206,9 @@ def notes(self) -> "SortedSet[Note]":
def notes(self, notes: Iterable[Note]) -> None:
self._notes = SortedSet(notes)

@property
@property # type: ignore[misc]
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property')
@serializable.xml_sequence(11)
def properties(self) -> "SortedSet[Property]":
"""
Provides the ability to document properties in a name-value store. This provides flexibility to include data not
80 changes: 60 additions & 20 deletions cyclonedx/model/service.py
Original file line number Diff line number Diff line change
@@ -15,10 +15,15 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.

from typing import Any, Iterable, Optional
from typing import Any, Iterable, Optional, Union
from uuid import uuid4

import serializable
from sortedcontainers import SortedSet

from cyclonedx.serialization import BomRefHelper

from ..schema.schema import SchemaVersion1Dot3, SchemaVersion1Dot4
from . import (
ComparableTuple,
DataClassification,
@@ -29,6 +34,7 @@
XsUri,
)
from .bom_ref import BomRef
from .dependency import Dependable
from .release_note import ReleaseNotes

"""
@@ -39,15 +45,17 @@
"""


class Service:
@serializable.serializable_class
class Service(Dependable):
"""
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,
def __init__(self, *, name: str, bom_ref: Optional[Union[str, BomRef]] = None,
provider: Optional[OrganizationalEntity] = None,
group: Optional[str] = None, version: Optional[str] = None, description: Optional[str] = None,
endpoints: Optional[Iterable[XsUri]] = None, authenticated: Optional[bool] = None,
x_trust_boundary: Optional[bool] = None, data: Optional[Iterable[DataClassification]] = None,
@@ -57,7 +65,10 @@ def __init__(self, *, name: str, bom_ref: Optional[str] = None, provider: Option
services: Optional[Iterable['Service']] = None,
release_notes: Optional[ReleaseNotes] = None,
) -> None:
self._bom_ref = BomRef(value=bom_ref)
if type(bom_ref) == BomRef:
self._bom_ref = bom_ref
else:
self._bom_ref = BomRef(value=str(bom_ref) if bom_ref else str(uuid4()))
self.provider = provider
self.group = group
self.name = name
@@ -73,7 +84,11 @@ def __init__(self, *, name: str, bom_ref: Optional[str] = None, provider: Option
self.release_notes = release_notes
self.properties = properties or [] # type: ignore

@property
@property # type: ignore[misc]
@serializable.json_name('bom-ref')
@serializable.type_mapping(BomRefHelper)
@serializable.xml_attribute()
@serializable.xml_name('bom-ref')
def bom_ref(self) -> BomRef:
"""
An optional identifier which can be used to reference the service elsewhere in the BOM. Uniqueness is enforced
@@ -86,7 +101,8 @@ def bom_ref(self) -> BomRef:
"""
return self._bom_ref

@property
@property # type: ignore[misc]
@serializable.xml_sequence(1)
def provider(self) -> Optional[OrganizationalEntity]:
"""
Get the The organization that provides the service.
@@ -100,7 +116,8 @@ def provider(self) -> Optional[OrganizationalEntity]:
def provider(self, provider: Optional[OrganizationalEntity]) -> None:
self._provider = provider

@property
@property # type: ignore[misc]
@serializable.xml_sequence(2)
def group(self) -> Optional[str]:
"""
The grouping name, namespace, or identifier. This will often be a shortened, single name of the company or
@@ -115,7 +132,8 @@ def group(self) -> Optional[str]:
def group(self, group: Optional[str]) -> None:
self._group = group

@property
@property # type: ignore[misc]
@serializable.xml_sequence(3)
def name(self) -> str:
"""
The name of the service. This will often be a shortened, single name of the service.
@@ -129,7 +147,8 @@ def name(self) -> str:
def name(self, name: str) -> None:
self._name = name

@property
@property # type: ignore[misc]
@serializable.xml_sequence(4)
def version(self) -> Optional[str]:
"""
The service version.
@@ -143,7 +162,8 @@ def version(self) -> Optional[str]:
def version(self, version: Optional[str]) -> None:
self._version = version

@property
@property # type: ignore[misc]
@serializable.xml_sequence(5)
def description(self) -> Optional[str]:
"""
Specifies a description for the service.
@@ -157,7 +177,9 @@ def description(self) -> Optional[str]:
def description(self, description: Optional[str]) -> None:
self._description = description

@property
@property # type: ignore[misc]
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'endpoint')
@serializable.xml_sequence(6)
def endpoints(self) -> "SortedSet[XsUri]":
"""
A list of endpoints URI's this service provides.
@@ -171,7 +193,8 @@ def endpoints(self) -> "SortedSet[XsUri]":
def endpoints(self, endpoints: Iterable[XsUri]) -> None:
self._endpoints = SortedSet(endpoints)

@property
@property # type: ignore[misc]
@serializable.xml_sequence(7)
def authenticated(self) -> Optional[bool]:
"""
A boolean value indicating if the service requires authentication. A value of true indicates the service
@@ -188,7 +211,10 @@ def authenticated(self) -> Optional[bool]:
def authenticated(self, authenticated: Optional[bool]) -> None:
self._authenticated = authenticated

@property
@property # type: ignore[misc]
@serializable.json_name('x-trust-boundary')
@serializable.xml_name('x-trust-boundary')
@serializable.xml_sequence(8)
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
@@ -205,7 +231,9 @@ def x_trust_boundary(self) -> Optional[bool]:
def x_trust_boundary(self, x_trust_boundary: Optional[bool]) -> None:
self._x_trust_boundary = x_trust_boundary

@property
@property # type: ignore[misc]
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'classification')
@serializable.xml_sequence(9)
def data(self) -> "SortedSet[DataClassification]":
"""
Specifies the data classification.
@@ -219,7 +247,9 @@ def data(self) -> "SortedSet[DataClassification]":
def data(self, data: Iterable[DataClassification]) -> None:
self._data = SortedSet(data)

@property
@property # type: ignore[misc]
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'licenses')
@serializable.xml_sequence(10)
def licenses(self) -> "SortedSet[LicenseChoice]":
"""
A optional list of statements about how this Service is licensed.
@@ -233,7 +263,9 @@ def licenses(self) -> "SortedSet[LicenseChoice]":
def licenses(self, licenses: Iterable[LicenseChoice]) -> None:
self._licenses = SortedSet(licenses)

@property
@property # type: ignore[misc]
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference')
@serializable.xml_sequence(11)
def external_references(self) -> "SortedSet[ExternalReference]":
"""
Provides the ability to document external references related to the Service.
@@ -247,7 +279,9 @@ def external_references(self) -> "SortedSet[ExternalReference]":
def external_references(self, external_references: Iterable[ExternalReference]) -> None:
self._external_references = SortedSet(external_references)

@property
@property # type: ignore[misc]
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'service')
@serializable.xml_sequence(13)
def services(self) -> "SortedSet['Service']":
"""
A list of services included or deployed behind the parent service.
@@ -265,7 +299,9 @@ def services(self) -> "SortedSet['Service']":
def services(self, services: Iterable['Service']) -> None:
self._services = SortedSet(services)

@property
@property # type: ignore[misc]
@serializable.view(SchemaVersion1Dot4)
@serializable.xml_sequence(14)
def release_notes(self) -> Optional[ReleaseNotes]:
"""
Specifies optional release notes.
@@ -279,7 +315,11 @@ def release_notes(self) -> Optional[ReleaseNotes]:
def release_notes(self, release_notes: Optional[ReleaseNotes]) -> None:
self._release_notes = release_notes

@property
@property # type: ignore[misc]
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property')
@serializable.xml_sequence(12)
def properties(self) -> "SortedSet[Property]":
"""
Provides the ability to document properties in a key/value store. This provides flexibility to include data not
@@ -313,4 +353,4 @@ def __hash__(self) -> int:
))

def __repr__(self) -> str:
return f'<Service group={self.group}, name={self.name}, version={self.version}, bom-ref={self.bom_ref}>'
return f'<Service bom-ref={self.bom_ref}, group={self.group}, name={self.name}, version={self.version}>'
206 changes: 142 additions & 64 deletions cyclonedx/model/vulnerability.py

Large diffs are not rendered by default.

29 changes: 3 additions & 26 deletions cyclonedx/output/__init__.py
Original file line number Diff line number Diff line change
@@ -22,34 +22,11 @@
import importlib
import os
from abc import ABC, abstractmethod
from enum import Enum
from typing import Iterable, Union, cast

from ..model.bom import Bom
from ..model.component import Component


class OutputFormat(str, Enum):
JSON: str = 'Json'
XML: str = 'Xml'


class SchemaVersion(str, Enum):
V1_0: str = 'V1Dot0'
V1_1: str = 'V1Dot1'
V1_2: str = 'V1Dot2'
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]}'

from ..schema import OutputFormat, SchemaVersion

LATEST_SUPPORTED_SCHEMA_VERSION = SchemaVersion.V1_4

@@ -125,7 +102,7 @@ def get_instance(bom: Bom, output_format: OutputFormat = OutputFormat.XML,
try:
module = importlib.import_module(f"cyclonedx.output.{output_format.value.lower()}")
output_klass = getattr(module, f"{output_format.value}{schema_version.value}")
except (ImportError, AttributeError):
raise ValueError(f"Unknown format {output_format.value.lower()!r}") from None
except (ImportError, AttributeError) as e:
raise ValueError(f"Unknown format {output_format.value.lower()!r}: {e}") from None

return cast(BaseOutput, output_klass(bom=bom))
189 changes: 28 additions & 161 deletions cyclonedx/output/json.py
Original file line number Diff line number Diff line change
@@ -19,26 +19,21 @@

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

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

ComponentDict = Dict[str, Union[
str,
List[Dict[str, str]],
List[Dict[str, Dict[str, str]]],
List[Dict[str, Union[str, List[Dict[str, str]]]]]]]
from . import BaseOutput


class Json(BaseOutput, BaseSchemaVersion):
@@ -52,171 +47,43 @@ 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

bom = self.get_bom()
bom.validate()

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

extras = {}
if self.bom_supports_dependencies():
dep_components = self._chained_components(bom)
if bom.metadata.component:
dep_components = [bom.metadata.component, *dep_components]
dependencies = []
for component in dep_components:
dependencies.append({
'ref': str(component.bom_ref),
'dependsOn': [*map(str, component.dependencies)]
})
if dependencies:
extras["dependencies"] = dependencies
del dep_components

if self.bom_supports_vulnerabilities():
vulnerabilities: List[Dict[Any, Any]] = []
if bom.components:
for component in bom.components:
for vulnerability in component.get_vulnerabilities():
vulnerabilities.append(
json.loads(json.dumps(vulnerability, cls=CycloneDxJSONEncoder))
)
if vulnerabilities:
extras["vulnerabilities"] = vulnerabilities

bom_json = json.loads(json.dumps(bom, cls=CycloneDxJSONEncoder))
bom_json = json.loads(self._specialise_output_for_schema_version(bom_json=bom_json))
self._json_output = json.dumps({**self._create_bom_element(), **bom_json, **extras})

self.generated = True

def _specialise_output_for_schema_version(self, bom_json: Dict[Any, Any]) -> str:
if 'metadata' in bom_json.keys():
if not self.bom_supports_metadata():
del bom_json['metadata']
else:
if 'tools' in bom_json['metadata'].keys():
if not self.bom_metadata_supports_tools():
del bom_json['metadata']['tools']
else:
if not self.bom_metadata_supports_tools_external_references():
for _tool in bom_json['metadata']['tools']:
if 'externalReferences' in _tool.keys():
del _tool['externalReferences']
del _tool
if 'licenses' in bom_json['metadata'].keys() and not self.bom_metadata_supports_licenses():
del bom_json['metadata']['licenses']
if 'properties' in bom_json['metadata'].keys() and not self.bom_metadata_supports_properties():
del bom_json['metadata']['properties']

if self.get_bom().metadata.component:
bom_json['metadata'] = self._recurse_specialise_component(bom_json['metadata'], 'component')

bom_json = self._recurse_specialise_component(bom_json)

if 'services' in bom_json.keys():
for _service in bom_json['services']:
if 'properties' in _service.keys() and not self.services_supports_properties():
del _service['properties']
if 'releaseNotes' in _service.keys() and not self.services_supports_release_notes():
del _service['releaseNotes']
del _service

if 'externalReferences' in bom_json.keys():
if not self.external_references_supports_hashes():
for _externalReference in bom_json['externalReferences']:
if 'hashes' in _externalReference.keys():
del _externalReference['hashes']
del _externalReference

return json.dumps(bom_json)
_json_core = {
'$schema': schema_uri,
'bomFormat': 'CycloneDX',
'specVersion': self.schema_version.to_version()
}
_view = SCHEMA_VERSIONS.get(self.get_schema_version())
if self.generated and force_regeneration:
self.get_bom().validate()
bom_json = json.loads(self.get_bom().as_json(view_=_view)) # type: ignore
bom_json.update(_json_core)
self._json_output = json.dumps(bom_json)
self.generated = True
return
elif self.generated:
return
else:
self.get_bom().validate()
bom_json = json.loads(self.get_bom().as_json(view_=_view)) # type: ignore
bom_json.update(_json_core)
self._json_output = json.dumps(bom_json)
self.generated = True
return

def output_as_string(self) -> str:
self.generate()
return self._json_output

# Builder Methods
def _create_bom_element(self) -> Dict[str, Union[str, int]]:
return {
"$schema": str(self._get_schema_uri()),
"bomFormat": "CycloneDX",
"specVersion": str(self.get_schema_version()),
"serialNumber": self.get_bom().get_urn_uuid(),
"version": 1
}

@abstractmethod
def _get_schema_uri(self) -> Optional[str]:
pass

def _recurse_specialise_component(self, bom_json: Dict[Any, Any], base_key: str = 'components') -> Dict[Any, Any]:
if base_key in bom_json.keys():
if isinstance(bom_json[base_key], dict):
bom_json[base_key] = self._specialise_component_data(component_json=bom_json[base_key])
else:
for i in range(len(bom_json[base_key])):
bom_json[base_key][i] = self._specialise_component_data(component_json=bom_json[base_key][i])

return bom_json

def _specialise_component_data(self, component_json: Dict[Any, Any]) -> Dict[Any, Any]:
if not self.component_supports_mime_type_attribute() and 'mime-type' in component_json.keys():
del component_json['mime-type']

if not self.component_supports_supplier() and 'supplier' in component_json.keys():
del component_json['supplier']

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

if self.component_version_optional() and 'version' in component_json \
and component_json.get('version', '') == "":
del component_json['version']

if not self.component_supports_pedigree() and 'pedigree' in component_json.keys():
del component_json['pedigree']
elif 'pedigree' in component_json.keys():
if 'ancestors' in component_json['pedigree'].keys():
# recurse into ancestors
component_json['pedigree'] = self._recurse_specialise_component(
bom_json=component_json['pedigree'], base_key='ancestors'
)
if 'descendants' in component_json['pedigree'].keys():
# recurse into descendants
component_json['pedigree'] = self._recurse_specialise_component(
bom_json=component_json['pedigree'], base_key='descendants'
)
if 'variants' in component_json['pedigree'].keys():
# recurse into variants
component_json['pedigree'] = self._recurse_specialise_component(
bom_json=component_json['pedigree'], base_key='variants'
)

if not self.external_references_supports_hashes() and 'externalReferences' \
in component_json.keys():
for j in range(len(component_json['externalReferences'])):
del component_json['externalReferences'][j]['hashes']

if not self.component_supports_properties() and 'properties' in component_json.keys():
del component_json['properties']

# recurse
if 'components' in component_json.keys():
component_json = self._recurse_specialise_component(bom_json=component_json)

if not self.component_supports_evidence() and 'evidence' in component_json.keys():
del component_json['evidence']

if not self.component_supports_release_notes() and 'releaseNotes' in component_json.keys():
del component_json['releaseNotes']

return component_json


class JsonV1Dot0(Json, SchemaVersion1Dot0):

342 changes: 0 additions & 342 deletions cyclonedx/output/schema.py

This file was deleted.

109 changes: 0 additions & 109 deletions cyclonedx/output/serializer/json.py

This file was deleted.

821 changes: 24 additions & 797 deletions cyclonedx/output/xml.py

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions cyclonedx/parser/__init__.py
Original file line number Diff line number Diff line change
@@ -20,7 +20,6 @@
Use a Parser instead of programmatically creating a Bom as a developer.
"""

from abc import ABC
from typing import List

from ..model.component import Component
@@ -41,10 +40,10 @@ def get_warning_message(self) -> str:
return self._warning

def __repr__(self) -> str:
return '<ParserWarning item=\'{}\'>'.format(self._item)
return f'<ParserWarning item=\'{self._item}\'>'


class BaseParser(ABC):
class BaseParser:
_components: List[Component] = []
_warnings: List[ParserWarning] = []

Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# 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
@@ -15,4 +13,27 @@
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.

from enum import Enum


class OutputFormat(str, Enum):
JSON: str = 'Json'
XML: str = 'Xml'


class SchemaVersion(str, Enum):
V1_0: str = 'V1Dot0'
V1_1: str = 'V1Dot1'
V1_2: str = 'V1Dot2'
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]}'
79 changes: 79 additions & 0 deletions cyclonedx/schema/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# 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.

from abc import ABC, abstractmethod

from serializable import ViewType

from ..schema import SchemaVersion


class BaseSchemaVersion(ABC, ViewType):

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

def get_schema_version(self) -> str:
return self.schema_version_enum.to_version()


class SchemaVersion1Dot4(BaseSchemaVersion):

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


class SchemaVersion1Dot3(BaseSchemaVersion):

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


class SchemaVersion1Dot2(BaseSchemaVersion):

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


class SchemaVersion1Dot1(BaseSchemaVersion):

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


class SchemaVersion1Dot0(BaseSchemaVersion):

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


SCHEMA_VERSIONS = {
'1.0': SchemaVersion1Dot0,
'1.1': SchemaVersion1Dot1,
'1.2': SchemaVersion1Dot2,
'1.3': SchemaVersion1Dot3,
'1.4': SchemaVersion1Dot4
}
78 changes: 78 additions & 0 deletions cyclonedx/serialization/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# 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

"""
Set of helper classes for use with ``serializable`` when conducting (de-)serialization.
"""

from uuid import UUID

# See https://github.com/package-url/packageurl-python/issues/65
from packageurl import PackageURL # type: ignore
from serializable.helpers import BaseHelper

from ..model.bom_ref import BomRef


class BomRefHelper(BaseHelper):

@classmethod
def serialize(cls, o: object) -> str:
if isinstance(o, BomRef):
return o.value

raise ValueError(f'Attempt to serialize a non-BomRef: {o.__class__}')

@classmethod
def deserialize(cls, o: object) -> BomRef:
try:
return BomRef(value=str(o))
except ValueError:
raise ValueError(f'BomRef string supplied ({o}) does not parse!')


class PackageUrl(BaseHelper):

@classmethod
def serialize(cls, o: object) -> str:
if isinstance(o, PackageURL):
return str(o.to_string())

raise ValueError(f'Attempt to serialize a non-PackageURL: {o.__class__}')

@classmethod
def deserialize(cls, o: object) -> PackageURL:
try:
return PackageURL.from_string(purl=str(o))
except ValueError:
raise ValueError(f'PURL string supplied ({o}) does not parse!')


class UrnUuidHelper(BaseHelper):

@classmethod
def serialize(cls, o: object) -> str:
if isinstance(o, UUID):
return o.urn

raise ValueError(f'Attempt to serialize a non-UUID: {o.__class__}')

@classmethod
def deserialize(cls, o: object) -> PackageURL:
try:
return UUID(str(o))
except ValueError:
raise ValueError(f'UUID string supplied ({o}) does not parse!')
6 changes: 5 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
@@ -20,6 +20,9 @@ CycloneDX is a lightweight BOM specification that is easily created, human-reada
This CycloneDX module for Python can generate valid CycloneDX bill-of-material document containing an aggregate of all
project dependencies.

As of version ``3.0.0``, the internal data model was adjusted to allow CycloneDX VEX documents to be produced as per
`official examples`_ linking VEX to a separate BOM.

This module is not designed for standalone use (i.e. it is not executable on it’s own). If you’re looking for a
CycloneDX tool to run to generate (SBOM) software bill-of-materials documents, why not checkout:

@@ -44,4 +47,5 @@ programmatically generate SBOMs.

.. _CycloneDX Python: https://pypi.org/project/cyclonedx-bom/
.. _Jake: https://pypi.org/project/jake
.. _CycloneDX Tool Center: https://cyclonedx.org/tool-center/
.. _CycloneDX Tool Center: https://cyclonedx.org/tool-center/
.. _official examples: https://cyclonedx.org/capabilities/bomlink/#linking-external-vex-to-bom-inventory
1 change: 1 addition & 0 deletions docs/schema-support.rst
Original file line number Diff line number Diff line change
@@ -43,6 +43,7 @@ supported in prior versions of the CycloneDX schema.
| ``bom.properties`` | No | See `schema specification bug 130`_ |
+----------------------------+---------------+---------------------------------------------------------------------------------------------------+
| ``bom.vulnerabilities`` | Yes | Note: Prior to CycloneDX 1.4, these were present under ``bom.components`` via a schema extension. |
| | | Note: As of ``cyclonedx-python-lib`` ``>3.0.0``, Vulnerability are modelled differently |
+----------------------------+---------------+---------------------------------------------------------------------------------------------------+
| ``bom.signature`` | No | |
+----------------------------+---------------+---------------------------------------------------------------------------------------------------+
6 changes: 0 additions & 6 deletions docs/support.rst
Original file line number Diff line number Diff line change
@@ -30,11 +30,5 @@ We endeavour to support all functionality for all `current actively supported Py
However, some features may not be possible/present in older Python versions due to their lack of support - which are
noted below.

Limitations in Python 3.6.x
~~~~~~~~~~~~~~~~~~~~~~~~~~~

* Unit Tests perform schema validation as part of their assertions. The ``jsonschema`` library does not support Python
3.6 and thus schema validation for JSON tests is skipped when running on Python 3.6.x

.. _GitHub Issue: https://github.com/CycloneDX/cyclonedx-python/issues
.. _current actively supported Python versions: https://www.python.org/downloads/
594 changes: 447 additions & 147 deletions poetry.lock

Large diffs are not rendered by default.

19 changes: 8 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -30,11 +30,11 @@ classifiers = [
'Topic :: Software Development',
'Topic :: System :: Software Distribution',
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Typing :: Typed',
]
keywords = [
@@ -46,30 +46,27 @@ keywords = [

[tool.poetry.dependencies]
# ATTENTION: keep `requirements.lowest.txt` file in sync
python = "^3.6"
packageurl-python = ">= 0.9"
python = "^3.7"
importlib-metadata = { version = ">= 3.4", python = "< 3.8" }
packageurl-python = ">= 0.9"
py-serializable = "^0.11.1"
setuptools = ">= 47.0.0"
toml = "^0.10.0"
sortedcontainers = "^2.4.0"

[tool.poetry.dev-dependencies]
tox = "^3.25.0"
ddt = "^1.6.0"
coverage = "^6.2"
mypy = ">= 0.920, <= 0.961"
autopep8 = "^1.6.0"
isort = { version = "^5.10.0", python = ">= 3.6.1" }
isort = "^5.10.0"
flake8 = "^4.0.1"
flake8-annotations = {version = "^2.7.0", python = ">= 3.6.2"}
flake8-annotations = "^2.7.0"
flake8-bugbear = "^22.7.1"
flake8-isort = { version = "^4.1.2", python = ">= 3.6.1" }
jsonschema = { version = ">= 4.4.0", python = "> 3.6"}
flake8-isort = "^4.1.2"
jsonschema = ">= 4.4.0"
lxml = ">=4.7.0"
# `types-setuptools` need to stay in sync with version of `setuptools` - but 47 was not typed...
types-setuptools = ">= 57.0.0"
# `types-toml` need to stay in sync with version of `toml`
types-toml = "^0.10.0"
xmldiff = ">=2.4"

[build-system]
3 changes: 1 addition & 2 deletions requirements.lowest.txt
Original file line number Diff line number Diff line change
@@ -2,8 +2,7 @@
# see pyptoject file for ranges

packageurl-python == 0.9.0
py-serializable == 0.11.1
importlib-metadata == 3.4.0 # ; python_version < '3.8'
setuptools == 47.0.0
types-setuptools == 57.0.0
toml == 0.10.0
types-toml == 0.10.0
10 changes: 7 additions & 3 deletions tests/base.py
Original file line number Diff line number Diff line change
@@ -38,14 +38,18 @@
from jsonschema import ValidationError, validate as json_validate

if sys.version_info >= (3, 8):
from importlib.metadata import version
from importlib.metadata import PackageNotFoundError, version
else:
from importlib_metadata import version
from importlib_metadata import PackageNotFoundError, version

from . import CDX_SCHEMA_DIRECTORY

cyclonedx_lib_name: str = 'cyclonedx-python-lib'
cyclonedx_lib_version: str = version(cyclonedx_lib_name)
cyclonedx_lib_version: str = 'DEV'
try:
cyclonedx_lib_version: str = version(cyclonedx_lib_name)
except PackageNotFoundError:
pass
single_uuid: str = 'urn:uuid:{}'.format(uuid4())


105 changes: 58 additions & 47 deletions tests/data.py
Original file line number Diff line number Diff line change
@@ -21,8 +21,10 @@
from datetime import datetime, timezone
from decimal import Decimal
from typing import List, Optional, TypeVar
from uuid import UUID

from packageurl import PackageURL
# See https://github.com/package-url/packageurl-python/issues/65
from packageurl import PackageURL # type: ignore

from cyclonedx.model import (
AttachedText,
@@ -55,6 +57,7 @@
Pedigree,
Swid,
)
from cyclonedx.model.dependency import Dependency
from cyclonedx.model.issue import IssueClassification, IssueType, IssueTypeSource
from cyclonedx.model.release_note import ReleaseNotes
from cyclonedx.model.service import Service
@@ -83,9 +86,15 @@
MOCK_UUID_4 = 'cd3e9c95-9d41-49e7-9924-8cf0465ae789'
MOCK_UUID_5 = 'bb5911d6-1a1d-41c9-b6e0-46e848d16655'
MOCK_UUID_6 = 'df70b5f1-8f53-47a4-be48-669ae78795e6'
MOCK_UUID_7 = UUID('6f266d1c-760f-4552-ae3b-41a9b74232fa')
MOCK_UUID_8 = UUID('77d15ab9-5602-4cca-8ed2-59ae579aafd3')
MOCK_UUID_9 = UUID('859ff614-35a7-4d37-803b-d89130cb2577')
MOCK_UUID_10 = UUID('0afa65bc-4acd-428b-9e17-8e97b1969745')
MOCK_BOM_UUID_1 = UUID('3e671687-395b-41f5-a30f-a58921a69b79')
MOCK_BOM_UUID_2 = UUID('d0b24ba4-102b-497e-b31f-4fdc3f0a3005')

TEST_UUIDS = [
MOCK_UUID_1, MOCK_UUID_2, MOCK_UUID_3, MOCK_UUID_4, MOCK_UUID_5, MOCK_UUID_6
UUID(MOCK_UUID_1), UUID(MOCK_UUID_2), UUID(MOCK_UUID_3), UUID(MOCK_UUID_4), UUID(MOCK_UUID_5), UUID(MOCK_UUID_6)
]


@@ -111,29 +120,30 @@ def get_bom_with_component_setuptools_with_release_notes() -> Bom:

def get_bom_with_dependencies_valid() -> Bom:
c1 = get_component_setuptools_simple()
c1.dependencies.update([
get_component_toml_with_hashes_with_references().bom_ref
])
return Bom(components=[
c1,
get_component_toml_with_hashes_with_references()
])
c2 = get_component_toml_with_hashes_with_references()
return Bom(
components=[c1, c2], dependencies=[
Dependency(ref=c1.bom_ref, dependencies=[
Dependency(ref=c2.bom_ref)
]),
Dependency(ref=c2.bom_ref)
]
)


def get_bom_with_dependencies_invalid() -> Bom:
c1 = get_component_setuptools_simple()
c1.dependencies.update([
get_component_toml_with_hashes_with_references().bom_ref
])
return Bom(components=[
c1
])
return Bom(components=[c1], dependencies=[Dependency(ref=c1.bom_ref)])


def get_bom_with_metadata_component_and_dependencies() -> Bom:
bom = Bom(components=[get_component_toml_with_hashes_with_references()])
bom.metadata.component = get_component_setuptools_simple()
bom.metadata.component.dependencies.update([get_component_toml_with_hashes_with_references().bom_ref])
bom.dependencies.add(
Dependency(ref=bom.metadata.component.bom_ref, dependencies=[
Dependency(ref=get_component_toml_with_hashes_with_references().bom_ref)
])
)
return bom


@@ -144,7 +154,8 @@ def get_bom_with_component_setuptools_complete() -> Bom:
def get_bom_with_component_setuptools_with_vulnerability() -> Bom:
bom = Bom()
component = get_component_setuptools_simple()
vulnerability = Vulnerability(
bom.components.add(component)
bom.vulnerabilities.add(Vulnerability(
bom_ref='my-vuln-ref-1', id='CVE-2018-7489', source=get_vulnerability_source_nvd(),
references=[
VulnerabilityReference(id='SOME-OTHER-ID', source=VulnerabilitySource(
@@ -188,18 +199,16 @@ def get_bom_with_component_setuptools_with_vulnerability() -> Bom:
state=ImpactAnalysisState.EXPLOITABLE, justification=ImpactAnalysisJustification.REQUIRES_ENVIRONMENT,
responses=[ImpactAnalysisResponse.CAN_NOT_FIX], detail='Some extra detail'
),
affects_targets=[
affects=[
BomTarget(
ref=component.purl.to_string() if component.purl else None,
versions=[BomTargetVersionRange(
version_range='49.0.0 - 54.0.0', status=ImpactAnalysisAffectedStatus.AFFECTED
range='49.0.0 - 54.0.0', status=ImpactAnalysisAffectedStatus.AFFECTED
)]
)
],
properties=get_properties_1()
)
component.add_vulnerability(vulnerability=vulnerability)
bom.components.add(component)
))
return bom


@@ -213,10 +222,10 @@ def get_bom_just_complete_metadata() -> Bom:
bom.metadata.component = get_component_setuptools_complete()
bom.metadata.manufacture = get_org_entity_1()
bom.metadata.supplier = get_org_entity_2()
bom.metadata.licenses = [LicenseChoice(license_=License(
spdx_license_id='Apache-2.0', license_text=AttachedText(
bom.metadata.licenses = [LicenseChoice(license=License(
id='Apache-2.0', text=AttachedText(
content='VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=', encoding=Encoding.BASE_64
), license_url=XsUri('https://www.apache.org/licenses/LICENSE-2.0.txt')
), url=XsUri('https://www.apache.org/licenses/LICENSE-2.0.txt')
))]
bom.metadata.properties = get_properties_1()
return bom
@@ -235,7 +244,7 @@ def get_bom_with_services_simple() -> Bom:
Service(name='my-second-service')
])
bom.metadata.component = Component(
name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY
name='cyclonedx-python-lib', version='1.0.0', type=ComponentType.LIBRARY
)
return bom

@@ -253,7 +262,7 @@ def get_bom_with_services_complex() -> Bom:
DataClassification(flow=DataFlow.OUTBOUND, classification='public')
],
licenses=[
LicenseChoice(license_expression='Commercial')
LicenseChoice(expression='Commercial')
],
external_references=[
get_external_reference_1()
@@ -264,7 +273,7 @@ def get_bom_with_services_complex() -> Bom:
Service(name='my-second-service')
])
bom.metadata.component = Component(
name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY
name='cyclonedx-python-lib', version='1.0.0', type=ComponentType.LIBRARY
)
return bom

@@ -282,7 +291,7 @@ def get_bom_with_nested_services() -> Bom:
DataClassification(flow=DataFlow.OUTBOUND, classification='public')
],
licenses=[
LicenseChoice(license_expression='Commercial')
LicenseChoice(expression='Commercial')
],
external_references=[
get_external_reference_1()
@@ -314,7 +323,7 @@ def get_bom_with_nested_services() -> Bom:
)
])
bom.metadata.component = Component(
name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY
name='cyclonedx-python-lib', version='1.0.0', type=ComponentType.LIBRARY
)
return bom

@@ -330,14 +339,16 @@ def get_bom_for_issue_275_components() -> Bom:
comp_c = Component(bom_ref=MOCK_UUID_4, name="comp_c", version="1.0.0")

comp_b.components.add(comp_c)
comp_b.dependencies.add(comp_c.bom_ref)
# comp_b.dependencies.add(comp_c.bom_ref)

libs = [comp_a, comp_b]
app.dependencies.add(comp_a.bom_ref)
app.dependencies.add(comp_b.bom_ref)
# app.dependencies.add(comp_a.bom_ref)
# app.dependencies.add(comp_b.bom_ref)

bom = Bom(components=libs)
bom.metadata.component = app
bom.register_dependency(target=app, depends_on=[comp_a, comp_b])
bom.register_dependency(target=comp_b, depends_on=[comp_c])
return bom


@@ -363,7 +374,7 @@ def get_bom_for_issue_328_components() -> Bom:
see https://github.com/CycloneDX/cyclonedx-python-lib/issues/328
"""

comp_root = Component(component_type=ComponentType.APPLICATION,
comp_root = Component(type=ComponentType.APPLICATION,
name='my-project', version='1', bom_ref='my-project')
comp_a = Component(name='A', version='0.1', bom_ref='component-A')
comp_b = Component(name='B', version='1.0', bom_ref='component-B')
@@ -402,7 +413,7 @@ def get_component_setuptools_complete(include_pedigree: bool = True) -> Componen
get_component_setuptools_simple(),
get_component_toml_with_hashes_with_references()
])
component.evidence = ComponentEvidence(copyright_=[Copyright(text='Commercial'), Copyright(text='Commercial 2')])
component.evidence = ComponentEvidence(copyright=[Copyright(text='Commercial'), Copyright(text='Commercial 2')])
component.release_notes = get_release_notes()
return component

@@ -416,7 +427,7 @@ def get_component_setuptools_simple(
purl=PackageURL(
type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz'
),
licenses=[LicenseChoice(license_expression='MIT License')],
licenses=[LicenseChoice(expression='MIT License')],
author='Test Author'
)

@@ -427,7 +438,7 @@ def get_component_setuptools_simple_no_version(bom_ref: Optional[str] = None) ->
purl=PackageURL(
type='pypi', name='setuptools', qualifiers='extension=tar.gz'
),
licenses=[LicenseChoice(license_expression='MIT License')],
licenses=[LicenseChoice(expression='MIT License')],
author='Test Author'
)

@@ -447,7 +458,7 @@ def get_component_toml_with_hashes_with_references(bom_ref: Optional[str] = None

def get_external_reference_1() -> ExternalReference:
return ExternalReference(
reference_type=ExternalReferenceType.DISTRIBUTION,
type=ExternalReferenceType.DISTRIBUTION,
url=XsUri('https://cyclonedx.org'),
comment='No comment',
hashes=[
@@ -459,14 +470,14 @@ def get_external_reference_1() -> ExternalReference:

def get_external_reference_2() -> ExternalReference:
return ExternalReference(
reference_type=ExternalReferenceType.WEBSITE,
type=ExternalReferenceType.WEBSITE,
url=XsUri('https://cyclonedx.org')
)


def get_issue_1() -> IssueType:
return IssueType(
classification=IssueClassification.SECURITY, id_='CVE-2021-44228', name='Apache Log3Shell',
type=IssueClassification.SECURITY, id='CVE-2021-44228', name='Apache Log3Shell',
description='Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features...',
source=IssueTypeSource(name='NVD', url=XsUri('https://nvd.nist.gov/vuln/detail/CVE-2021-44228')),
references=[
@@ -478,7 +489,7 @@ def get_issue_1() -> IssueType:

def get_issue_2() -> IssueType:
return IssueType(
classification=IssueClassification.SECURITY, id_='CVE-2021-44229', name='Apache Log4Shell',
type=IssueClassification.SECURITY, id='CVE-2021-44229', name='Apache Log4Shell',
description='Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features...',
source=IssueTypeSource(name='NVD', url=XsUri('https://nvd.nist.gov/vuln/detail/CVE-2021-44228')),
references=[
@@ -523,7 +534,7 @@ def get_pedigree_1() -> Pedigree:
get_component_setuptools_simple(bom_ref='ded1d73e-1fca-4302-b520-f1bc53979958')
],
commits=[Commit(uid='a-random-uid', message="A commit message")],
patches=[Patch(type_=PatchClassification.BACKPORT)],
patches=[Patch(type=PatchClassification.BACKPORT)],
notes='Some notes here please'
)

@@ -541,7 +552,7 @@ def get_release_notes() -> ReleaseNotes:
).decode(encoding='UTF-8')

return ReleaseNotes(
type_='major', title="Release Notes Title",
type='major', title="Release Notes Title",
featured_image=XsUri('https://cyclonedx.org/theme/assets/images/CycloneDX-Twitter-Card.png'),
social_image=XsUri('https://cyclonedx.org/cyclonedx-icon.png'),
description="This release is a test release", timestamp=MOCK_TIMESTAMP,
@@ -554,13 +565,13 @@ def get_release_notes() -> ReleaseNotes:
Note(
text=NoteText(
content=text_content, content_type='text/plain; charset=UTF-8',
content_encoding=Encoding.BASE_64
encoding=Encoding.BASE_64
), locale='en-GB'
),
Note(
text=NoteText(
content=text_content, content_type='text/plain; charset=UTF-8',
content_encoding=Encoding.BASE_64
encoding=Encoding.BASE_64
), locale='en-US'
)
],
@@ -604,9 +615,9 @@ def get_vulnerability_source_owasp() -> VulnerabilitySource:


def reorder(items: List[T], indexes: List[int]) -> List[T]:
'''
"""
Return list of items in the order indicated by indexes.
'''
"""
reordered_items = []
for i in range(len(items)):
reordered_items.append(items[indexes[i]])
3 changes: 1 addition & 2 deletions tests/fixtures/json/1.2/bom_dependencies.json
Original file line number Diff line number Diff line change
@@ -57,8 +57,7 @@
]
},
{
"ref": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"dependsOn": []
"ref": "pkg:pypi/toml@0.10.2?extension=tar.gz"
}
]
}
3 changes: 1 addition & 2 deletions tests/fixtures/json/1.2/bom_dependencies_component.json
Original file line number Diff line number Diff line change
@@ -57,8 +57,7 @@
]
},
{
"ref": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"dependsOn": []
"ref": "pkg:pypi/toml@0.10.2?extension=tar.gz"
}
]
}
4 changes: 2 additions & 2 deletions tests/fixtures/json/1.2/bom_external_references.json
Original file line number Diff line number Diff line change
@@ -2,10 +2,10 @@
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.2",
"serialNumber": "urn:uuid:b254c902-deb4-4298-9969-027541ee365c",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2023-01-07T13:45:57.467119+00:00",
"timestamp": "2023-01-07T13:44:32.312678+00:00",
"tools": [
{
"vendor": "CycloneDX",
6 changes: 2 additions & 4 deletions tests/fixtures/json/1.2/bom_issue_275_components.json
Original file line number Diff line number Diff line change
@@ -51,8 +51,7 @@
]
},
{
"ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda",
"dependsOn": []
"ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda"
},
{
"ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857",
@@ -61,8 +60,7 @@
]
},
{
"ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
"dependsOn": []
"ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789"
}
]
}
45 changes: 22 additions & 23 deletions tests/fixtures/json/1.2/bom_services_complex.json
Original file line number Diff line number Diff line change
@@ -2,10 +2,10 @@
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.2",
"serialNumber": "urn:uuid:97251de9-d320-463f-a31d-399d88719b86",
"serialNumber": "urn:uuid:d0b24ba4-102b-497e-b31f-4fdc3f0a3005",
"version": 1,
"metadata": {
"timestamp": "2023-01-07T13:45:57.701426+00:00",
"timestamp": "2023-01-07T13:44:32.312678+00:00",
"tools": [
{
"vendor": "CycloneDX",
@@ -15,7 +15,7 @@
],
"component": {
"type": "library",
"bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda",
"bom-ref": "bb5911d6-1a1d-41c9-b6e0-46e848d16655",
"name": "cyclonedx-python-lib",
"version": "1.0.0"
}
@@ -24,20 +24,20 @@
{
"bom-ref": "my-specific-bom-ref-for-my-first-service",
"provider": {
"name": "CycloneDX",
"url": [
"https://cyclonedx.org"
],
"contact": [
{
"name": "A N Other",
"email": "someone@somewhere.tld",
"phone": "+44 (0)1234 567890"
"email": "paul.horton@owasp.org",
"name": "Paul Horton"
},
{
"name": "Paul Horton",
"email": "paul.horton@owasp.org"
"email": "someone@somewhere.tld",
"name": "A N Other",
"phone": "+44 (0)1234 567890"
}
],
"name": "CycloneDX",
"url": [
"https://cyclonedx.org"
]
},
"group": "a-group",
@@ -52,8 +52,8 @@
"x-trust-boundary": true,
"data": [
{
"flow": "outbound",
"classification": "public"
"classification": "public",
"flow": "outbound"
}
],
"licenses": [
@@ -63,15 +63,9 @@
],
"externalReferences": [
{
"url": "https://cyclonedx.org",
"comment": "No comment",
"type": "distribution",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
]
"url": "https://cyclonedx.org"
}
]
},
@@ -82,8 +76,13 @@
],
"dependencies": [
{
"ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda",
"dependsOn": []
"ref": "bb5911d6-1a1d-41c9-b6e0-46e848d16655"
},
{
"ref": "my-specific-bom-ref-for-my-first-service"
},
{
"ref": "be2c6502-7e9a-47db-9a66-e34f729810a3"
}
]
}
159 changes: 79 additions & 80 deletions tests/fixtures/json/1.2/bom_services_nested.json
Original file line number Diff line number Diff line change
@@ -1,136 +1,128 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.2",
"serialNumber": "urn:uuid:3471759e-0fc2-4284-b558-e760e6d23c6c",
"version": 1,
"metadata": {
"timestamp": "2023-01-07T13:45:57.729167+00:00",
"component": {
"bom-ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
"name": "cyclonedx-python-lib",
"type": "library",
"version": "1.0.0"
},
"timestamp": "2023-01-07T13:44:32.312678+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"vendor": "CycloneDX",
"version": "TESTING"
}
],
"component": {
"type": "library",
"bom-ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
"name": "cyclonedx-python-lib",
"version": "1.0.0"
}
]
},
"serialNumber": "urn:uuid:d0b24ba4-102b-497e-b31f-4fdc3f0a3005",
"services": [
{
"authenticated": false,
"bom-ref": "my-specific-bom-ref-for-my-first-service",
"provider": {
"name": "CycloneDX",
"url": [
"https://cyclonedx.org"
],
"contact": [
{
"name": "A N Other",
"email": "someone@somewhere.tld",
"phone": "+44 (0)1234 567890"
},
{
"name": "Paul Horton",
"email": "paul.horton@owasp.org"
}
]
},
"group": "a-group",
"name": "my-first-service",
"version": "1.2.3",
"data": [
{
"classification": "public",
"flow": "outbound"
}
],
"description": "Description goes here",
"endpoints": [
"/api/thing/1",
"/api/thing/2"
],
"authenticated": false,
"x-trust-boundary": true,
"data": [
"externalReferences": [
{
"flow": "outbound",
"classification": "public"
"comment": "No comment",
"type": "distribution",
"url": "https://cyclonedx.org"
}
],
"group": "a-group",
"licenses": [
{
"expression": "Commercial"
}
],
"externalReferences": [
{
"url": "https://cyclonedx.org",
"comment": "No comment",
"type": "distribution",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
]
}
],
"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": "be2c6502-7e9a-47db-9a66-e34f729810a3",
"name": "first-nested-service"
},
{
"authenticated": true,
"bom-ref": "my-specific-bom-ref-for-second-nested-service",
"group": "no-group",
"name": "second-nested-service",
"provider": {
"name": "CycloneDX",
"url": [
"https://cyclonedx.org"
],
"contact": [
{
"name": "A N Other",
"email": "someone@somewhere.tld",
"phone": "+44 (0)1234 567890"
"email": "paul.horton@owasp.org",
"name": "Paul Horton"
},
{
"name": "Paul Horton",
"email": "paul.horton@owasp.org"
"email": "someone@somewhere.tld",
"name": "A N Other",
"phone": "+44 (0)1234 567890"
}
],
"name": "CycloneDX",
"url": [
"https://cyclonedx.org"
]
},
"group": "no-group",
"name": "second-nested-service",
"version": "3.2.1",
"authenticated": true,
"x-trust-boundary": false
},
{
"bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3",
"name": "first-nested-service"
}
]
],
"version": "1.2.3",
"x-trust-boundary": true
},
{
"bom-ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857",
"name": "my-second-service",
"services": [
{
"bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda",
"group": "what-group",
"name": "yet-another-nested-service",
"provider": {
"name": "CycloneDX",
"url": [
"https://cyclonedx.org"
],
"contact": [
{
"name": "A N Other",
"email": "someone@somewhere.tld",
"phone": "+44 (0)1234 567890"
"email": "paul.horton@owasp.org",
"name": "Paul Horton"
},
{
"name": "Paul Horton",
"email": "paul.horton@owasp.org"
"email": "someone@somewhere.tld",
"name": "A N Other",
"phone": "+44 (0)1234 567890"
}
],
"name": "CycloneDX",
"url": [
"https://cyclonedx.org"
]
},
"group": "what-group",
"name": "yet-another-nested-service",
"version": "6.5.4"
},
{
@@ -140,10 +132,17 @@
]
}
],
"specVersion": "1.2",
"version": 1,
"dependencies": [
{
"ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
"dependsOn": []
"ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857"
},
{
"ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789"
},
{
"ref": "my-specific-bom-ref-for-my-first-service"
}
]
}
15 changes: 10 additions & 5 deletions tests/fixtures/json/1.2/bom_services_simple.json
Original file line number Diff line number Diff line change
@@ -2,10 +2,10 @@
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.2",
"serialNumber": "urn:uuid:228f339b-f73c-4379-af8e-3ad09dbde1d8",
"serialNumber": "urn:uuid:d0b24ba4-102b-497e-b31f-4fdc3f0a3005",
"version": 1,
"metadata": {
"timestamp": "2023-01-07T13:45:57.753759+00:00",
"timestamp": "2023-01-07T13:44:32.312678+00:00",
"tools": [
{
"vendor": "CycloneDX",
@@ -15,7 +15,7 @@
],
"component": {
"type": "library",
"bom-ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857",
"bom-ref": "df70b5f1-8f53-47a4-be48-669ae78795e6",
"name": "cyclonedx-python-lib",
"version": "1.0.0"
}
@@ -32,8 +32,13 @@
],
"dependencies": [
{
"ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857",
"dependsOn": []
"ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda"
},
{
"ref": "be2c6502-7e9a-47db-9a66-e34f729810a3"
},
{
"ref": "df70b5f1-8f53-47a4-be48-669ae78795e6"
}
]
}
7 changes: 3 additions & 4 deletions tests/fixtures/json/1.2/bom_setuptools.json
Original file line number Diff line number Diff line change
@@ -2,10 +2,10 @@
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.2",
"serialNumber": "urn:uuid:21a3711c-be49-4007-b4df-c90af6eb8725",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2023-01-07T13:45:59.330881+00:00",
"timestamp": "2023-01-07T13:44:32.312678+00:00",
"tools": [
{
"vendor": "CycloneDX",
@@ -31,8 +31,7 @@
],
"dependencies": [
{
"ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz",
"dependsOn": []
"ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz"
}
]
}
71 changes: 31 additions & 40 deletions tests/fixtures/json/1.2/bom_setuptools_complete.json
Original file line number Diff line number Diff line change
@@ -2,10 +2,10 @@
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.2",
"serialNumber": "urn:uuid:26201075-4b54-44d2-a6d3-bc90e55c9db7",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2023-01-07T13:45:57.623239+00:00",
"timestamp": "2023-01-07T13:44:32.312678+00:00",
"tools": [
{
"vendor": "CycloneDX",
@@ -24,14 +24,14 @@
"https://cyclonedx.org"
],
"contact": [
{
"name": "Paul Horton",
"email": "paul.horton@owasp.org"
},
{
"name": "A N Other",
"email": "someone@somewhere.tld",
"phone": "+44 (0)1234 567890"
},
{
"name": "Paul Horton",
"email": "paul.horton@owasp.org"
}
]
},
@@ -107,55 +107,55 @@
"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"
}
],
"purl": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"externalReferences": [
{
"type": "distribution",
"url": "https://cyclonedx.org",
"comment": "No comment",
"type": "distribution"
"comment": "No comment"
}
]
}
],
"variants": [
{
"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"
},
{
"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"
}
],
"purl": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"externalReferences": [
{
"type": "distribution",
"url": "https://cyclonedx.org",
"comment": "No comment",
"type": "distribution"
"comment": "No comment"
}
]
},
{
"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": [
@@ -173,9 +173,9 @@
},
"externalReferences": [
{
"type": "distribution",
"url": "https://cyclonedx.org",
"comment": "No comment",
"type": "distribution"
"comment": "No comment"
}
],
"components": [
@@ -197,18 +197,18 @@
"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"
}
],
"purl": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"externalReferences": [
{
"type": "distribution",
"url": "https://cyclonedx.org",
"comment": "No comment",
"type": "distribution"
"comment": "No comment"
}
]
}
@@ -217,16 +217,7 @@
],
"dependencies": [
{
"ref": "df70b5f1-8f53-47a4-be48-669ae78795e6",
"dependsOn": []
},
{
"ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz",
"dependsOn": []
},
{
"ref": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"dependsOn": []
"ref": "df70b5f1-8f53-47a4-be48-669ae78795e6"
}
]
}
37 changes: 37 additions & 0 deletions tests/fixtures/json/1.2/bom_setuptools_no_version.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:77d15ab9-5602-4cca-8ed2-59ae579aafd3",
"version": 1,
"metadata": {
"timestamp": "2023-01-07T13:44:32.312678+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "TESTING"
}
]
},
"components": [
{
"type": "library",
"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"
}
],
"dependencies": [
{
"ref": "pkg:pypi/setuptools?extension=tar.gz"
}
]
}
7 changes: 3 additions & 4 deletions tests/fixtures/json/1.2/bom_setuptools_with_cpe.json
Original file line number Diff line number Diff line change
@@ -2,10 +2,10 @@
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.2",
"serialNumber": "urn:uuid:65baf289-d8ad-4128-800d-b5292c938c3a",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2023-01-07T13:45:59.351895+00:00",
"timestamp": "2023-01-07T13:44:32.312678+00:00",
"tools": [
{
"vendor": "CycloneDX",
@@ -32,8 +32,7 @@
],
"dependencies": [
{
"ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz",
"dependsOn": []
"ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz"
}
]
}
7 changes: 3 additions & 4 deletions tests/fixtures/json/1.2/bom_toml_1.json
Original file line number Diff line number Diff line change
@@ -2,10 +2,10 @@
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.2",
"serialNumber": "urn:uuid:f60062dc-9bb8-4415-be36-78f0c52c5c64",
"serialNumber": "urn:uuid:6f266d1c-760f-4552-ae3b-41a9b74232fa",
"version": 1,
"metadata": {
"timestamp": "2023-01-07T13:45:57.552271+00:00",
"timestamp": "2023-01-07T13:44:32.312678+00:00",
"tools": [
{
"vendor": "CycloneDX",
@@ -38,8 +38,7 @@
],
"dependencies": [
{
"ref": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"dependsOn": []
"ref": "pkg:pypi/toml@0.10.2?extension=tar.gz"
}
]
}
7 changes: 3 additions & 4 deletions tests/fixtures/json/1.2/bom_with_full_metadata.json
Original file line number Diff line number Diff line change
@@ -2,10 +2,10 @@
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.2",
"serialNumber": "urn:uuid:47f912c6-4879-4f4b-ae9c-2cf51f15be08",
"serialNumber": "urn:uuid:d0b24ba4-102b-497e-b31f-4fdc3f0a3005",
"version": 1,
"metadata": {
"timestamp": "2023-01-07T13:45:57.775590+00:00",
"timestamp": "2023-01-07T13:44:32.312678+00:00",
"tools": [
{
"vendor": "CycloneDX",
@@ -256,8 +256,7 @@
},
"dependencies": [
{
"ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857",
"dependsOn": []
"ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857"
}
]
}
3 changes: 1 addition & 2 deletions tests/fixtures/json/1.3/bom_dependencies.json
Original file line number Diff line number Diff line change
@@ -63,8 +63,7 @@
]
},
{
"ref": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"dependsOn": []
"ref": "pkg:pypi/toml@0.10.2?extension=tar.gz"
}
]
}
3 changes: 1 addition & 2 deletions tests/fixtures/json/1.3/bom_dependencies_component.json
Original file line number Diff line number Diff line change
@@ -63,8 +63,7 @@
]
},
{
"ref": "pkg:pypi/toml@0.10.2?extension=tar.gz",
"dependsOn": []
"ref": "pkg:pypi/toml@0.10.2?extension=tar.gz"
}
]
}
4 changes: 2 additions & 2 deletions tests/fixtures/json/1.3/bom_external_references.json
Original file line number Diff line number Diff line change
@@ -2,10 +2,10 @@
"$schema": "http://cyclonedx.org/schema/bom-1.3a.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:00236b4e-8837-423e-8789-734863b0f78a",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2023-01-07T13:45:57.491496+00:00",
"timestamp": "2023-01-07T13:44:32.312678+00:00",
"tools": [
{
"vendor": "CycloneDX",
6 changes: 2 additions & 4 deletions tests/fixtures/json/1.3/bom_issue_275_components.json
Original file line number Diff line number Diff line change
@@ -51,8 +51,7 @@
]
},
{
"ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda",
"dependsOn": []
"ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda"
},
{
"ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857",
@@ -61,8 +60,7 @@
]
},
{
"ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
"dependsOn": []
"ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789"
}
]
}
15 changes: 10 additions & 5 deletions tests/fixtures/json/1.3/bom_services_complex.json
Original file line number Diff line number Diff line change
@@ -2,10 +2,10 @@
"$schema": "http://cyclonedx.org/schema/bom-1.3a.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:401351ba-6438-4b6d-8f6f-f10f1ae41f8b",
"serialNumber": "urn:uuid:d0b24ba4-102b-497e-b31f-4fdc3f0a3005",
"version": 1,
"metadata": {
"timestamp": "2023-01-07T13:45:58.031955+00:00",
"timestamp": "2023-01-07T13:44:32.312678+00:00",
"tools": [
{
"vendor": "CycloneDX",
@@ -15,7 +15,7 @@
],
"component": {
"type": "library",
"bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda",
"bom-ref": "bb5911d6-1a1d-41c9-b6e0-46e848d16655",
"name": "cyclonedx-python-lib",
"version": "1.0.0"
}
@@ -92,8 +92,13 @@
],
"dependencies": [
{
"ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda",
"dependsOn": []
"ref": "my-specific-bom-ref-for-my-first-service"
},
{
"ref": "be2c6502-7e9a-47db-9a66-e34f729810a3"
},
{
"ref": "bb5911d6-1a1d-41c9-b6e0-46e848d16655"
}
]
}
13 changes: 9 additions & 4 deletions tests/fixtures/json/1.3/bom_services_nested.json
Original file line number Diff line number Diff line change
@@ -2,10 +2,10 @@
"$schema": "http://cyclonedx.org/schema/bom-1.3a.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:6ae24228-acba-422e-bc82-450a230cf04d",
"serialNumber": "urn:uuid:d0b24ba4-102b-497e-b31f-4fdc3f0a3005",
"version": 1,
"metadata": {
"timestamp": "2023-01-07T13:45:58.058446+00:00",
"timestamp": "2023-01-07T13:44:32.312678+00:00",
"tools": [
{
"vendor": "CycloneDX",
@@ -152,8 +152,13 @@
],
"dependencies": [
{
"ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
"dependsOn": []
"ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789"
},
{
"ref": "my-specific-bom-ref-for-my-first-service"
},
{
"ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857"
}
]
}
15 changes: 10 additions & 5 deletions tests/fixtures/json/1.3/bom_services_simple.json
Original file line number Diff line number Diff line change
@@ -2,10 +2,10 @@
"$schema": "http://cyclonedx.org/schema/bom-1.3a.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:e3ad515c-a48a-44d8-a17f-ed11e6a68a1a",
"serialNumber": "urn:uuid:d0b24ba4-102b-497e-b31f-4fdc3f0a3005",
"version": 1,
"metadata": {
"timestamp": "2023-01-07T13:45:58.086656+00:00",
"timestamp": "2023-01-07T13:44:32.312678+00:00",
"tools": [
{
"vendor": "CycloneDX",
@@ -15,7 +15,7 @@
],
"component": {
"type": "library",
"bom-ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857",
"bom-ref": "df70b5f1-8f53-47a4-be48-669ae78795e6",
"name": "cyclonedx-python-lib",
"version": "1.0.0"
}
@@ -32,8 +32,13 @@
],
"dependencies": [
{
"ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857",
"dependsOn": []
"ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda"
},
{
"ref": "be2c6502-7e9a-47db-9a66-e34f729810a3"
},
{
"ref": "df70b5f1-8f53-47a4-be48-669ae78795e6"
}
]
}
7 changes: 3 additions & 4 deletions tests/fixtures/json/1.3/bom_setuptools.json
Original file line number Diff line number Diff line change
@@ -2,10 +2,10 @@
"$schema": "http://cyclonedx.org/schema/bom-1.3a.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:8d1060a2-afd0-4540-b145-9d936fe333e6",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2023-01-07T13:45:59.372749+00:00",
"timestamp": "2023-01-07T13:44:32.312678+00:00",
"tools": [
{
"vendor": "CycloneDX",
@@ -31,8 +31,7 @@
],
"dependencies": [
{
"ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz",
"dependsOn": []
"ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz"
}
]
}
Loading