Skip to content

Commit 5d3964d

Browse files
authored
Merge pull request #15087 from apache/feature/sbom
feature: generate sboms for all published binary jar files
2 parents 25c98c8 + 83651ba commit 5d3964d

File tree

124 files changed

+433
-109
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

124 files changed

+433
-109
lines changed

build.gradle

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,6 @@ subprojects {
105105
cacheChangingModulesFor(cacheHours, 'hours')
106106
}
107107
}
108-
109-
apply from: rootProject.layout.projectDirectory.file('gradle/dependency-licenses.gradle')
110108
}
111109

112110
apply {

buildSrc/build.gradle

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,12 @@ repositories {
6363
dependencies {
6464
implementation platform("org.apache.grails:grails-gradle-bom:${gradleProperties.projectVersion}")
6565
implementation 'org.apache.grails.gradle:grails-publish'
66-
implementation "gradle.plugin.com.hierynomus.gradle.plugins:license-gradle-plugin:${gradleProperties.gradleLicensePluginVersion}", {
67-
// Due to https://github.com/hierynomus/license-gradle-plugin/issues/161, spring must be excluded
68-
exclude group: 'org.springframework', module: 'spring-core'
69-
}
7066
implementation 'cloud.wondrify:asset-pipeline-gradle'
7167
implementation 'org.apache.grails:grails-docs-core'
7268
implementation 'org.apache.grails:grails-gradle-plugins'
7369
implementation 'org.asciidoctor:asciidoctor-gradle-jvm'
7470
implementation 'org.springframework.boot:spring-boot-gradle-plugin'
7571
implementation "org.nosphere.apache.rat:org.nosphere.apache.rat.gradle.plugin:${gradleProperties.apacheRatVersion}"
72+
implementation "org.cyclonedx.bom:org.cyclonedx.bom.gradle.plugin:${gradleProperties.gradleCycloneDxPluginVersion}"
7673
implementation "org.gradle.crypto.checksum:org.gradle.crypto.checksum.gradle.plugin:${gradleProperties.gradleChecksumPluginVersion}"
7774
}

gradle.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ yakworksHibernateGroovyProxyVersion=1.1
4747
# Build dependency versions not managed by BOMs
4848
apacheRatVersion=0.8.1
4949
gradleChecksumPluginVersion=1.4.0
50-
gradleLicensePluginVersion=0.16.1
50+
# note: the cyclonedx 3.0.0-alpha-1 still does not set the project correctly, so we must use the older version
51+
gradleCycloneDxPluginVersion=2.4.0
5152

5253
# micronaut libraries not in the bom due to the potential for spring mismatches
5354
micronautPlatformVersion=4.9.2

gradle/dependency-licenses.gradle

Lines changed: 0 additions & 30 deletions
This file was deleted.

gradle/java-config.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ tasks.withType(GroovyCompile).configureEach {
4444
// Grails determines the grails version via the META-INF/MANIFEST.MF file
4545
// Note: we exclude attributes such as Built-By, Build-Jdk, Created-By to ensure the build is reproducible.
4646
tasks.withType(Jar).configureEach {
47+
if (project.findProperty('skipJavaComponent')) {
48+
it.enabled = false
49+
return
50+
}
51+
4752
manifest.attributes(
4853
'Implementation-Title': 'Apache Grails',
4954
'Implementation-Version': grailsVersion,

gradle/publish-config.gradle

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,29 +30,36 @@ extensions.configure(GrailsPublishExtension) {
3030
it.developers = findProperty('pomDevelopers') as Map<String, String> ?: [graemerocher: 'Graeme Rocher']
3131
it.pomCustomization = findProperty('pomCustomization') as Closure
3232
it.publishTestSources = findProperty('pomPublishTestSources') ?: false
33-
it.testRepositoryPath = rootProject.layout.buildDirectory.dir('local-maven')
33+
it.testRepositoryPath = findProperty('skipJavaComponent') ? null : rootProject.layout.projectDirectory.dir('../build/local-maven')
3434
}
3535

36-
tasks.withType(Jar).configureEach {
37-
if(it.archiveClassifier.getOrNull() != 'javadoc') {
38-
from(rootProject.layout.projectDirectory.file('DISCLAIMER')) {
39-
into('META-INF')
40-
}
36+
if (findProperty('skipJavaComponent')) {
37+
// since the publish plugin won't register
38+
tasks.register('publishAllPublicationsToTestCaseMavenRepoRepository')
39+
}
4140

42-
def projectLicense = layout.projectDirectory.file('src/main/resources/META-INF/LICENSE')
43-
if (!projectLicense.asFile.exists()) {
44-
def basicLicense = rootProject.layout.projectDirectory.file('licenses/LICENSE-Apache-2.0.txt')
45-
from(basicLicense) {
41+
if (!findProperty('skipJavaComponent')) {
42+
tasks.withType(Jar).configureEach {
43+
if (it.archiveClassifier.getOrNull() != 'javadoc') {
44+
from(rootProject.layout.projectDirectory.file('DISCLAIMER')) {
4645
into('META-INF')
47-
rename { 'LICENSE' }
4846
}
49-
}
5047

51-
def projectNotice = layout.projectDirectory.file('src/main/resources/META-INF/NOTICE')
52-
if (!projectNotice.asFile.exists()) {
53-
def basicNotice = rootProject.layout.projectDirectory.file('grails-core/src/main/resources/META-INF/NOTICE')
54-
from(basicNotice) {
55-
into('META-INF')
48+
def projectLicense = layout.projectDirectory.file('src/main/resources/META-INF/LICENSE')
49+
if (!projectLicense.asFile.exists()) {
50+
def basicLicense = rootProject.layout.projectDirectory.file('licenses/LICENSE-Apache-2.0.txt')
51+
from(basicLicense) {
52+
into('META-INF')
53+
rename { 'LICENSE' }
54+
}
55+
}
56+
57+
def projectNotice = layout.projectDirectory.file('src/main/resources/META-INF/NOTICE')
58+
if (!projectNotice.asFile.exists()) {
59+
def basicNotice = rootProject.layout.projectDirectory.file('grails-core/src/main/resources/META-INF/NOTICE')
60+
from(basicNotice) {
61+
into('META-INF')
62+
}
5663
}
5764
}
5865
}

gradle/sbom-config.gradle

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
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+
* https://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 groovy.json.JsonOutput
21+
import groovy.json.JsonSlurper
22+
import org.cyclonedx.gradle.CycloneDxTask
23+
import org.cyclonedx.model.ExternalReference
24+
import org.cyclonedx.model.LicenseChoice
25+
import org.cyclonedx.model.License
26+
import org.cyclonedx.model.OrganizationalContact
27+
import org.cyclonedx.model.OrganizationalEntity
28+
import org.cyclonedx.model.Component
29+
30+
import java.nio.charset.StandardCharsets
31+
import java.time.format.DateTimeFormatter
32+
import java.time.temporal.ChronoUnit
33+
34+
apply plugin: 'org.cyclonedx.bom'
35+
36+
project.ext.setProperty('sbomOutputLocation', project.layout.buildDirectory.file("${findProperty('pomArtifactId') ?: project.name}-${projectVersion}-sbom.json"))
37+
38+
def sbomTask = tasks.named('cyclonedxBom', CycloneDxTask)
39+
sbomTask.configure { CycloneDxTask it ->
40+
// the 2.x version of Cyclonedx uses a legacy syntax & helpers for setting inputs so the syntax below
41+
// is required until the 3.x version is GA
42+
it.setProjectType(findProperty('sbomProjectType')?.toString() ?: Component.Type.FRAMEWORK.name())
43+
it.@componentName.set(findProperty('pomArtifactId')?.toString() ?: project.name)
44+
it.@organizationalEntity.set(new OrganizationalEntity().tap {
45+
name = 'Apache Software Foundation'
46+
urls = ['https://www.apache.org/', 'https://security.apache.org/']
47+
addContact(new OrganizationalContact().tap {
48+
name = 'Apache Grails Development Team'
49+
50+
})
51+
})
52+
it.@licenseChoice.set(new LicenseChoice().tap {
53+
addLicense(new License().tap {
54+
name = 'Apache-2.0'
55+
url = 'https://www.apache.org/licenses/LICENSE-2.0.txt'
56+
})
57+
})
58+
59+
it.@externalReferences.set([
60+
new ExternalReference().tap {
61+
url = 'https://grails.apache.org/'
62+
type = ExternalReference.Type.WEBSITE
63+
}
64+
])
65+
66+
// sboms are published for the purposes of vulnerability analysis so only include the runtime classpath
67+
it.@includeConfigs.set(['runtimeClasspath'])
68+
it.@skipConfigs.set(['compileClasspath', 'testRuntimeClasspath'])
69+
70+
// turn off license text since it's base64 encoded & will inflate the jar sizes
71+
it.@includeLicenseText.set(false)
72+
73+
// disable xml output
74+
it.xmlOutput.unsetConvention()
75+
76+
def sbomOutputLocation = findProperty('sbomOutputLocation')
77+
it.jsonOutput.set(sbomOutputLocation.get())
78+
it.outputs.file(sbomOutputLocation)
79+
80+
// cyclonedx does not support "choosing" the license placed in the sbom
81+
// see: https://github.com/CycloneDX/cyclonedx-gradle-plugin/issues/16
82+
it.doLast {
83+
// ordered so that first value is the most preferred, this list is from https://www.apache.org/legal/resolved.html
84+
def preferredLicenses = ['Apache-2.0', 'EPL-1.0', 'BSD-3-Clause', 'EPL-2.0', 'MIT', 'MIT-0', '0BSD', 'UPL-1.0',
85+
'CC0-1.0', 'ICU', 'Xnet', 'NCSA', 'W3C', 'Zlib', 'AFL-3.0', 'MS-PL', 'PSF-2.0', 'APAFML',
86+
'BSL-1.0', 'WTFPL', 'Unlicense', 'HPND', 'EPICS', 'TCL']
87+
88+
// licenses are standardized @ https://spdx.org/licenses/
89+
def licenses = [
90+
'Apache-2.0' : [
91+
id : 'Apache-2.0',
92+
url : 'https://www.apache.org/licenses/LICENSE-2.0'
93+
],
94+
'BSD-2-Clause': [
95+
id : 'BSD-2-Clause',
96+
url : 'https://opensource.org/license/bsd-3-clause/'
97+
],
98+
'BSD-3-Clause': [
99+
id : 'BSD-3-Clause',
100+
url : 'https://opensource.org/license/bsd-3-clause/'
101+
],
102+
// Variant of Apache 1.1 license. Approved by legal LEGAL-707
103+
'OpenSymphony' : [
104+
// id is optional and the opensymphony license doesn't have an SPDX id
105+
name: 'The OpenSymphony Software License, Version 1.1',
106+
url: 'https://raw.githubusercontent.com/sitemesh/sitemesh2/refs/heads/master/LICENSE.txt'
107+
],
108+
'UPL-1.0' : [
109+
id: 'UPL-1.0',
110+
url: 'https://oss.oracle.com/licenses/upl/'
111+
],
112+
113+
]
114+
115+
def licenseMapping = [
116+
'pkg:maven/org.antlr/[email protected]?type=jar' : 'BSD-3-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205
117+
'pkg:maven/jline/[email protected]?type=jar' : 'BSD-2-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205
118+
'pkg:maven/org.jline/[email protected]?type=jar' : 'BSD-2-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205
119+
'pkg:maven/org.liquibase.ext/[email protected]?type=jar': 'Apache-2.0', // maps incorrectly because of https://github.com/liquibase/liquibase/issues/2445 & the base pom does not define a license
120+
'pkg:maven/com.oracle.coherence.ce/[email protected]?type=pom': 'UPL-1.0', // does not have map based on license id
121+
'pkg:maven/com.oracle.coherence.ce/[email protected]?type=pom': 'UPL-1.0', // does not have map based on license id
122+
'pkg:maven/opensymphony/[email protected]?type=jar' : 'OpenSymphony', // custom license approved by legal LEGAL-707
123+
'pkg:maven/org.jruby/[email protected]?type=jar' : 'BSD-3-Clause'// https://web.archive.org/web/20240822213507/http://www.jcraft.com/jzlib/LICENSE.txt shows it's a 3 clause
124+
]
125+
126+
// we don't distribute these so these licenses are considered acceptable, but we still prefer ASF licenses.
127+
// Require a whitelist of any case of category X licenses to prevent accidental inclusion in a distributed artifact
128+
// this list will need to be updated anytime we change versions so we can revise the licenses
129+
def licenseExceptions = [
130+
'grails-data-hibernate5-core' : [
131+
'pkg:maven/org.hibernate.common/[email protected]?type=jar': 'LGPL-2.1-only', // hibernate 5 is LGPL, we are migrating to ASF license in hibernate 7
132+
'pkg:maven/org.hibernate/[email protected]?type=jar': 'LGPL-2.1-only', // hibernate 5 is LGPL, we are migrating to ASF license in hibernate 7
133+
],
134+
'grails-data-hibernate5' : [
135+
'pkg:maven/org.hibernate.common/[email protected]?type=jar': 'LGPL-2.1-only', // hibernate 5 is LGPL, we are migrating to ASF license in hibernate 7
136+
'pkg:maven/org.hibernate/[email protected]?type=jar': 'LGPL-2.1-only', // hibernate 5 is LGPL, we are migrating to ASF license in hibernate 7
137+
],
138+
'grails-data-hibernate5-spring-boot': [
139+
'pkg:maven/org.hibernate.common/[email protected]?type=jar': 'LGPL-2.1-only', // hibernate 5 is LGPL, we are migrating to ASF license in hibernate 7
140+
'pkg:maven/org.hibernate/[email protected]?type=jar': 'LGPL-2.1-only', // hibernate 5 is LGPL, we are migrating to ASF license in hibernate 7
141+
],
142+
'grails-data-hibernate5-dbmigration': [
143+
'pkg:maven/javax.xml.bind/[email protected]?type=jar': 'CDDL-1.1', // api export
144+
],
145+
]
146+
147+
def pickLicense = { String bomRef, List licenseChoices ->
148+
if (!bomRef) {
149+
throw new GradleException("No bomRef found for a dependency of ${project.name}, cannot pick license")
150+
}
151+
152+
logger.info('Picking license for {} from {} choices', bomRef, licenseChoices.size())
153+
if (licenseMapping.containsKey(bomRef)) {
154+
// There are several reasons that cyclone will get the license wrong, usually due to upstream not publishing information or publishing it incorrectly
155+
// see the licenseMapping map above for details
156+
def licenseId = licenseMapping[bomRef]
157+
logger.lifecycle('Forcing license for {} to {}', bomRef, licenseId)
158+
159+
def licenseBlock = licenses[licenseId]
160+
if (!licenseBlock) {
161+
throw new GradleException("Cannot find license information for id ${licenseId} to use for bomRef ${bomRef} in project ${project.name}")
162+
}
163+
164+
return licenseBlock
165+
}
166+
167+
if (!(licenseChoices instanceof List) || licenseChoices.isEmpty()) {
168+
throw new GradleException("No License was found for dependency: ${bomRef} in project ${project.name}")
169+
}
170+
171+
def licenseIds = licenseChoices.findAll { it instanceof Map && it.license instanceof Map && it.license.id }
172+
def foundLicense = preferredLicenses.find { p -> licenseIds.any { it.license.id == p } }
173+
if (foundLicense) {
174+
return licenseIds.find { it.license.id == foundLicense }
175+
}
176+
177+
def defaultLicense = licenseChoices[0] // pick the first one found
178+
def defaultLicenseId = defaultLicense.license.id as String
179+
if (defaultLicenseId == null) {
180+
throw new GradleException("Could not determine License id for dependency: ${bomRef} in project ${project.name} for value ${defaultLicense}")
181+
}
182+
if (!(defaultLicenseId in preferredLicenses)) {
183+
def projectLicenseExemptions = licenseExceptions[project.name] ?: [:]
184+
def permittedLicense = projectLicenseExemptions.get(bomRef) == defaultLicenseId
185+
if (!permittedLicense) {
186+
throw new GradleException("Unpermitted License found for bom dependency: ${bomRef} in project ${project.name} : ${defaultLicenseId}")
187+
}
188+
}
189+
190+
return defaultLicense
191+
}
192+
193+
// json schema is documented here: https://cyclonedx.org/docs/1.6/json/
194+
def rewriteSbom = { File f ->
195+
def bom = new JsonSlurper().parse(f)
196+
197+
// timestamp is not reproducible: https://github.com/CycloneDX/cyclonedx-gradle-plugin/issues/292
198+
bom.metadata.timestamp = DateTimeFormatter.ISO_INSTANT.format(buildDate.truncatedTo(ChronoUnit.SECONDS))
199+
200+
// components[*].licenses
201+
def comps = (bom instanceof Map && bom.components instanceof List) ? bom.components : []
202+
comps.each { c ->
203+
if (c instanceof Map && c.licenses instanceof List && !c.licenses.isEmpty()) {
204+
def chosen = pickLicense(c['bom-ref'] as String, c.licenses as List)
205+
if (chosen != null) {
206+
c.licenses = [chosen]
207+
}
208+
}
209+
}
210+
211+
// force the serialNumber to be reproducible by removing it & recalculating
212+
bom.serialNumber = ''
213+
String withOutSerial = JsonOutput.prettyPrint(JsonOutput.toJson(bom))
214+
def uuid = UUID.nameUUIDFromBytes(withOutSerial.getBytes(StandardCharsets.UTF_8.name()))
215+
def urn = "urn:uuid:${uuid.toString()}" as String
216+
bom.serialNumber = urn
217+
218+
f.setText(JsonOutput.prettyPrint(JsonOutput.toJson(bom)), StandardCharsets.UTF_8.name())
219+
220+
logger.info('Rewrote JSON SBOM ({}) to pick preferred license', project.relativePath(f))
221+
}
222+
223+
sbomOutputLocation.get().with { rewriteSbom(it.asFile) }
224+
}
225+
}
226+
227+
// for now, we only publish the sbom inside of the binary jar (our bom projects won't have a jar file)
228+
pluginManager.withPlugin('java') {
229+
tasks.named('assemble').configure {
230+
dependsOn('cyclonedxBom')
231+
}
232+
233+
if (!project.findProperty('skipJavaComponent')) {
234+
tasks.named('jar').configure { Jar jar ->
235+
jar.dependsOn('cyclonedxBom')
236+
237+
jar.from(findProperty('sbomOutputLocation')) {
238+
into('META-INF')
239+
rename {
240+
'sbom.json'
241+
}
242+
}
243+
244+
jar.manifest {
245+
attributes('Sbom-Location': 'META-INF/sbom.json')
246+
attributes('Sbom-Format': 'CycloneDX')
247+
}
248+
}
249+
}
250+
}

grails-async/core/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,6 @@ apply {
5050
from rootProject.layout.projectDirectory.file('gradle/code-style-config.gradle')
5151
from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle')
5252
from rootProject.layout.projectDirectory.file('gradle/publish-config.gradle')
53+
from rootProject.layout.projectDirectory.file('gradle/sbom-config.gradle')
5354
from rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
5455
}

0 commit comments

Comments
 (0)