Skip to content

Commit 5f3407d

Browse files
committed
WIP: Add CycloneDX SBOM
* Java: Uses cyclonedx-gradle-plugin * Python: Uses cyclonedx-bom * Not as rich as the SBOM for Java :(
1 parent 26c56ee commit 5f3407d

File tree

11 files changed

+647
-10
lines changed

11 files changed

+647
-10
lines changed

Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,14 @@ client-license-check: client-setup-env ## Run license compliance check
168168
@$(ACTIVATE_AND_CD) && pip-licenses
169169
@echo "--- License compliance check complete ---"
170170

171+
.PHONY: client-license-check
172+
client-sbom: client-setup-env ## Generate SBOM
173+
@echo "--- Starting SBOM gernation ---"
174+
@$(ACTIVATE_AND_CD) && mkdir -p dist; \
175+
cyclonedx-py poetry --only main --output-reproducible --validate --output-format JSON --output-file dist/bom.json --verbose; \
176+
cyclonedx-py poetry --only main --output-reproducible --validate --output-format XML --output-file dist/bom.xml --verbose
177+
@echo "--- SBOM gernation complete ---"
178+
171179
.PHONY: client-build
172180
client-build: client-setup-env ## Build client distribution. Pass FORMAT=sdist or FORMAT=wheel to build a specific format.
173181
@echo "--- Building client distribution ---"

build-logic/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ plugins { `kotlin-dsl` }
2121

2222
dependencies {
2323
implementation(gradleKotlinDsl())
24+
implementation(baselibs.cyclonedx)
2425
implementation(baselibs.errorprone)
2526
implementation(baselibs.idea.ext)
2627
implementation(baselibs.jandex)
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import java.util.Base64
21+
import org.cyclonedx.gradle.CyclonedxDirectTask
22+
import org.cyclonedx.model.AttachmentText
23+
import org.cyclonedx.model.Component.Type.APPLICATION
24+
import org.cyclonedx.model.License
25+
import org.cyclonedx.model.LicenseChoice
26+
import org.cyclonedx.model.Property
27+
import sbom.CyclonedxBundleTask
28+
import sbom.createCyclonedxConfigurations
29+
30+
if (project.plugins.hasPlugin("io.quarkus")) {
31+
// See https://quarkus.io/guides/cyclonedx#gradle-dependency-sboms
32+
tasks.named<CyclonedxDirectTask>("cyclonedxDirectBom").configure {
33+
includeConfigs.addAll("quarkusProdRuntimeClasspathConfiguration")
34+
}
35+
}
36+
37+
val cyclonedxApplicationBomTask =
38+
tasks.register<CyclonedxBundleTask>("cyclonedxApplicationBom") {
39+
group = "publishing"
40+
description = "Generate CycloneDX SBOMs for this as an application"
41+
42+
val cyclonedxDirectBom = tasks.named<CyclonedxDirectTask>("cyclonedxDirectBom")
43+
inputBoms.from(cyclonedxDirectBom.map { it.jsonOutput })
44+
45+
// want to include the original license text in the generated SBOM as it may include variations
46+
// from the "standard" license text
47+
includeLicenseText = true
48+
49+
// build system information and serial number break reproducible builds
50+
includeBuildSystem = false
51+
includeBomSerialNumber = false
52+
53+
projectType = APPLICATION
54+
55+
// Needed for projects that use subdirectories in their build/ directory, like the Spark plugin
56+
jsonOutput.set(project.layout.buildDirectory.file("reports/cyclonedx-app/bom.json"))
57+
xmlOutput.set(project.layout.buildDirectory.file("reports/cyclonedx-app/bom.xml"))
58+
59+
val relativeProjectDir = project.projectDir.relativeTo(project.rootProject.projectDir)
60+
val gitInfo = GitInfo.memoized(project)
61+
62+
licenseChoice.set(
63+
LicenseChoice().apply {
64+
addLicense(
65+
License().apply {
66+
val gitCommit = GitInfo.memoized(project).gitHead
67+
id = "Apache-2.0"
68+
// TODO URL or text ??
69+
url = gitInfo.rawGithubLink("$relativeProjectDir/distribution/LICENSE")
70+
setLicenseText(
71+
AttachmentText().apply() {
72+
contentType = "plain/text"
73+
encoding = "base64"
74+
text =
75+
Base64.getEncoder()
76+
.encodeToString(project.file("distribution/LICENSE").readBytes())
77+
}
78+
)
79+
80+
// TODO Is there a better way to include NOTICE + DISCLAIMER in a CycloneDX SBOM?
81+
val props = mutableListOf<Property>()
82+
props.add(
83+
Property().apply {
84+
name = "NOTICE"
85+
value =
86+
project.file("distribution/NOTICE").readText(Charsets.UTF_8).replace("\n", "\\n")
87+
}
88+
)
89+
val disclaimerFile = project.file("distribution/DISCLAIMER")
90+
if (disclaimerFile.isFile) {
91+
props.add(
92+
Property().apply {
93+
name = "DISCLAIMER"
94+
value = disclaimerFile.readText(Charsets.UTF_8).replace("\n", "\\n")
95+
}
96+
)
97+
}
98+
properties = props
99+
}
100+
)
101+
}
102+
)
103+
}
104+
105+
createCyclonedxConfigurations(project, cyclonedxApplicationBomTask)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import java.util.Base64
21+
import org.cyclonedx.model.AttachmentText
22+
import org.cyclonedx.model.License
23+
import org.cyclonedx.model.LicenseChoice
24+
import org.cyclonedx.model.Property
25+
import publishing.digestTaskOutputs
26+
import publishing.signTaskOutputs
27+
import sbom.CyclonedxBundleTask
28+
import sbom.createCyclonedxConfigurations
29+
30+
plugins { id("org.cyclonedx.bom") }
31+
32+
val bundleSboms by
33+
configurations.creating {
34+
isCanBeConsumed = false
35+
isCanBeResolved = true
36+
}
37+
38+
val cyclonedxBundleBom = tasks.register<CyclonedxBundleTask>("cyclonedxBundleBom")
39+
40+
cyclonedxBundleBom.configure {
41+
inputBoms = bundleSboms
42+
// The distribution itself has no dependencies, just components
43+
includeDependencies = false
44+
45+
// build system information and serial number break reproducible builds
46+
includeBuildSystem = false
47+
includeBomSerialNumber = false
48+
49+
val relativeProjectDir = project.projectDir.relativeTo(project.rootProject.projectDir)
50+
val gitInfo = GitInfo.memoized(project)
51+
52+
licenseChoice.set(
53+
LicenseChoice().apply {
54+
addLicense(
55+
License().apply {
56+
id = "Apache-2.0"
57+
// TODO URL or text ??
58+
url = gitInfo.rawGithubLink("$relativeProjectDir/LICENSE")
59+
setLicenseText(
60+
AttachmentText().apply() {
61+
contentType = "plain/text"
62+
encoding = "base64"
63+
text = Base64.getEncoder().encodeToString(project.file("LICENSE").readBytes())
64+
}
65+
)
66+
67+
// TODO Is there a better way to include NOTICE + DISCLAIMER in a CycloneDX SBOM?
68+
val props = mutableListOf<Property>()
69+
props.add(
70+
Property().apply {
71+
name = "NOTICE"
72+
value = project.file("NOTICE").readText(Charsets.UTF_8).replace("\n", "\\n")
73+
}
74+
)
75+
val disclaimerFile = project.file("DISCLAIMER")
76+
if (disclaimerFile.isFile) {
77+
props.add(
78+
Property().apply {
79+
name = "DISCLAIMER"
80+
value = disclaimerFile.readText(Charsets.UTF_8).replace("\n", "\\n")
81+
}
82+
)
83+
}
84+
properties = props
85+
}
86+
)
87+
}
88+
)
89+
}
90+
91+
createCyclonedxConfigurations(project, cyclonedxBundleBom)
92+
93+
tasks.named("assemble") { dependsOn(cyclonedxBundleBom) }
94+
95+
digestTaskOutputs(cyclonedxBundleBom)
96+
97+
signTaskOutputs(cyclonedxBundleBom)

build-logic/src/main/kotlin/publishing/PublishingHelperPlugin.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import org.gradle.kotlin.dsl.registering
3939
import org.gradle.kotlin.dsl.withType
4040
import org.gradle.plugins.signing.SigningExtension
4141
import org.gradle.plugins.signing.SigningPlugin
42+
import sbom.configureCycloneDx
4243

4344
/**
4445
* Release-publishing helper plugin to generate publications that pass Sonatype validations,
@@ -85,6 +86,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl
8586

8687
apply(plugin = "maven-publish")
8788
apply(plugin = "signing")
89+
apply(plugin = "org.cyclonedx.bom")
8890

8991
// Generate a source tarball for a release to be uploaded to
9092
// https://dist.apache.org/repos/dist/dev/<name>/apache-<name>-<version-with-rc>/
@@ -178,5 +180,6 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl
178180
}
179181

180182
addAdditionalJarContent(this)
183+
configureCycloneDx()
181184
}
182185
}

0 commit comments

Comments
 (0)