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 ValueStringWhen to use
ComponentType.APPLICATIONapplicationA runnable program
ComponentType.LIBRARYlibraryA shared/static library
ComponentType.FILEfileA raw file (e.g. an ELF binary, config)
ComponentType.CONTAINERcontainerDocker image or OCI artifact
ComponentType.FIRMWAREfirmwareEmbedded firmware
ComponentType.FRAMEWORKframeworkHigher-level library frameworks
ComponentType.OPERATING_SYSTEMoperating-systemOS distribution
ComponentType.DEVICEdeviceHardware 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 Component

Properties 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 LicenseFactory from cyclonedx.factory.license, not from cyclonedx.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.dependencies takes Iterable[Dependency]
  • you wrap each child’s bom_ref in another Dependency(ref=...), not in a BomRef directly.
  • Using bare BomRef objects 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_outputter calls bom.validate() internally before serializing.
  • If your dependency graph references bom_ref values that don’t exist as components, it will raise a MissingComponentForBomRefException.

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 LicenseFactory

Older 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 PackageURL

Serialization

# 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))