Generate an SBOM¶
dfetch report generates a CycloneDX SBOM listing every vendored
dependency with its URL, revision, and auto-detected license. Downstream
tools can use the SBOM to monitor for known vulnerabilities or enforce a
license policy across an organisation.
$ dfetch report -t sbom -o dfetch.cdx.json
Dfetch parses each project’s license at report time and can recognise common
license files with high accuracy. For every fetched project the licenses
field is always populated:
Identified — the SPDX identifier is recorded (e.g.
MIT,Apache-2.0), and the base64-encoded license text is embedded inlicenses[].textso downstream tooling can verify the text matches the declared identifier.File found, unclassifiable — a license-like file (
LICENSE,COPYING, …) was detected but its text could not be matched to a known SPDX identifier with sufficient confidence. The field is set to the SPDX expressionNOASSERTIONwithacknowledgementset toconcludedanddfetch:license:noassertion:reasonset toUNCLASSIFIABLE_LICENSE_TEXT.No license file present — no license-like file was found. The field is set to the SPDX expression
NOASSERTIONwithacknowledgementset toconcludedanddfetch:license:noassertion:reasonset toNO_LICENSE_FILE.
This ensures the licenses field is never silently omitted for scanned
projects, giving downstream compliance tooling actionable context regardless of
the detection outcome. Projects that have never been fetched are not scanned
and will not have license assertions in the SBOM output.
Example: A fetched archive with an unclassifiable license file gets NOASSERTION
Given an archive "SomeProject.tar.gz" with the files
| path |
| LICENSE |
And the manifest 'dfetch.yaml'
"""
manifest:
version: '0.0'
projects:
- name: SomeProject
url: some-remote-server/SomeProject.tar.gz
vcs: archive
"""
And all projects are updated
When I run "dfetch report -t sbom"
Then the 'report.cdx.json' json file includes
"""
{
"components": [
{
"name": "SomeProject",
"licenses": [
{
"acknowledgement": "concluded",
"expression": "NOASSERTION"
}
],
"properties": [
{
"name": "dfetch:license:finding",
"value": "License file(s) found (LICENSE) but could not be classified"
},
{
"name": "dfetch:license:noassertion:reason",
"value": "UNCLASSIFIABLE_LICENSE_TEXT"
},
{
"name": "dfetch:license:threshold",
"value": "0.80"
},
{
"name": "dfetch:license:tool",
"value": "<infer-license-version>"
}
],
"evidence": {
"licenses": [
{
"acknowledgement": "concluded",
"expression": "NOASSERTION"
}
]
}
}
]
}
"""
Example: A fetched archive with no license file gets NOASSERTION
Given an archive "SomeProject.tar.gz" with the files
| path |
| README.md |
And the manifest 'dfetch.yaml'
"""
manifest:
version: '0.0'
projects:
- name: SomeProject
url: some-remote-server/SomeProject.tar.gz
vcs: archive
"""
And all projects are updated
When I run "dfetch report -t sbom"
Then the 'report.cdx.json' json file includes
"""
{
"components": [
{
"name": "SomeProject",
"licenses": [
{
"acknowledgement": "concluded",
"expression": "NOASSERTION"
}
],
"properties": [
{
"name": "dfetch:license:finding",
"value": "No license file found in source tree"
},
{
"name": "dfetch:license:noassertion:reason",
"value": "NO_LICENSE_FILE"
},
{
"name": "dfetch:license:threshold",
"value": "0.80"
},
{
"name": "dfetch:license:tool",
"value": "<infer-license-version>"
}
],
"evidence": {
"licenses": [
{
"acknowledgement": "concluded",
"expression": "NOASSERTION"
}
]
}
}
]
}
"""
License detection auditability¶
For every scanned component dfetch records properties that let auditors reproduce or re-evaluate results without re-running dfetch:
dfetch:license:<license-label>:confidenceThe probability score (0-1) returned by infer-license for each successfully identified license. The label is typically the SPDX ID, but may fall back to another detected license label when SPDX is unavailable.
dfetch:license:thresholdThe minimum confidence required to accept an inference (
0.80by default). If the threshold changes, stored scores can be compared against the new value without re-fetching.dfetch:license:toolThe infer-license library version used during the scan. Different library versions may classify the same text differently; recording the version enables reproducible re-evaluation.
For components with NOASSERTION licenses two additional properties provide
machine-readable context:
dfetch:license:noassertion:reasonNO_LICENSE_FILEorUNCLASSIFIABLE_LICENSE_TEXT, indicating why the license could not be asserted.dfetch:license:findingA human-readable description of the detection outcome — for example, “License file(s) found (LICENSE) but could not be classified” or “No license file found in source tree”. Useful for quick triage in dashboards that surface custom component properties.
Archive dependencies (tar.gz, zip, …) are recorded with a
distribution external reference. When an integrity.hash: field is set
in the manifest, the SBOM includes a SHA-256 component hash for
supply-chain integrity verification.
Example: A fetched project generates a json sbom
Given the manifest 'dfetch.yaml'
"""
manifest:
version: '0.0'
projects:
- name: cpputest
url: https://github.com/cpputest/cpputest
tag: v3.4
src: 'include/CppUTest'
"""
And all projects are updated
When I run "dfetch report -t sbom"
Then the 'report.cdx.json' json file includes
"""
{
"components": [
{
"bom-ref": "cpputest-v3.4",
"evidence": {
"identity": [
{
"concludedValue": "cpputest",
"field": "name",
"methods": [
{
"confidence": 0.4,
"technique": "manifest-analysis",
"value": "Name as used for project in dfetch.yaml"
}
]
},
{
"concludedValue": "pkg:github/cpputest/cpputest@v3.4#include/CppUTest",
"field": "purl",
"methods": [
{
"confidence": 0.4,
"technique": "manifest-analysis",
"value": "Determined from https://github.com/cpputest/cpputest as used for the project cpputest in dfetch.yaml"
}
]
},
{
"concludedValue": "v3.4",
"field": "version",
"methods": [
{
"confidence": 0.4,
"technique": "manifest-analysis",
"value": "Version as used for project in dfetch.yaml"
}
]
}
],
"licenses": [
{
"license": {
"id": "BSD-3-Clause"
}
}
],
"occurrences": [
{
"line": 5,
"location": "dfetch.yaml",
"offset": 13
}
]
},
"externalReferences": [
{
"type": "vcs",
"url": "https://github.com/cpputest/cpputest"
}
],
"group": "cpputest",
"licenses": [
{
"license": {
"id": "BSD-3-Clause"
}
}
],
"name": "cpputest",
"purl": "pkg:github/cpputest/cpputest@v3.4#include/CppUTest",
"properties": [
{
"name": "dfetch:license:BSD-3-Clause:confidence"
},
{
"name": "dfetch:license:threshold",
"value": "0.80"
},
{
"name": "dfetch:license:tool",
"value": "<infer-license-version>"
}
],
"type": "library",
"version": "v3.4"
}
],
"dependencies": [
{
"ref": "cpputest-v3.4"
}
],
"metadata": {
"timestamp": "2025-10-10T18:28:32.074803+00:00",
"tools": {
"components": [
{
"bom-ref": "dfetch-0.13.0",
"externalReferences": [
{
"type": "build-system",
"url": "https://github.com/dfetch-org/dfetch/actions"
},
{
"type": "distribution",
"url": "https://pypi.org/project/dfetch/"
},
{
"type": "documentation",
"url": "https://dfetch.readthedocs.io/"
},
{
"type": "issue-tracker",
"url": "https://github.com/dfetch-org/dfetch/issues"
},
{
"type": "license",
"url": "https://github.com/dfetch-org/dfetch/blob/main/LICENSE"
},
{
"type": "release-notes",
"url": "https://github.com/dfetch-org/dfetch/blob/main/CHANGELOG.rst"
},
{
"type": "vcs",
"url": "https://github.com/dfetch-org/dfetch"
},
{
"type": "website",
"url": "https://dfetch-org.github.io/"
}
],
"licenses": [
{
"license": {
"acknowledgement": "declared",
"id": "MIT"
}
}
],
"name": "dfetch",
"supplier": {
"name": "dfetch-org"
},
"type": "application",
"version": "0.13.0"
},
{
"description": "Python library for CycloneDX",
"externalReferences": [
{
"type": "build-system",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions"
},
{
"type": "distribution",
"url": "https://pypi.org/project/cyclonedx-python-lib/"
},
{
"type": "documentation",
"url": "https://cyclonedx-python-library.readthedocs.io/"
},
{
"type": "issue-tracker",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues"
},
{
"type": "license",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE"
},
{
"type": "release-notes",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md"
},
{
"type": "vcs",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib"
},
{
"type": "website",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/#readme"
}
],
"group": "CycloneDX",
"licenses": [
{
"license": {
"acknowledgement": "declared",
"id": "Apache-2.0"
}
}
],
"name": "cyclonedx-python-lib",
"type": "library",
"version": "11.7.0"
}
]
}
},
"serialNumber": "urn:uuid:7621038e-3047-4862-99e7-d637ee9458a9",
"version": 1,
"$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.6"
}
"""
Example: A fetched archive without a hash generates a json sbom
Given an archive "SomeProject.tar.gz"
And the manifest 'dfetch.yaml'
"""
manifest:
version: '0.0'
projects:
- name: SomeProject
url: some-remote-server/SomeProject.tar.gz
vcs: archive
"""
And all projects are updated
When I run "dfetch report -t sbom"
Then the 'report.cdx.json' json file includes
"""
{
"components": [
{
"name": "SomeProject",
"type": "library",
"externalReferences": [
{
"type": "distribution",
"url": "<archive-url>"
}
]
}
]
}
"""
Example: A fetched archive with sha256 hash generates a json sbom with hash
Given an archive "SomeProject.tar.gz"
And the manifest 'dfetch.yaml'
"""
manifest:
version: '0.0'
projects:
- name: SomeProject
url: some-remote-server/SomeProject.tar.gz
vcs: archive
integrity:
hash: sha256:<archive-sha256>
"""
And all projects are updated
When I run "dfetch report -t sbom"
Then the 'report.cdx.json' json file includes
"""
{
"components": [
{
"name": "SomeProject",
"version": "sha256:<archive-sha256>",
"type": "library",
"hashes": [
{
"alg": "SHA-256",
"content": "<archive-sha256>"
}
],
"externalReferences": [
{
"type": "distribution",
"url": "<archive-url>"
}
]
}
]
}
"""
Viewing the SBOM in DependencyTrack¶
DependencyTrack is a popular open-source SBOM analysis platform that ingests CycloneDX SBOMs generated by dfetch.
NOASSERTION is the SPDX value for a component whose license cannot be
determined. Dfetch uses it when no license file is found or when the file
text cannot be matched to a known SPDX identifier. See the
SPDX specification for the full definition.
When viewing a component with a NOASSERTION license in DependencyTrack:
The license column shows
NOASSERTION.The properties panel displays the dfetch license detection metadata (
dfetch:license:noassertion:reason,dfetch:license:finding, etc.).The license detail view is empty — DependencyTrack has no detail to show for a non-specific SPDX expression. The raw CycloneDX payload carries
acknowledgementand thedfetch:license:findingproperty for human-readable context.
GitLab¶
Upload the SBOM as a CycloneDX artifact so GitLab surfaces it in the dependency scanning dashboard. See GitLab dependency scanning for details.
dfetch:
image: "python:3.13"
script:
- pip install dfetch
- dfetch report -t sbom -o dfetch.cdx.json
artifacts:
reports:
cyclonedx:
- dfetch.cdx.json
GitHub Actions¶
Generate and upload the SBOM as a workflow artifact. See GitHub dependency submission for details.
jobs:
SBOM-generation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Generate SBOM
run: pip install dfetch && dfetch report -t sbom -o dfetch.cdx.json
- uses: actions/upload-artifact@v4
with:
name: sbom
path: dfetch.cdx.json