The CycloneDX Gradle plugin generates CycloneDX Software Bill of Materials (SBOM) documents from Gradle's resolved dependency graphs. It records the components and relationships Gradle selected after conflict resolution, substitution, constraints, and transitive dependency resolution.
Apply the plugin to each project that should be described. Every such project gets:
- a Direct SBOM of that project, from
cyclonedxDirectBom; and - a
cyclonedxBomtask that produces an Aggregate SBOM of the projects it explicitly declares as members.
The plugin writes both JSON and XML by default and supports configuration cache, parallel execution, Gradle up-to-date checks, the build cache, and Gradle's Project Isolation.
Important
Plugin 4.0 replaced root-driven aggregation with explicit membership. Applying the plugin only to the root project no longer generates SBOMs for subprojects or includes them in the Aggregate SBOM. See the migration guide.
Note
This README documents the code on the current branch. For an installed release, use the README from that release's Git tag.
- Requirements
- Quick start
- Choose an SBOM
- Aggregate a multi-project build
- Configure the tasks
- Configuration reference
- Advanced recipes
- Using SBOMs with SLSA provenance
- Compatibility history
- Migrating from 3.x
- Community and contributing
- License
| Requirement | Plugin 4.x |
|---|---|
| Gradle | 8.4 or newer |
| Build JVM | Java 8 or newer; support for versions before Java 17 is deprecated |
| CycloneDX schema | 1.6 by default; 1.7 can be selected with schemaVersion |
| Output formats | JSON and XML |
The JVM that can run a particular build also depends on the Gradle Java compatibility matrix. The plugin is tested on Java 8, 11, 17, 21, and 25.
Apply the plugin to the project. The version shown here matches the version declared by this branch.
Kotlin DSL (build.gradle.kts):
plugins {
id("org.cyclonedx.bom") version "4.0.0"
}Groovy DSL (build.gradle):
plugins {
id 'org.cyclonedx.bom' version '4.0.0'
}Generate the project's Direct SBOM:
./gradlew cyclonedxDirectBomWith the default configuration, the outputs are:
build/reports/cyclonedx-direct/bom.json
build/reports/cyclonedx-direct/bom.xml
To produce an Aggregate SBOM, declare which projects it contains. A single-project build declares itself:
Kotlin DSL:
dependencies {
cyclonedxAggregation(project(":"))
}Groovy DSL:
dependencies {
cyclonedxAggregation project(':')
}./gradlew cyclonedxBomThe Aggregate SBOM is written to build/reports/cyclonedx/bom.{json,xml}. Multi-project builds are covered in
Aggregate a multi-project build.
| Task | Use it when | Default output |
|---|---|---|
cyclonedxDirectBom |
You need the SBOM of one project, for example to publish or attest with that project's artifact. | <project>/build/reports/cyclonedx-direct/bom.{json,xml} |
cyclonedxBom |
You need one SBOM combining several projects, for example for a distribution assembled from them. | <project>/build/reports/cyclonedx/bom.{json,xml} |
Applying the plugin to a project registers both tasks on that project, and on that project only. cyclonedxBom
aggregates exactly the projects declared on the same project's cyclonedxAggregation configuration. When none are
declared it has nothing to aggregate and is skipped, so a project can apply the plugin purely to contribute its Direct
SBOM.
Generate every Direct SBOM of the build:
./gradlew cyclonedxDirectBomGenerate one subproject's Direct SBOM:
./gradlew :subproject:cyclonedxDirectBomApply the plugin to every project whose Direct SBOM belongs to the Aggregate SBOM, and to the Aggregating Project that
produces it. Then declare the members on the Aggregating Project's cyclonedxAggregation configuration, in the same way
as Gradle's jacocoAggregation and testReportAggregation.
In app-a/build.gradle.kts and app-b/build.gradle.kts:
plugins {
id("org.cyclonedx.bom") version "4.0.0"
}In the Aggregating Project's build.gradle.kts:
plugins {
id("org.cyclonedx.bom") version "4.0.0"
}
dependencies {
cyclonedxAggregation(project(":app-a"))
cyclonedxAggregation(project(":app-b"))
}Or, in Groovy DSL:
dependencies {
cyclonedxAggregation project(':app-a')
cyclonedxAggregation project(':app-b')
}Run ./gradlew :cyclonedxBom. Gradle builds each member's Direct SBOM first. The Aggregate SBOM contains every
declared member with its components and relationships, plus a dependency from the Aggregate SBOM's main component to
each member's main component. The leading : runs only the root's aggregation; an unqualified ./gradlew cyclonedxBom
also runs, and skips, the task in every project that declares no members.
Membership is explicit only:
- Nothing is discovered. A project that is not declared is not in the Aggregate SBOM, even if it applies the plugin.
- The Aggregating Project's own Direct SBOM, with the dependencies declared in its build script, is included only when
it declares itself with
cyclonedxAggregation(project(":")), or with its own path when it is not the root. cyclonedxAggregationaccepts only project dependencies. A module dependency fails the build.
Rather than leaving a declared member out of the document, cyclonedxBom fails when that member:
- does not apply the plugin, so it has no
cyclonedxDirectBomvariant; - has a disabled
cyclonedxDirectBomtask, or unsets both outputs, so it publishes no Direct SBOM; - has no Direct SBOM on disk because the producing task was skipped at execution time, for example by
onlyIf; or - has a Direct SBOM that cannot be parsed.
Any project can be an Aggregating Project, and several can coexist, for example one Aggregate SBOM per distribution.
Remove its cyclonedxAggregation declaration. Disabling a declared member's cyclonedxDirectBom task does not exclude
it: the member then publishes no Direct SBOM and cyclonedxBom fails, even when a file from an earlier build is still
on disk.
To also stop generating a project's Direct SBOM, disable the task in that project once nothing declares it:
Kotlin DSL:
tasks.cyclonedxDirectBom {
enabled = false
}Groovy DSL:
tasks.cyclonedxDirectBom {
enabled = false
}The per-project setup above is compatible with
Project Isolation: each project configures only
itself, and the Aggregate SBOM consumes members through variant-aware dependency resolution. It is tested with
-Dorg.gradle.unsafe.isolated-projects=true on Gradle 8.11 and newer. Project Isolation is an incubating Gradle feature.
Anything that configures other projects from one build script, such as allprojects, subprojects, or the whole-tree
initialization script, requires Project Isolation to be disabled.
Configuration belongs directly to cyclonedxDirectBom and cyclonedxBom; the plugin does not add an extension.
Configure a Direct SBOM in the project it describes, and configure an Aggregate SBOM in its Aggregating Project. Values
are not copied between the tasks. For example, set schemaVersion on both when Direct and Aggregate SBOMs should use
the same non-default schema.
Configure each Direct SBOM in the project it describes. For example, in subproject/build.gradle.kts:
Kotlin DSL:
import org.cyclonedx.gradle.CyclonedxDirectTask
import org.cyclonedx.model.Component
tasks.named<CyclonedxDirectTask>("cyclonedxDirectBom") {
projectType = Component.Type.APPLICATION
includeLicenseText = true
}Or in subproject/build.gradle:
import org.cyclonedx.gradle.CyclonedxDirectTask
import org.cyclonedx.model.Component
tasks.named('cyclonedxDirectBom', CyclonedxDirectTask) {
projectType = Component.Type.APPLICATION
includeLicenseText = true
}When many projects share settings, put the plugin and this configuration in a
convention plugin and apply
it to those projects. Unlike allprojects and subprojects, a convention plugin keeps each project's configuration
local to it and remains compatible with Project Isolation.
By default, a Direct SBOM scans every resolvable project configuration. includeConfigs and skipConfigs contain
regular expressions matched against the whole configuration name, as with Java's String.matches. An empty
includeConfigs includes every resolvable configuration; a matching skipConfigs entry always excludes a
configuration.
testConfigs does not select dependencies. It controls the cdx:maven:package:test label on components that are
already in the Direct SBOM. A component is marked as test only when every configuration that contributed it matches a
testConfigs pattern.
Kotlin DSL:
tasks.cyclonedxDirectBom {
includeConfigs = listOf("runtimeClasspath", "compileClasspath")
skipConfigs = listOf("(?i).*test.*")
testConfigs = listOf("(?i).*test.*")
}Groovy DSL:
tasks.cyclonedxDirectBom {
includeConfigs = ['runtimeClasspath', 'compileClasspath']
skipConfigs = ['(?i).*test.*']
testConfigs = ['(?i).*test.*']
}Set testConfigs to an empty list when no configuration should be classified as a Test Configuration.
Both tasks write JSON and XML by default. Assign a different file to move or rename an output. Clear an output's
convention to disable that format. The explicit RegularFile cast keeps the example compatible with Gradle 8.4.
Configure each project's tasks in that project, or in a convention plugin it applies.
Kotlin DSL:
import org.gradle.api.file.RegularFile
tasks.cyclonedxDirectBom {
jsonOutput = layout.buildDirectory.file("reports/sbom/${project.name}-bom.json")
xmlOutput.convention(null as RegularFile?)
}
tasks.cyclonedxBom {
jsonOutput = layout.buildDirectory.file("reports/sbom/bom.json")
xmlOutput.convention(null as RegularFile?)
}Groovy DSL:
import org.gradle.api.file.RegularFile
tasks.cyclonedxDirectBom {
jsonOutput = layout.buildDirectory.file("reports/sbom/${project.name}-bom.json")
xmlOutput.convention((RegularFile) null)
}
tasks.cyclonedxBom {
jsonOutput = layout.buildDirectory.file('reports/sbom/bom.json')
xmlOutput.convention((RegularFile) null)
}A member that disables one format still contributes the other. A member that disables both publishes no Direct SBOM, so declaring it fails aggregation.
When includeBuildSystem is true, the plugin automatically detects build URLs from GitHub Actions, GitLab CI,
Jenkins, CircleCI, Travis CI, and Drone. Set buildSystemEnvironmentVariable to use another environment variable or
to construct a URL from several variables.
Every variable in a template must exist and have a non-blank value; otherwise no build-system reference is added.
Kotlin DSL:
tasks.cyclonedxDirectBom {
buildSystemEnvironmentVariable = "\${CI_SERVER_URL}/jobs/\${CI_JOB_ID}"
}Groovy DSL:
tasks.cyclonedxDirectBom {
buildSystemEnvironmentVariable = '${CI_SERVER_URL}/jobs/${CI_JOB_ID}'
}To read one variable directly, set the property to its name, for example "BUILD_URL" in Kotlin or 'BUILD_URL' in
Groovy.
| Property | Type | Default | Description |
|---|---|---|---|
componentGroup |
String |
Project group | Group of the document's main component. |
componentName |
String |
Project name | Name of the document's main component. |
componentVersion |
String |
Project version | Version of the document's main component. |
componentDescription |
String |
Not set | Description of the document's main component. |
projectType |
Component.Type |
LIBRARY |
CycloneDX type of the main component. Kotlin requires an enum value such as Component.Type.APPLICATION; Groovy also accepts a type name such as 'application'. |
schemaVersion |
Version |
VERSION_16 |
CycloneDX schema used for serialization. Set Version.VERSION_17 to opt in to CycloneDX 1.7. |
includeBomSerialNumber |
Boolean |
true |
Add a generated urn:uuid: serial number. |
includeLicenseText |
Boolean |
false |
Include complete license text when it can be resolved. |
includeBuildSystem |
Boolean |
true |
Add a BUILD_SYSTEM external reference when the build URL can be detected. |
buildSystemEnvironmentVariable |
String |
Not set | Environment-variable name or ${NAME} template used instead of automatic CI detection. |
organizationalEntity |
OrganizationalEntity |
Not set | Manufacturer or organizational metadata for the SBOM. |
licenseChoice |
LicenseChoice |
Not set | License information placed in the SBOM metadata. |
externalReferences |
List<ExternalReference> |
Not set | External references added to the main component. A VCS reference is detected from CI or the Git remote when one was not supplied. |
jsonOutput |
RegularFileProperty |
Task-specific bom.json |
JSON output. Clear its convention to disable JSON. |
xmlOutput |
RegularFileProperty |
Task-specific bom.xml |
XML output. Clear its convention to disable XML. |
| Property | Type | Default | Description |
|---|---|---|---|
includeConfigs |
List<String> |
[] |
Full-match regular expressions for configurations to scan. Empty means every resolvable configuration. |
skipConfigs |
List<String> |
[] |
Full-match regular expressions for configurations to exclude. Exclusion takes precedence over inclusion. |
testConfigs |
List<String> |
["^test.*"] |
Full-match regular expressions that classify Test Configurations for cdx:maven:package:test. Empty means no configuration is a Test Configuration. |
includeMetadataResolution |
Boolean |
true |
Resolve additional dependency metadata such as descriptions, publishers, external references, and licenses. |
includeBuildEnvironment |
Boolean |
false |
Also scan resolvable buildscript configurations. The include and skip patterns apply to them too. |
cyclonedxBom has no additional user-configurable properties.
These properties use types from cyclonedx-core-java, which is exposed by the plugin.
Kotlin DSL:
import org.cyclonedx.Version
import org.cyclonedx.model.Component
import org.cyclonedx.model.ExternalReference
import org.cyclonedx.model.License
import org.cyclonedx.model.LicenseChoice
import org.cyclonedx.model.OrganizationalContact
import org.cyclonedx.model.OrganizationalEntity
tasks.cyclonedxDirectBom {
projectType = Component.Type.APPLICATION
schemaVersion = Version.VERSION_17
componentName = "payment-service"
componentVersion = "2.0.0"
organizationalEntity = OrganizationalEntity().apply {
name = "ACME Corporation"
urls = listOf("https://www.example.com")
addContact(OrganizationalContact().apply {
name = "Security Team"
email = "security@example.com"
})
}
externalReferences = listOf(
ExternalReference().apply {
type = ExternalReference.Type.WEBSITE
url = "https://www.example.com/payment-service"
}
)
licenseChoice = LicenseChoice().apply {
addLicense(License().apply {
name = "Apache-2.0"
url = "https://www.apache.org/licenses/LICENSE-2.0.txt"
})
}
}Groovy DSL:
import org.cyclonedx.Version
import org.cyclonedx.model.Component
import org.cyclonedx.model.ExternalReference
import org.cyclonedx.model.License
import org.cyclonedx.model.LicenseChoice
import org.cyclonedx.model.OrganizationalContact
import org.cyclonedx.model.OrganizationalEntity
def organization = new OrganizationalEntity()
organization.name = 'ACME Corporation'
organization.urls = ['https://www.example.com']
def contact = new OrganizationalContact()
contact.name = 'Security Team'
contact.email = 'security@example.com'
organization.addContact(contact)
def website = new ExternalReference()
website.type = ExternalReference.Type.WEBSITE
website.url = 'https://www.example.com/payment-service'
def license = new License()
license.name = 'Apache-2.0'
license.url = 'https://www.apache.org/licenses/LICENSE-2.0.txt'
def licenses = new LicenseChoice()
licenses.addLicense(license)
tasks.cyclonedxDirectBom {
projectType = Component.Type.APPLICATION
schemaVersion = Version.VERSION_17
componentName = 'payment-service'
componentVersion = '2.0.0'
organizationalEntity = organization
externalReferences = [website]
licenseChoice = licenses
}An explicit VCS external reference suppresses automatic Git-remote detection. Other explicit reference types are combined with the automatically detected VCS reference when one is available.
The example configures one Direct SBOM. Configure the same metadata properties on cyclonedxBom when they should
describe the Aggregate SBOM's main component or metadata.
An initialization script can generate SBOMs without changing a build's files. This is useful in CI or for a build you do not own. The following script restores the zero-wiring, whole-tree aggregation of plugin 3.x: it applies the plugin to every project and declares every project, including the root, as a member of the root's Aggregate SBOM.
Warning
This script configures every project from the root, which is cross-project configuration. It works only with Project Isolation disabled. With Project Isolation enabled, apply the plugin and declare members in each build script instead.
Kotlin DSL (init.gradle.kts):
import org.cyclonedx.gradle.CyclonedxPlugin
initscript {
repositories {
gradlePluginPortal()
}
dependencies {
classpath("org.cyclonedx.bom:org.cyclonedx.bom.gradle.plugin:4.0.0")
}
}
rootProject {
val aggregator = this
allprojects {
apply<CyclonedxPlugin>()
aggregator.dependencies.add("cyclonedxAggregation", aggregator.dependencies.project(mapOf("path" to path)))
}
}Run:
./gradlew :cyclonedxBom --init-script init.gradle.ktsGroovy DSL (init.gradle):
import org.cyclonedx.gradle.CyclonedxPlugin
initscript {
repositories {
gradlePluginPortal()
}
dependencies {
classpath 'org.cyclonedx.bom:org.cyclonedx.bom.gradle.plugin:4.0.0'
}
}
rootProject { aggregator ->
aggregator.allprojects { member ->
member.apply plugin: CyclonedxPlugin
aggregator.dependencies.add('cyclonedxAggregation', aggregator.dependencies.project(path: member.path))
}
}Run:
./gradlew :cyclonedxBom --init-script init.gradleallprojects visits the root first, so the root applies the plugin before any member is declared on it. To leave a
project out, skip it in the allprojects block instead of disabling its task.
SLSA Build levels apply to an artifact's build provenance and build platform, not to its SBOM. This plugin generates the CycloneDX SBOM; a hosted build platform can separately generate signed SLSA provenance and bind that same artifact to the SBOM in an SBOM attestation. Applying this plugin alone does not establish a SLSA Build level, and there is no such thing as a “SLSA-compliant SBOM.”
An artifact must be paired with an SBOM whose boundary describes it. A JAR from one Gradle project normally uses that
project's cyclonedxDirectBom. Use cyclonedxBom only when the attested distribution represents the same set of
Contributing Projects as the Aggregate SBOM.
A Direct SBOM can be published with the artifact it describes. For a normal Maven repository, the cyclonedx
classifier follows the convention used by other CycloneDX JVM tooling:
import org.cyclonedx.gradle.CyclonedxDirectTask
import org.gradle.api.publish.maven.MavenPublication
plugins {
id("org.cyclonedx.bom") version "4.0.0"
id("maven-publish")
id("java")
}
val cyclonedxDirectBom = tasks.named<CyclonedxDirectTask>("cyclonedxDirectBom") {
xmlOutput.unsetConvention()
includeConfigs = listOf("compileClasspath", "runtimeClasspath")
jsonOutput.set(
layout.buildDirectory.file("reports/cyclonedx-direct/${project.name}-${project.version}-cyclonedx.json")
)
}
publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
artifact(cyclonedxDirectBom.flatMap { it.jsonOutput }) {
classifier = "cyclonedx"
extension = "json"
builtBy(cyclonedxDirectBom)
}
}
}
}This produces <artifact>-<version>-cyclonedx.json. Publication makes the SBOM available to consumers; it does not
sign the document or create SLSA provenance. The Gradle Plugin Portal accepts JAR artifacts but not JSON classifiers,
so projects that publish only there can distribute the same versioned file as a GitHub Release asset instead. This
project uses that release-asset approach for its own SBOM.
The following release workflow builds one JAR, creates SLSA Build provenance and a CycloneDX SBOM attestation for that exact artifact, and publishes the versioned SBOM as a GitHub Release asset. Replace the example paths with the single release artifact described by your Direct SBOM.
name: Build and attest
on:
release:
types: [published]
permissions:
contents: write
id-token: write
attestations: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v6
- name: Build artifact and Direct SBOM
run: ./gradlew jar cyclonedxDirectBom
- name: Attest SLSA Build provenance
uses: actions/attest@v4
with:
subject-path: build/libs/my-app-1.0.0.jar
- name: Attest CycloneDX SBOM
uses: actions/attest@v4
with:
subject-path: build/libs/my-app-1.0.0.jar
sbom-path: build/reports/cyclonedx-direct/my-app-1.0.0-cyclonedx.json
- name: Publish SBOM with the release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release upload "${{ github.event.release.tag_name }}" \
build/reports/cyclonedx-direct/my-app-1.0.0-cyclonedx.json \
--clobberGitHub documents its artifact provenance attestations as providing SLSA v1 Build Level 2 for supported workflows. Other hosted build platforms can implement the same two-attestation pattern. Choose action version pinning and release asset retry behavior that match your project's security policy. This project's own pinned, self-verifying implementation is in the release workflow.
Consumers can verify both claims against a downloaded artifact. Provenance is the default predicate; CycloneDX uses
the https://cyclonedx.org/bom predicate:
gh attestation verify my-app-1.0.0.jar --repo owner/repository
gh attestation verify my-app-1.0.0.jar \
--repo owner/repository \
--predicate-type https://cyclonedx.org/bomSee GitHub's documentation for artifact attestations
and gh attestation verify.
| Plugin version | Gradle version |
|---|---|
| 4.x | 8.4 or newer; Project Isolation tested on 8.11 or newer |
| 3.x | 8.4 or newer |
| 2.x | 8.0 or newer |
| 1.x | Earlier than 8.0 |
The table records the newest schema supported by each plugin line. Use the newest plugin version compatible with the Gradle version and downstream CycloneDX consumer in your environment.
| Plugin version | Newest CycloneDX schema | Formats |
|---|---|---|
| 4.x | 1.7 opt-in; 1.6 default | XML and JSON |
| 3.x | 1.7 opt-in; 1.6 default | XML and JSON |
| 2.x | 1.6 | XML and JSON |
| 1.10.x | 1.6 | XML and JSON |
| 1.9.x | 1.6 | XML and JSON |
| 1.8.x | 1.5 | XML and JSON |
| 1.7.x | 1.4 | XML and JSON |
| 1.6.x | 1.4 | XML and JSON |
| 1.5.x | 1.3 | XML and JSON |
| 1.4.x | 1.3 | XML and JSON |
| 1.2.x | 1.2 | XML and JSON |
| 1.1.x | 1.1 | XML |
| 1.0.x | 1.0 | XML |
- Read the CycloneDX specification.
- Ask questions in the CycloneDX discussion group or Slack.
- Report bugs and request features in GitHub Issues.
- See CONTRIBUTING.md before submitting a change.
Copyright (c) OWASP Foundation. All Rights Reserved.
Licensed under the Apache License, Version 2.0. See LICENSE.