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 Development #262

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# EditorConfig is awesome: https://EditorConfig.org

root = true

[*]
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
end_of_line = lf

[*.py]
indent_style = space
indent_size = 4

[*.{yml,yaml}]
indent_style = space
indent_size = 2

[*.toml]
indent_style = space
indent_size = 2

[*.md]
charset = latin1
indent_style = space
indent_size = 2
# 2 trailing spaces indicate line breaks.
trim_trailing_whitespace = false

[*.{rst,txt}]
indent_style = space
indent_size = 4

[*.ini]
charset = latin1
indent_style = space
indent_size = 4
30 changes: 22 additions & 8 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ name: Deploy to PyPI

on:
push:
branches:
- main
branches: [ 'main' ]
workflow_dispatch:

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

jobs:
release:
Expand All @@ -19,22 +22,33 @@ 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: 3.9
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
with:
version: ${{ env.POETRY_VERSION }}
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true

- name: Install dependencies
run: |
python -m pip install poetry --upgrade pip
poetry config virtualenvs.create false
poetry install
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.29.5
uses: relekang/python-semantic-release@v7.31.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
repository_username: __token__
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/manual-release-candidate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/poetry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:
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:
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
66 changes: 57 additions & 9 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.

import warnings
from datetime import datetime, timezone
from typing import Iterable, Optional, Set
Expand All @@ -26,8 +27,10 @@
from ..exception.model import UnknownComponentDependencyException
from ..parser import BaseParser
from . import ExternalReference, LicenseChoice, OrganizationalContact, OrganizationalEntity, Property, ThisTool, Tool
from .bom_ref import BomRef
from .component import Component
from .service import Service
from .vulnerability import Vulnerability


class BomMetaData:
Expand Down Expand Up @@ -230,18 +233,21 @@ def from_parser(parser: BaseParser) -> 'Bom':

def __init__(self, *, components: Optional[Iterable[Component]] = None,
services: Optional[Iterable[Service]] = None,
external_references: Optional[Iterable[ExternalReference]] = None) -> None:
external_references: Optional[Iterable[ExternalReference]] = None,
serial_number: Optional[UUID] = None, version: int = 1) -> None:
"""
Create a new Bom that you can manually/programmatically add data to later.

Returns:
New, empty `cyclonedx.model.bom.Bom` instance.
"""
self.uuid = uuid4()
self.uuid = serial_number or uuid4()
self.metadata = BomMetaData()
self.components = components or [] # type: ignore
self.services = services or [] # type: ignore
self.external_references = external_references or [] # type: ignore
self.vulnerabilities = SortedSet()
self.version = version

@property
def uuid(self) -> UUID:
Expand Down Expand Up @@ -313,7 +319,7 @@ def get_urn_uuid(self) -> str:
Returns:
URN formatted UUID that uniquely identified this Bom instance.
"""
return 'urn:uuid:{}'.format(self.__uuid)
return self.__uuid.urn

def has_component(self, component: Component) -> bool:
"""
Expand Down Expand Up @@ -366,15 +372,57 @@ def _get_all_components(self) -> Set[Component]:

return components

def get_vulnerabilities_for_bom_ref(self, bom_ref: BomRef) -> "SortedSet[Vulnerability]":
"""
Get all known Vulnerabilities that affect the supplied bom_ref.

Args:
bom_ref: `BomRef`

Returns:
`SortedSet` of `Vulnerability`
"""

vulnerabilities: SortedSet[Vulnerability] = SortedSet()
for v in self.vulnerabilities:
for target in v.affects:
if target.ref == bom_ref.value:
vulnerabilities.add(v)
return vulnerabilities

def has_vulnerabilities(self) -> bool:
"""
Check whether this Bom has any declared vulnerabilities.

Returns:
`bool` - `True` if at least one `cyclonedx.model.component.Component` has at least one Vulnerability,
`False` otherwise.
`bool` - `True` if this Bom has at least one Vulnerability, `False` otherwise.
"""
return bool(self.vulnerabilities)

@property
def vulnerabilities(self) -> "SortedSet[Vulnerability]":
"""
Get all the Vulnerabilities in this BOM.

Returns:
Set of `Vulnerability`
"""
return any(c.has_vulnerabilities() for c in self.components)
return self._vulnerabilities

@vulnerabilities.setter
def vulnerabilities(self, vulnerabilities: Iterable[Vulnerability]) -> None:
self._vulnerabilities = SortedSet(vulnerabilities)

@property
def version(self) -> int:
return self._version

@version.setter
def version(self, version: int) -> None:
self._version = version

def urn(self) -> str:
return f'urn:cdx:{self.uuid}/{self.version}'

def validate(self) -> bool:
"""
Expand All @@ -399,9 +447,9 @@ def validate(self) -> bool:
# 2. Dependencies should exist for the Component this BOM is describing, if one is set
if self.metadata.component and not self.metadata.component.dependencies:
warnings.warn(
f'The Component this BOM is describing (PURL={self.metadata.component.purl}) has no defined '
f'dependencies which means the Dependency Graph is incomplete - you should add direct dependencies to '
f'this Component to complete the Dependency Graph data.',
f'The Component this BOM is describing {self.metadata.component.purl} has no defined dependencies '
f'which means the Dependency Graph is incomplete - you should add direct dependencies to this Component'
f'to complete the Dependency Graph data.',
UserWarning
)

Expand Down
2 changes: 1 addition & 1 deletion cyclonedx/model/bom_ref.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
33 changes: 0 additions & 33 deletions cyclonedx/model/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
from .bom_ref import BomRef
from .issue import IssueType
from .release_note import ReleaseNotes
from .vulnerability import Vulnerability


class Commit:
Expand Down Expand Up @@ -763,7 +762,6 @@ def __init__(self, *, name: str, component_type: ComponentType = ComponentType.L
self.licenses = [LicenseChoice(license_expression=license_str)] # type: ignore

self.__dependencies: "SortedSet[BomRef]" = SortedSet()
self.__vulnerabilites: "SortedSet[Vulnerability]" = SortedSet()

@property
def type(self) -> ComponentType:
Expand Down Expand Up @@ -1128,37 +1126,6 @@ def dependencies(self) -> "SortedSet[BomRef]":
def dependencies(self, dependencies: Iterable[BomRef]) -> None:
self.__dependencies = SortedSet(dependencies)

def add_vulnerability(self, vulnerability: Vulnerability) -> None:
"""
Add a Vulnerability to this Component.

Args:
vulnerability:
`cyclonedx.model.vulnerability.Vulnerability` instance to add to this Component.

Returns:
None
"""
self.__vulnerabilites.add(vulnerability)

def get_vulnerabilities(self) -> "SortedSet[Vulnerability]":
"""
Get all the Vulnerabilities for this Component.

Returns:
Set of `Vulnerability`
"""
return self.__vulnerabilites

def has_vulnerabilities(self) -> bool:
"""
Does this Component have any vulnerabilities?

Returns:
`True` if this Component has 1 or more vulnerabilities, `False` otherwise.
"""
return bool(self.get_vulnerabilities())

def get_all_nested_components(self, include_self: bool = False) -> Set["Component"]:
components = set()
if include_self:
Expand Down
15 changes: 4 additions & 11 deletions cyclonedx/output/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,6 @@ def generate(self, force_regeneration: bool = False) -> None:
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})
Expand Down Expand Up @@ -136,6 +125,10 @@ def _specialise_output_for_schema_version(self, bom_json: Dict[Any, Any]) -> str
and 'hashes' in bom_json['externalReferences'][i].keys():
del bom_json['externalReferences'][i]['hashes']

# Remove Vulnerabilities if not supported
if not self.bom_supports_vulnerabilities() and 'vulnerabilities' in bom_json.keys():
del bom_json['vulnerabilities']

return json.dumps(bom_json)

def output_as_string(self) -> str:
Expand Down
Loading