CycloneDX is a lightweight software bill of materials (SBOM) standard that provides comprehensive inventory of all software components and dependencies. It's designed for use in application security contexts and software supply chain management.
What is CycloneDX?
CycloneDX is an OWASP standard for SBOM that creates a complete, machine-readable inventory of:
- Application components
- Libraries and dependencies
- Files and containers
- Services and APIs
- Vulnerabilities and license information
Key Features
- Lightweight: Minimal overhead and complexity
- Comprehensive: Supports components, services, vulnerabilities, and more
- Standardized: JSON and XML formats with proper schema validation
- Tooling Support: Rich ecosystem of tools and libraries
- Security-Focused: Designed for software supply chain security
Java Implementation with CycloneDX Core
Dependencies Setup
<!-- CycloneDX Maven Plugin --> <plugin> <groupId>org.cyclonedx</groupId> <artifactId>cyclonedx-maven-plugin</artifactId> <version>2.7.9</version> <configuration> <projectType>library</projectType> <schemaVersion>1.4</schemaVersion> <includeBomSerialNumber>true</includeBomSerialNumber> <includeCompileScope>true</includeCompileScope> <includeProvidedScope>true</includeProvidedScope> <includeRuntimeScope>true</includeRuntimeScope> <includeSystemScope>true</includeSystemScope> <includeTestScope>false</includeTestScope> <includeLicenseText>false</includeLicenseText> <outputFormat>json</outputFormat> <outputName>bom</outputName> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>makeAggregateBom</goal> </goals> </execution> </executions> </plugin> <!-- CycloneDX Core Library --> <dependency> <groupId>org.cyclonedx</groupId> <artifactId>cyclonedx-core-java</artifactId> <version>7.4.3</version> </dependency> <!-- For Maven project parsing --> <dependency> <groupId>org.cyclonedx</groupId> <artifactId>cyclonedx-maven-plugin</artifactId> <version>2.7.9</version> </dependency> <!-- For Gradle project parsing --> <dependency> <groupId>org.cyclonedx</groupId> <artifactId>cyclonedx-gradle-plugin</artifactId> <version>1.7.4</version> </dependency>
Basic BOM Generation
Example 1: Programmatic BOM Creation
package com.example.sbom;
import org.cyclonedx.BomGeneratorFactory;
import org.cyclonedx.CycloneDxSchema;
import org.cyclonedx.model.Bom;
import org.cyclonedx.model.Component;
import org.cyclonedx.model.ExternalReference;
import org.cyclonedx.model.License;
import org.cyclonedx.model.LicenseChoice;
import org.cyclonedx.model.Property;
import org.cyclonedx.generators.json.BomJsonGenerator;
import java.util.Arrays;
import java.util.UUID;
public class BasicBomCreator {
public Bom createBasicBom() {
// Create a new BOM instance
Bom bom = new Bom();
// Set BOM metadata
bom.setSerialNumber("urn:uuid:" + UUID.randomUUID().toString());
bom.setVersion(1);
// Create main application component
Component application = new Component();
application.setType(Component.Type.APPLICATION);
application.setName("My Spring Boot Application");
application.setVersion("1.0.0");
application.setGroup("com.example");
application.setDescription("A sample Spring Boot microservice");
application.setBomRef("my-app-1.0.0");
// Add properties
Property buildProperty = new Property();
buildProperty.setName("build-timestamp");
buildProperty.setValue(String.valueOf(System.currentTimeMillis()));
application.setProperties(Arrays.asList(buildProperty));
// Add external references
ExternalReference repoRef = new ExternalReference();
repoRef.setUrl("https://github.com/example/my-app");
repoRef.setType(ExternalReference.Type.VCS);
application.setExternalReferences(Arrays.asList(repoRef));
// Add component to BOM
bom.setComponents(Arrays.asList(application));
return bom;
}
public String generateBomJson(Bom bom) {
BomJsonGenerator generator = BomGeneratorFactory.createJson(CycloneDxSchema.Version.VERSION_14, bom);
return generator.toJsonString();
}
public static void main(String[] args) {
BasicBomCreator creator = new BasicBomCreator();
Bom bom = creator.createBasicBom();
String bomJson = creator.generateBomJson(bom);
System.out.println("Generated BOM:");
System.out.println(bomJson);
}
}
Example 2: BOM with Dependencies
package com.example.sbom;
import org.cyclonedx.model.Bom;
import org.cyclonedx.model.Component;
import org.cyclonedx.model.Dependency;
import org.cyclonedx.model.OrganizationalEntity;
import org.cyclonedx.model.OrganizationalContact;
import org.cyclonedx.model.Metadata;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
public class DependencyBomCreator {
public Bom createBomWithDependencies() {
Bom bom = new Bom();
bom.setSerialNumber("urn:uuid:" + UUID.randomUUID().toString());
bom.setVersion(1);
// Set metadata
Metadata metadata = new Metadata();
// Set component for the application itself
Component application = createComponent(
"application-server",
"Application Server",
"2.5.0",
Component.Type.APPLICATION,
"com.example"
);
metadata.setComponent(application);
// Set manufacturer/organization
OrganizationalEntity manufacturer = new OrganizationalEntity();
manufacturer.setName("Example Corp");
metadata.setManufacturer(manufacturer);
// Set authors
OrganizationalContact author = new OrganizationalContact();
author.setName("John Doe");
author.setEmail("[email protected]");
metadata.setAuthors(Arrays.asList(author));
bom.setMetadata(metadata);
// Create dependency components
List<Component> components = new ArrayList<>();
// Spring Boot Starter Web
Component springWeb = createComponent(
"spring-boot-starter-web",
"Spring Boot Starter Web",
"2.7.0",
Component.Type.LIBRARY,
"org.springframework.boot"
);
components.add(springWeb);
// Jackson Databind
Component jackson = createComponent(
"jackson-databind",
"Jackson DataBind",
"2.13.3",
Component.Type.LIBRARY,
"com.fasterxml.jackson.core"
);
components.add(jackson);
// Logback Classic
Component logback = createComponent(
"logback-classic",
"Logback Classic",
"1.2.11",
Component.Type.LIBRARY,
"ch.qos.logback"
);
components.add(logback);
bom.setComponents(components);
// Create dependency graph
List<Dependency> dependencies = new ArrayList<>();
// Application depends on all libraries
Dependency appDependency = new Dependency();
appDependency.setRef(application.getBomRef());
appDependency.setDependencies(Arrays.asList(
createDependencyRef(springWeb.getBomRef()),
createDependencyRef(jackson.getBomRef()),
createDependencyRef(logback.getBomRef())
));
dependencies.add(appDependency);
// Spring Web depends on Jackson
Dependency springDependency = new Dependency();
springDependency.setRef(springWeb.getBomRef());
springDependency.setDependencies(Arrays.asList(
createDependencyRef(jackson.getBomRef())
));
dependencies.add(springDependency);
bom.setDependencies(dependencies);
return bom;
}
private Component createComponent(String name, String description,
String version, Component.Type type, String group) {
Component component = new Component();
component.setName(name);
component.setDescription(description);
component.setVersion(version);
component.setType(type);
component.setGroup(group);
component.setBomRef(group + ":" + name + ":" + version);
return component;
}
private Dependency createDependencyRef(String ref) {
Dependency dependency = new Dependency();
dependency.setRef(ref);
return dependency;
}
}
Advanced BOM Features
Example 3: BOM with Vulnerabilities and Licenses
package com.example.sbom;
import org.cyclonedx.model.Bom;
import org.cyclonedx.model.Component;
import org.cyclonedx.model.License;
import org.cyclonedx.model.LicenseChoice;
import org.cyclonedx.model.Vulnerability;
import org.cyclonedx.model.VulnerabilityRating;
import org.cyclonedx.model.VulnerabilitySource;
import org.cyclonedx.model.Affect;
import org.cyclonedx.model.Rating;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Date;
public class SecurityBomCreator {
public Bom createBomWithSecurityData() {
Bom bom = new BasicBomCreator().createBasicBom();
// Add components with license information
Component log4j = createComponentWithLicense();
bom.setComponents(Arrays.asList(log4j));
// Add vulnerability information
Vulnerability vulnerability = createVulnerability();
bom.setVulnerabilities(Arrays.asList(vulnerability));
return bom;
}
private Component createComponentWithLicense() {
Component component = new Component();
component.setName("log4j-core");
component.setVersion("2.14.1");
component.setGroup("org.apache.logging.log4j");
component.setType(Component.Type.LIBRARY);
component.setBomRef("log4j-core-2.14.1");
// Add license information
License license = new License();
license.setId("Apache-2.0");
license.setName("Apache License 2.0");
LicenseChoice licenseChoice = new LicenseChoice();
licenseChoice.setLicenses(Arrays.asList(license));
component.setLicenses(Arrays.asList(licenseChoice));
// Add purl (Package URL)
component.setPurl("pkg:maven/org.apache.logging.log4j/[email protected]");
return component;
}
private Vulnerability createVulnerability() {
Vulnerability vulnerability = new Vulnerability();
vulnerability.setId("CVE-2021-44228");
vulnerability.setSource(createVulnerabilitySource());
vulnerability.setDescription("Apache Log4j2 2.0-beta9 through 2.15.0 " +
"excludes certain class names from JNDI lookups, which allows remote " +
"attackers to execute arbitrary code via a crafted request.");
// Set ratings
VulnerabilityRating rating = new VulnerabilityRating();
rating.setSource(createVulnerabilitySource());
rating.setScore(new BigDecimal("9.8"));
rating.setSeverity(Rating.Severity.CRITICAL);
rating.setMethod(Rating.Method.CVSSV3);
vulnerability.setRatings(Arrays.asList(rating));
// Set affected components
Affect affect = new Affect();
affect.setRef("log4j-core-2.14.1");
vulnerability.setAffects(Arrays.asList(affect));
// Set recommendations
vulnerability.setRecommendation("Upgrade to Log4j 2.17.0 or later");
return vulnerability;
}
private VulnerabilitySource createVulnerabilitySource() {
VulnerabilitySource source = new VulnerabilitySource();
source.setName("NVD");
source.setUrl("https://nvd.nist.gov/vuln/detail/CVE-2021-44228");
return source;
}
}
Maven Integration
Example 4: Maven Plugin Configuration
<!-- Complete CycloneDX Maven Plugin Configuration --> <plugin> <groupId>org.cyclonedx</groupId> <artifactId>cyclonedx-maven-plugin</artifactId> <version>2.7.9</version> <configuration> <projectType>library</projectType> <schemaVersion>1.4</schemaVersion> <includeBomSerialNumber>true</includeBomSerialNumber> <includeCompileScope>true</includeCompileScope> <includeProvidedScope>true</includeProvidedScope> <includeRuntimeScope>true</includeRuntimeScope> <includeSystemScope>true</includeSystemScope> <includeTestScope>false</includeTestScope> <includeLicenseText>false</includeLicenseText> <outputFormat>all</outputFormat> <outputName>bom</outputName> <includeDevDependencies>false</includeDevDependencies> <includePlugins>false</includePlugins> <includePluginDependencies>false</includePluginDependencies> </configuration> <executions> <execution> <id>make-bom</id> <phase>package</phase> <goals> <goal>makeAggregateBom</goal> </goals> </execution> </executions> </plugin>
Example 5: Custom Maven Mojo for BOM Generation
package com.example.maven;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.project.MavenProject;
import org.cyclonedx.model.Bom;
import org.cyclonedx.model.Component;
import org.cyclonedx.generators.json.BomJsonGenerator;
import org.cyclonedx.BomGeneratorFactory;
import org.cyclonedx.CycloneDxSchema;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Custom Maven Mojo for generating enhanced CycloneDX BOM
*/
@Mojo(name = "generate-enhanced-bom")
public class EnhancedBomMojo extends AbstractMojo {
@Parameter(defaultValue = "${project}", required = true, readonly = true)
private MavenProject project;
@Parameter(property = "outputDirectory", defaultValue = "${project.build.directory}")
private File outputDirectory;
@Parameter(property = "includeBuildInfo", defaultValue = "true")
private boolean includeBuildInfo;
public void execute() throws MojoExecutionException {
getLog().info("Generating enhanced CycloneDX BOM for: " + project.getName());
try {
Bom bom = createEnhancedBom();
writeBomToFile(bom);
getLog().info("BOM generated successfully");
} catch (Exception e) {
throw new MojoExecutionException("Failed to generate BOM", e);
}
}
private Bom createEnhancedBom() {
Bom bom = new Bom();
bom.setVersion(1);
// Create main component from Maven project
Component mainComponent = new Component();
mainComponent.setType(Component.Type.APPLICATION);
mainComponent.setGroup(project.getGroupId());
mainComponent.setName(project.getArtifactId());
mainComponent.setVersion(project.getVersion());
mainComponent.setDescription(project.getDescription());
mainComponent.setBomRef(createBomRef(project));
// Add build information if enabled
if (includeBuildInfo) {
addBuildProperties(mainComponent);
}
List<Component> components = new ArrayList<>();
components.add(mainComponent);
// TODO: Add dependencies from Maven project
// This would require parsing project dependencies
bom.setComponents(components);
return bom;
}
private String createBomRef(MavenProject project) {
return project.getGroupId() + ":" + project.getArtifactId() + ":" + project.getVersion();
}
private void addBuildProperties(Component component) {
// Add build timestamp, Java version, etc.
// This would be implemented based on build environment
}
private void writeBomToFile(Bom bom) throws IOException {
outputDirectory.mkdirs();
File bomFile = new File(outputDirectory, "enhanced-bom.json");
BomJsonGenerator generator = BomGeneratorFactory.createJson(
CycloneDxSchema.Version.VERSION_14, bom);
try (FileWriter writer = new FileWriter(bomFile)) {
writer.write(generator.toJsonString());
}
}
}
BOM Validation and Processing
Example 6: BOM Validation and Analysis
package com.example.sbom;
import org.cyclonedx.model.Bom;
import org.cyclonedx.model.Component;
import org.cyclonedx.parsers.JsonParser;
import org.cyclonedx.parsers.XmlParser;
import org.cyclonedx.exception.ParseException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class BomAnalyzer {
private final JsonParser jsonParser;
private final XmlParser xmlParser;
public BomAnalyzer() {
this.jsonParser = new JsonParser();
this.xmlParser = new XmlParser();
}
public Bom parseJsonBom(File bomFile) throws IOException, ParseException {
try (FileInputStream fis = new FileInputStream(bomFile)) {
return jsonParser.parse(fis);
}
}
public Bom parseXmlBom(File bomFile) throws IOException, ParseException {
try (FileInputStream fis = new FileInputStream(bomFile)) {
return xmlParser.parse(fis);
}
}
public void analyzeBom(Bom bom) {
System.out.println("=== BOM Analysis ===");
System.out.println("BOM Version: " + bom.getVersion());
System.out.println("Serial Number: " + bom.getSerialNumber());
System.out.println("Component Count: " +
(bom.getComponents() != null ? bom.getComponents().size() : 0));
if (bom.getComponents() != null) {
// Group components by type
Map<Component.Type, List<Component>> componentsByType =
bom.getComponents().stream()
.collect(Collectors.groupingBy(Component::getType));
componentsByType.forEach((type, components) -> {
System.out.println(type + " components: " + components.size());
});
// List unique groups/organizations
List<String> groups = bom.getComponents().stream()
.map(Component::getGroup)
.filter(group -> group != null && !group.isEmpty())
.distinct()
.collect(Collectors.toList());
System.out.println("Unique groups: " + groups);
}
if (bom.getVulnerabilities() != null) {
System.out.println("Vulnerabilities: " + bom.getVulnerabilities().size());
}
}
public List<Component> findComponentsByLicense(Bom bom, String licenseId) {
if (bom.getComponents() == null) {
return List.of();
}
return bom.getComponents().stream()
.filter(component -> hasLicense(component, licenseId))
.collect(Collectors.toList());
}
private boolean hasLicense(Component component, String licenseId) {
if (component.getLicenses() == null) {
return false;
}
return component.getLicenses().stream()
.anyMatch(licenseChoice ->
licenseChoice.getLicenses() != null &&
licenseChoice.getLicenses().stream()
.anyMatch(license ->
licenseId.equals(license.getId()) ||
licenseId.equals(license.getName())
)
);
}
}
Integration with CI/CD Pipeline
Example 7: BOM Generation in CI Pipeline
package com.example.ci;
import org.cyclonedx.model.Bom;
import org.cyclonedx.model.Component;
import org.cyclonedx.generators.json.BomJsonGenerator;
import org.cyclonedx.BomGeneratorFactory;
import org.cyclonedx.CycloneDxSchema;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.UUID;
public class CiBomGenerator {
public void generateCdBom(String buildId, String commitHash,
String branch, String buildUrl) {
Bom bom = new Bom();
bom.setSerialNumber("urn:uuid:" + UUID.randomUUID().toString());
bom.setVersion(1);
// Create component with CI/CD context
Component component = new Component();
component.setType(Component.Type.APPLICATION);
component.setName("My Microservice");
component.setVersion("1.0.0-" + buildId);
component.setGroup("com.example");
component.setBomRef("app-build-" + buildId);
// Add CI/CD properties
component.setProperties(Arrays.asList(
createProperty("ci-build-id", buildId),
createProperty("ci-commit-hash", commitHash),
createProperty("ci-branch", branch),
createProperty("ci-build-url", buildUrl),
createProperty("ci-timestamp", String.valueOf(System.currentTimeMillis()))
));
bom.setComponents(Arrays.asList(component));
// Generate and write BOM
String bomJson = generateBomJson(bom);
writeBomToFile(bomJson, "bom-" + buildId + ".json");
System.out.println("Generated CI/CD BOM for build: " + buildId);
}
private org.cyclonedx.model.Property createProperty(String name, String value) {
org.cyclonedx.model.Property property = new org.cyclonedx.model.Property();
property.setName(name);
property.setValue(value);
return property;
}
private String generateBomJson(Bom bom) {
BomJsonGenerator generator = BomGeneratorFactory.createJson(
CycloneDxSchema.Version.VERSION_14, bom);
return generator.toJsonString();
}
private void writeBomToFile(String bomJson, String filename) {
Path outputPath = Paths.get("target", filename);
try (FileWriter writer = new FileWriter(outputPath.toFile())) {
writer.write(bomJson);
} catch (IOException e) {
throw new RuntimeException("Failed to write BOM file", e);
}
}
}
Best Practices for CycloneDX in Java
- Automate BOM Generation: Integrate into build process
- Include All Dependencies: Ensure complete dependency tree
- Regular Updates: Keep BOM current with code changes
- Security Scanning: Use BOM for vulnerability analysis
- License Compliance: Track all component licenses
- Version Control: Store BOMs with release artifacts
Conclusion
CycloneDX provides a comprehensive standard for software bill of materials in Java applications. Key benefits include:
- Supply Chain Security: Complete visibility into dependencies
- License Compliance: Track and manage open source licenses
- Vulnerability Management: Identify and track security issues
- Standardization: Industry-standard format for tool integration
- Automation: Seamless integration with build tools and CI/CD
By implementing CycloneDX in your Java projects, you gain critical visibility into your software supply chain, enabling better security, compliance, and maintenance practices.