Skip to content
Merged
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
1 change: 1 addition & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"lextudio.restructuredtext",
"alexkrechik.cucumberautocomplete",
"ms-python.python",
"ms-python.pylint",
"ms-python.isort",
"ms-python.black-formatter",
"ms-python.debugpy",
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
- run: dfetch check
- run: dfetch update
- run: dfetch update
- run: dfetch report -t sbom
- name: Dfetch SARIF Check
uses: ./
with:
Expand Down Expand Up @@ -109,6 +110,8 @@ jobs:
- run: dfetch check
- run: dfetch update
- run: dfetch update
- run: dfetch report -t sbom

- name: Dfetch SARIF Check
uses: ./
with:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Release 0.11.0 (unreleased)
* Don't show animation when running in CI (#702)
* Improve logic for creating Purls in SBoM (#780)
* Add External VCS reference to SBoM if possible (#780)
* Use CycloneDX schema version 1.6 (#542)
* Add security policy (#784)
* Add provenance / release attestation to pypi package (#784)

Expand Down
2 changes: 1 addition & 1 deletion dfetch.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@
"extensions": {
"recommendations": [
"alexkrechik.cucumberautocomplete",
"bungcip.better-toml",
"tamasfe.even-better-toml",
"jebbs.plantuml",
"lextudio.restructuredtext",
"ms-python.black-formatter",
Expand Down
40 changes: 19 additions & 21 deletions dfetch/reporting/sbom_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,24 @@
An fetched project generates an sbom
"""

import json
from typing import cast

from cyclonedx.model import (
ExternalReference,
ExternalReferenceType,
LicenseChoice,
Tool,
XsUri,
)
from cyclonedx.builder.this import this_component as cdx_lib_component
from cyclonedx.model import ExternalReference, ExternalReferenceType, XsUri
from cyclonedx.model.bom import Bom
from cyclonedx.model.component import Component, ComponentType
from cyclonedx.output import get_instance
from cyclonedx.output.json import Json
from cyclonedx.schema import OutputFormat
from cyclonedx.model.license import LicenseExpression
from cyclonedx.model.tool import Tool
from cyclonedx.output import make_outputter
from cyclonedx.schema import OutputFormat, SchemaVersion

import dfetch.util.purl
from dfetch.manifest.project import ProjectEntry
from dfetch.reporting.reporter import Reporter

# PyRight is pedantic with decorators see https://github.com/madpah/serializable/issues/8
# It might be fixable with https://github.com/microsoft/pyright/discussions/4426, would prefer
# upstream fix, for now suppress, mypy will keep us safe.
# pyright: reportCallIssue=false, reportAttributeAccessIssue=false


class SbomReporter(Reporter):
"""Reporter for generating SBoM's."""
Expand All @@ -46,7 +44,8 @@ class SbomReporter(Reporter):
def __init__(self) -> None:
"""Start the report."""
self._bom = Bom()
self._bom.metadata.tools.add(self.dfetch_tool)
self._bom.metadata.tools.tools.add(self.dfetch_tool)
self._bom.metadata.tools.components.add(cdx_lib_component())

def add_project(
self, project: ProjectEntry, license_name: str, version: str
Expand Down Expand Up @@ -91,21 +90,20 @@ def add_project(
)

if license_name:
component.licenses.add(LicenseChoice(expression=license_name))
component.licenses.add(LicenseExpression(license_name))
self._bom.components.add(component)

def dump_to_file(self, outfile: str) -> bool:
"""Dump the SBoM to file."""
output_format = OutputFormat(
OutputFormat.XML if outfile.endswith(".xml") else OutputFormat.JSON
)
outputter = cast(Json, get_instance(bom=self._bom, output_format=output_format))

parsed = json.loads(outputter.output_as_string())
outputter._json_output = ( # pylint: disable=protected-access # type: ignore
json.dumps(parsed, indent=4)
outputter = make_outputter(
bom=self._bom,
output_format=output_format,
schema_version=SchemaVersion.V1_6,
)

outputter.output_to_file(outfile, allow_overwrite=True)
outputter.output_to_file(outfile, allow_overwrite=True, indent=4)

return True
22 changes: 11 additions & 11 deletions features/report-sbom.feature
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Feature: Create an CycloneDX sbom
The generated SBOM can be used as input for other tools to monitor dependencies.
The tools track vulnerabilities or can enforce a license policy within an organization.

Scenario: An fetched project generates an sbom
Scenario: An fetched project generates a json sbom
Given the manifest 'dfetch.yaml'
"""
manifest:
Expand All @@ -25,19 +25,19 @@ Feature: Create an CycloneDX sbom
When I run "dfetch report -t sbom"
Then the 'report.json' file contains
"""
{
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
{
"$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:e989dc42-a199-4fe4-87f1-2b7f7a5f48cf",
"specVersion": "1.6",
"serialNumber": "urn:uuid:3ce78767-c202-4beb-935e-67f539cf3a58",
"version": 1,
"dependencies": [
{
"ref": "a3aff0d8-2f40-4482-bded-577466c0bde9"
"ref": "BomRef.7805091949677974.3172811758515278"
}
],
"metadata": {
"timestamp": "2023-03-25T19:15:03.697694+00:00",
"timestamp": "2025-10-03T20:56:03.645362+00:00",
"tools": [
{
"vendor": "dfetch-org",
Expand All @@ -47,14 +47,14 @@ Feature: Create an CycloneDX sbom
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "4.2.2",
"version": "11.1.0",
"externalReferences": [
{
"url": "https://pypi.org/project/cyclonedx-python-lib/",
"type": "distribution"
},
{
"url": "https://cyclonedx.org",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/#readme",
"type": "website"
},
{
Expand All @@ -70,7 +70,7 @@ Feature: Create an CycloneDX sbom
"type": "release-notes"
},
{
"url": "https://cyclonedx.github.io/cyclonedx-python-lib/",
"url": "https://cyclonedx-python-library.readthedocs.io/",
"type": "documentation"
},
{
Expand All @@ -88,7 +88,7 @@ Feature: Create an CycloneDX sbom
"components": [
{
"type": "library",
"bom-ref": "a3aff0d8-2f40-4482-bded-577466c0bde9",
"bom-ref": "BomRef.7805091949677974.3172811758515278",
"name": "cpputest",
"version": "v3.4",
"externalReferences": [
Expand Down
3 changes: 3 additions & 0 deletions features/steps/generic_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
git_hash = re.compile(r"(\s?)[a-f0-9]{40}(\s?)")
iso_timestamp = re.compile(r'"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{6}\+\d{2}:\d{2}')
urn_uuid = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
bom_ref = re.compile(r"BomRef\.[0-9]+\.[0-9]+")
svn_error = re.compile(r"svn: E\d{6}: .+")


Expand Down Expand Up @@ -101,6 +102,7 @@ def check_content(
(git_hash, r"\1[commit hash]\2"),
(iso_timestamp, "[timestamp]"),
(urn_uuid, "[urn-uuid]"),
(bom_ref, "[bom-ref]"),
],
text=expected,
)
Expand All @@ -110,6 +112,7 @@ def check_content(
(git_hash, r"\1[commit hash]\2"),
(iso_timestamp, "[timestamp]"),
(urn_uuid, "[urn-uuid]"),
(bom_ref, "[bom-ref]"),
],
text=actual,
)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ dependencies = [
"sarif-om==1.0.4",
"semver==3.0.4",
"patch-ng==1.18.1",
"cyclonedx-python-lib==4.2.2",
"cyclonedx-python-lib==11.1.0",
"infer-license==0.1.0; python_version <= '3.10.0'",
"infer-license==0.2.0; python_version > '3.10.0'",
'setuptools; python_version >= "3.12"', # contains 'pkg_resources' for infer-license
Expand Down
Loading