What Is CycloneDX?
CycloneDX is the international standard for Software Bill of Materials (SBOM), it helps provide a structure for your software so you can keep track of it. This can be libraries, sub-applications, firmware, even hardware is supported too (with caveats since it’s designed with software in mind).
The purpose of SBOMs was primarily due to the U.S. Governments Executive Order 14028, Improving the Nation’s Cybersecurity | NIST. Essentially trying to find a way to prevent major supply chain vulnerabilities from happening so often by creating SBOMs to manage and track of every single component in all of your software, so when it does in fact get a vulnerability. You’re aware and can push out the changes.
I can talk more about SBOMs some other time, but CycloneDX is the best standard that’s well maintained by the good folks of OWASP on CycloneDX Bill of Materials Standard | CycloneDX.
They actually make a lot of tools, and one of the most useful ones I’ve used is the cyclonedx-python-lib. A Python Library for building and consuming SBOMs. It models the spec as Python dataclasses and handles serialization to JSON or XML, but JSON is being pushed more nowadays.
Installation
# do your virtual environment or however you like to manage python
pip install cyclonedx-python-lib
# you can install extras too like: json-validation, xml-validation, validation
pip install 'cyclonedx-python-lib[validation]'Core Model
Bom
The top-level container, purpose is to hold things for other things like the top level component, what built the sbom itself, when it was made, and some other things too.
The spec is really good to read as well if you want to know where you should put things: CycloneDX v1.7 JSON Reference
from cyclonedx.model.bom import Bom
bom = Bom() # empty BOM, auto-generates serial_number, time generated
bom = Bom(
components=[...], # Iterable[Component]
metadata=..., # BomMetaData
dependencies=[...], # Iterable[Dependency]
vulnerabilities=[...], # Iterable[Vulnerability]
services=[...], # Iterable[Service]
properties=[...], # Iterable[Property]
external_references=[...], # Iterable[ExternalReference]
)All parameters are keyword-only and optional. The BOM auto-assigns a UUID serial number and version 1 as well as when the sbom was created.
Component
This is where you’ll put your main components to your software. Basically the meat of your application or firmware.
from cyclonedx.model.component import Component, ComponentType
from packageurl import PackageURL
comp = Component(
name="libelf", # required
type=ComponentType.LIBRARY, # required; see ComponentType below
version="0.8.13",
group="gnu", # like a Maven groupId or npm scope
description="ELF parsing library",
bom_ref="libelf-0.8.13", # stable ID for cross-referencing; auto-generated if omitted
purl=PackageURL("generic", None, "libelf", "0.8.13"),
hashes=[...], # Iterable[HashType]
licenses=[...], # Iterable[LicenseExpression | DisjunctiveLicense]
properties=[...], # Iterable[Property]
external_references=[...], # Iterable[ExternalReference]
components=[...], # nested sub-components
cpe="cpe:2.3:a:gnu:libelf:0.8.13:*:*:*:*:*:*:*",
supplier=..., # OrganizationalEntity
manufacturer=..., # OrganizationalEntity
authors=[...], # Iterable[OrganizationalContact]
scope=ComponentScope.REQUIRED,
)ComponentType - pick the one that matches what you’re describing:
| Enum Value | String | When to use |
|---|---|---|
ComponentType.APPLICATION | application | A runnable program |
ComponentType.LIBRARY | library | A shared/static library |
ComponentType.FILE | file | A raw file (e.g. an ELF binary, config) |
ComponentType.CONTAINER | container | Docker image or OCI artifact |
ComponentType.FIRMWARE | firmware | Embedded firmware |
ComponentType.FRAMEWORK | framework | Higher-level library frameworks |
ComponentType.OPERATING_SYSTEM | operating-system | OS distribution |
ComponentType.DEVICE | device | Hardware device |
| the only required parts of the Component are the name and type. |
BomMetaData
Describes the BOM itself, who made it, when, and under what lifecycle.
from cyclonedx.model.bom import BomMetaData
from cyclonedx.model.component import Component, ComponentType
from cyclonedx.model.contact import OrganizationalEntity, OrganizationalContact
from cyclonedx.model.lifecycle import PredefinedLifecycle, LifecyclePhase
from cyclonedx.model import XsUri
meta = BomMetaData(
component=Component( # the thing this BOM describes
name="my-scanner",
type=ComponentType.APPLICATION,
version="1.0.0",
),
supplier=OrganizationalEntity(
name="Acme Corp",
urls=[XsUri("https://acme.com")],
),
authors=[OrganizationalContact(name="Bay")],
lifecycles=[PredefinedLifecycle(phase=LifecyclePhase.BUILD)],
properties=[...],
)
bom = Bom(metadata=meta)LifecyclePhase values: DESIGN, PRE_BUILD, BUILD, POST_BUILD, OPERATIONS, DISCOVERY, DECOMMISSION
Property
I like to think of the Property as a list of things you can add on to a component when you don’t really see anything else fit. Very convenient if you have verbose SBOMs.
from cyclonedx.model import Property
prop = Property(name="compiler", value="gcc")
properties = [Property(name="elf:arch", value="x86_64"), Property(name="scanner:source_path", value="/usr/bin/ls")]
# note for it to be accepted it must be list of props, or properties in ComponentProperties can be attached to Bom, Component, Service, and BomMetaData.
HashType
You can hash the contents with this, and then store it in a component.
from cyclonedx.model import HashType, HashAlgorithm
h = HashType(alg=HashAlgorithm.SHA_256, content="deadbeef...")Supported algorithms: MD5, SHA_1, SHA_256, SHA_384, SHA_512, SHA3_256, SHA3_384, SHA3_512, BLAKE2B_256, BLAKE2B_384, BLAKE2B_512, BLAKE3
Example of hashing a binary:
import hashlib
from pathlib import Path
def sha256_of(path: str) -> str:
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
h = HashType(alg=HashAlgorithm.SHA_256, content=sha256_of("/usr/bin/ls"))ExternalReference
Links a component to external resources. (e.g. source repos, download URLs, advisories, etc. )
from cyclonedx.model import ExternalReference, ExternalReferenceType, XsUri
ref = ExternalReference(
type=ExternalReferenceType.VCS,
url=XsUri("https://github.com/acme/libelf"),
comment="upstream source",
)Common ExternalReferenceType values: VCS, ISSUE_TRACKER, WEBSITE, ADVISORIES, BOM, MAILING_LIST, SOCIAL, CHAT, DOCUMENTATION, SUPPORT, DISTRIBUTION, LICENSE, BUILD_META, BUILD_SYSTEM, SECURITY_CONTACT
Licenses
You’ll want to add licenses to components (including the top level one).
from cyclonedx.factory.license import LicenseFactory
lf = LicenseFactory()
# SPDX expression (Apache-2.0, MIT, etc. — or compound expressions)
expr = lf.make_with_expression("Apache-2.0 OR MIT")
# Single named license
lic = lf.make_from_string("MIT") # returns DisjunctiveLicense
lic = lf.make_from_string("Apache-2.0")
# Attach to component
comp = Component(
name="mylib",
type=ComponentType.LIBRARY,
licenses=[expr], # or [lic] for a single named license
)Import note: Import
LicenseFactoryfromcyclonedx.factory.license, not fromcyclonedx.model.bom— the latter re-export is deprecated and will warn.
Dependency Graph
The dependency graph lives on the Bom, not on individual components. You build it using Dependency objects that reference bom_ref values.
This can be confusing at first, but there will be components that depend on one another and you would need to be able to show that.
In the python it’s done like this:
from cyclonedx.model.dependency import Dependency
# define root, and the dependencies for that
root = Component(name="my-app", type=ComponentType.APPLICATION, bom_ref="my-app")
dep1 = Component(name="libelf", type=ComponentType.LIBRARY, bom_ref="libelf")
dep2 = Component(name="capstone", type=ComponentType.LIBRARY, bom_ref="capstone")
# my-app depends on libelf and capstone
dep_node = Dependency(
ref=root.bom_ref,
dependencies=[
Dependency(ref=dep1.bom_ref),
Dependency(ref=dep2.bom_ref),
]
)
bom = Bom(
components=[root, dep1, dep2],
dependencies=[dep_node],
)
# to visualize dependencies
# my-app
# |- libelf
# |- capstone
Key detail:
Dependency.dependenciestakesIterable[Dependency]
- you wrap each child’s
bom_refin anotherDependency(ref=...), not in aBomRefdirectly. - Using bare
BomRefobjects here will crash at serialization.
Building a BOM
Putting it all together, a small SBOM generator could look like this:
from cyclonedx.model.bom import Bom, BomMetaData
from cyclonedx.model.component import Component, ComponentType
from cyclonedx.model import Property, HashType, HashAlgorithm, ExternalReference, ExternalReferenceType, XsUri
from cyclonedx.model.dependency import Dependency
from cyclonedx.model.lifecycle import PredefinedLifecycle, LifecyclePhase
from cyclonedx.factory.license import LicenseFactory
from cyclonedx.output import make_outputter
from cyclonedx.schema import OutputFormat, SchemaVersion
from packageurl import PackageURL
import hashlib
from pathlib import Path
lf = LicenseFactory()
# top level component
scanner = Component(
name="my-scanner",
type=ComponentType.APPLICATION,
version="1.0.0",
bom_ref="my-scanner",
)
# defining a component
target = Component(
name="libfoo",
type=ComponentType.LIBRARY,
version="2.1.0",
bom_ref="libfoo-2.1.0",
purl=PackageURL("generic", "example", "libfoo", "2.1.0"),
hashes=[
HashType(
alg=HashAlgorithm.SHA_256,
content=hashlib.sha256(b"placeholder").hexdigest(),
)
],
licenses=[lf.make_from_string("MIT")],
properties=[Property(name="build:static", value="false")],
external_references=[
ExternalReference(
type=ExternalReferenceType.VCS,
url=XsUri("https://github.com/example/libfoo"),
)
],
)
# putting bom obj together
bom = Bom(
components=[target],
metadata=BomMetaData(
component=scanner,
lifecycles=[PredefinedLifecycle(phase=LifecyclePhase.BUILD)],
),
dependencies=[
Dependency(
ref=scanner.bom_ref,
dependencies=[Dependency(ref=target.bom_ref)],
)
],
)
# serializing output
# JSON
outputter = make_outputter(bom, OutputFormat.JSON, SchemaVersion.V1_6)
json_string = outputter.output_as_string()
# XML
outputter = make_outputter(bom, OutputFormat.XML, SchemaVersion.V1_6)
xml_string = outputter.output_as_string()
# Write directly to a file
with open("sbom.json", "w") as f:
outputter = make_outputter(bom, OutputFormat.JSON, SchemaVersion.V1_6)
outputter.output_to_file(filename="sbom.json", allow_overwrite=True)Available schema versions: V1_0 through V1_7. Use V1_6 or V1_7
make_outputtercallsbom.validate()internally before serializing.- If your dependency graph references
bom_refvalues that don’t exist as components, it will raise aMissingComponentForBomRefException.
Parsing an Existing BOM
From JSON
import json
from cyclonedx.model.bom import Bom
with open("sbom.json") as f:
data = json.load(f)
bom = Bom.from_json(data=data) # type: ignore[attr-defined]
for component in bom.components:
print(component.name, component.version)
for prop in component.properties:
print(f" {prop.name} = {prop.value}")From XML
import xml.etree.ElementTree as ET
from cyclonedx.model.bom import Bom
tree = ET.parse("sbom.xml")
root = tree.getroot()
bom = Bom.from_xml(data=root) # type: ignore[attr-defined]Common mistakes
Auto-generated bom-ref values are unstable
If you don’t set bom_ref explicitly, the library generates an internal identifier like BomRef.4540520638001949.6520260990005295. This changes between runs, which breaks diff-ability and cross-referencing.
Always set bom_ref to a stable string when you need deterministic output.
# Bad - changes every run
comp = Component(name="libelf", type=ComponentType.LIBRARY)
# Good
comp = Component(name="libelf", type=ComponentType.LIBRARY, bom_ref="libelf-0.8.13")Dependency.dependencies takes Dependency, not BomRef
The type hint in the constructor says Iterable[Dependency], not Iterable[BomRef]. Passing bare BomRef objects compiles fine but crashes during serialization with AttributeError: 'BomRef' object has no attribute 'ref'.
# Wrong — BomRef objects, not Dependency
Dependency(ref=root.bom_ref, dependencies=[dep1.bom_ref, dep2.bom_ref])
# Correct — wrap each in Dependency()
Dependency(ref=root.bom_ref, dependencies=[
Dependency(ref=dep1.bom_ref),
Dependency(ref=dep2.bom_ref),
])Bom.from_xml() wants an Element, not a string
# Wrong
bom = Bom.from_xml(data=xml_string)
# Correct
import xml.etree.ElementTree as ET
root_element = ET.fromstring(xml_string) # or ET.parse(file).getroot()
bom = Bom.from_xml(data=root_element)LicenseFactory import location
# Deprecated — will warn
from cyclonedx.model.bom import LicenseFactory
# Correct
from cyclonedx.factory.license import LicenseFactoryOlder schema versions silently drop fields
lifecycles, tags, and several other fields were introduced in later schema versions. If you serialize to V1_4 or earlier, those fields disappear without error. Stick to V1_5+ for full feature coverage, V1_6 and V1_7 for current best practice.
The dependency graph warning
If your BomMetaData.component (the root descriptor) has no registered dependencies in the graph, you’ll get:
UserWarning: The Component this BOM is describing ... has no defined dependencies which means the Dependency Graph is incomplete
This is a warning, not an error. Suppress it or resolve it by adding the root to your dependencies list.
Quick Reference
Imports Cheat Sheet
from cyclonedx.model.bom import Bom, BomMetaData
from cyclonedx.model.component import Component, ComponentType, ComponentScope
from cyclonedx.model.dependency import Dependency
from cyclonedx.model.license import LicenseExpression, DisjunctiveLicense
from cyclonedx.model.lifecycle import PredefinedLifecycle, LifecyclePhase
from cyclonedx.model.contact import OrganizationalEntity, OrganizationalContact
from cyclonedx.model import (
Property,
HashType, HashAlgorithm,
ExternalReference, ExternalReferenceType,
XsUri,
)
from cyclonedx.factory.license import LicenseFactory
from cyclonedx.output import make_outputter
from cyclonedx.schema import OutputFormat, SchemaVersion
from packageurl import PackageURLSerialization
# JSON string
make_outputter(bom, OutputFormat.JSON, SchemaVersion.V1_6).output_as_string()
# XML string
make_outputter(bom, OutputFormat.XML, SchemaVersion.V1_6).output_as_string()
# JSON round-trip
import json
bom2 = Bom.from_json(data=json.loads(json_string))
# XML round-trip
import xml.etree.ElementTree as ET
bom2 = Bom.from_xml(data=ET.fromstring(xml_string))