jdaugherty commented on code in PR #15625:
URL: https://github.com/apache/grails-core/pull/15625#discussion_r3177485886
##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy:
##########
@@ -218,81 +251,101 @@ class SbomPlugin implements Plugin<Project> {
Provider<Boolean> isReproducibleBuildProvider =
project.provider { lookupProperty(project, 'isReproducibleBuild') as boolean }
Provider<ZonedDateTime> buildDateProvider = project.provider {
lookupProperty(project, 'buildDate') as ZonedDateTime }
doLast {
- // json schema is documented here:
https://cyclonedx.org/docs/1.6/json/
- def rewriteSbom = { File f ->
- def bom = new JsonSlurper().parse(f)
-
- // timestamp is not reproducible:
https://github.com/CycloneDX/cyclonedx-gradle-plugin/issues/292
- // Use a fixed epoch when SOURCE_DATE_EPOCH is not set
so the SBOM is identical between
- // builds. This prevents the non-reproducible
timestamp from changing the jar checksum
- // and cascading cache misses through the compile
classpath of downstream projects.
- ZonedDateTime sbomTimestamp =
isReproducibleBuildProvider.get() ?
- buildDateProvider.get() :
- Instant.EPOCH.atZone(ZoneOffset.UTC)
- bom['metadata']['timestamp'] =
DateTimeFormatter.ISO_INSTANT.format(sbomTimestamp.truncatedTo(ChronoUnit.SECONDS))
-
- // components[*]
- def comps = (bom instanceof Map && bom.components
instanceof List) ? bom.components : []
- comps.each { c ->
- // .licenses => choose a license that is
compatible with ASF policy if multiple licensed
- if (c instanceof Map && c.licenses instanceof List
&& !(c.licenses as List).empty) {
- def chosen = pickLicense(logger, projectName,
c['bom-ref'] as String, c.licenses as List)
- if (chosen != null) {
- c.licenses = [chosen]
- }
- }
+ ZonedDateTime sbomTimestamp =
isReproducibleBuildProvider.get() ?
+ buildDateProvider.get() :
+ Instant.EPOCH.atZone(ZoneOffset.UTC)
+ sbomOutputLocation.get().with {
+ rewriteSbomFile(it.asFile, logger, projectName,
projectPath, sbomTimestamp)
+ }
+ }
- // .hashes => project hashes are only generated if
the jar file has been created,
- // which with a parallel build may not have
occurred, so for any dependency that is a
- // project we exclude them
- if (c instanceof Map && c.hashes instanceof List
&& !(c.hashes as List).empty) {
- def componentPath = c['bom-ref'] as String
- if (componentPath.contains('?project_path=')) {
- c.remove('hashes')
- }
- }
- }
+ }
+ }
+ }
- // dependencies[*].dependsOn is not reproducible, so
sort it
- def dependencies = (bom instanceof Map &&
bom.dependencies instanceof List) ? bom.dependencies : []
- dependencies.each { d ->
- if (d instanceof Map && d.dependsOn instanceof
List && !(d.dependsOn as List).empty) {
- d.dependsOn = (d.dependsOn as List).sort()
- }
- }
+ /**
+ * Post-process the SBOM JSON for reproducibility: pin the timestamp,
normalize
+ * dependency ordering, drop hashes for in-build project references, strip
the
+ * {@code build-system} externalReference (auto-injected by the
cyclonedx-gradle-plugin
+ * when run on GitHub Actions and unknowable from a local rebuild), and
recompute the
+ * {@code serialNumber} as a deterministic UUID derived from project path
+ content.
+ */
+ @CompileDynamic
+ private static void rewriteSbomFile(
+ File f,
+ org.gradle.api.logging.Logger logger,
+ String projectName,
+ String projectPath,
+ ZonedDateTime sbomTimestamp) {
+ Set<String> externalRefTypesToStrip = ['build-system'] as Set<String>
+ if (f == null || !f.exists() || f.length() == 0) {
+ logger.warn('SBOM file is missing or empty, skipping
reproducibility rewrite: {}', f)
+ return
+ }
+ def bom = new JsonSlurper().parse(f)
- // force the serialNumber to be reproducible by
clearing it & recalculating.
- // Mix the projectPath into the UUID seed so two
modules whose post-processed
- // BOM JSON happens to be identical (for example,
empty BOM platforms with no
- // runtime dependencies, or modules whose
metadata.component is filled in
- // identically by the CycloneDX plugin) still receive
distinct serialNumbers
- // as required by the CycloneDX specification.
Including projectPath preserves
- // reproducibility because the same project path +
same content always yields
- // the same UUID across rebuilds. This guards against
collisions introduced by
- // CycloneDX 3.0.0 / Gradle 9 metadata changes.
- // See:
https://cyclonedx.org/docs/1.6/json/#serialNumber
- bom['serialNumber'] = ''
- def withoutSerial =
JsonOutput.prettyPrint(JsonOutput.toJson(bom))
- def uuidSeed = "${projectPath}\n${withoutSerial}"
- def uuid =
UUID.nameUUIDFromBytes(uuidSeed.getBytes(StandardCharsets.UTF_8))
- bom['serialNumber'] = "urn:uuid:$uuid".toString()
-
-
f.setText(JsonOutput.prettyPrint(JsonOutput.toJson(bom)),
StandardCharsets.UTF_8.name())
-
- logger.info('Rewrote JSON SBOM ({}) to pick preferred
license', projectPath)
- }
+ bom['metadata']['timestamp'] =
DateTimeFormatter.ISO_INSTANT.format(sbomTimestamp.truncatedTo(ChronoUnit.SECONDS))
- sbomOutputLocation.get().with { rewriteSbom(it.asFile) }
+ def comps = (bom instanceof Map && bom.components instanceof List) ?
bom.components : []
+ comps.each { c ->
+ if (c instanceof Map && c.licenses instanceof List && !(c.licenses
as List).empty) {
+ def chosen = pickLicense(logger, projectName, c['bom-ref'] as
String, c.licenses as List)
+ if (chosen != null) {
+ c.licenses = [chosen]
}
+ }
+ if (c instanceof Map && c.hashes instanceof List && !(c.hashes as
List).empty) {
+ def componentPath = c['bom-ref'] as String
+ if (componentPath.contains('?project_path=')) {
+ c.remove('hashes')
+ }
+ }
+ }
+
+ def dependencies = (bom instanceof Map && bom.dependencies instanceof
List) ? bom.dependencies : []
+ dependencies.each { d ->
+ if (d instanceof Map && d.dependsOn instanceof List &&
!(d.dependsOn as List).empty) {
+ d.dependsOn = (d.dependsOn as List).sort()
}
}
+
+ def componentMeta = (bom instanceof Map && bom.metadata instanceof
Map) ? bom.metadata.component : null
+ if (componentMeta instanceof Map && componentMeta.externalReferences
instanceof List) {
+ componentMeta.externalReferences =
(componentMeta.externalReferences as List).findAll { ref ->
+ !(ref instanceof Map) ||
!externalRefTypesToStrip.contains(ref['type'] as String)
+ }
+ }
+
+ // Mix projectPath into the UUID seed so two modules whose
post-processed BOM JSON
+ // happens to be identical (for example, empty BOM platforms with no
runtime
+ // dependencies, or modules whose metadata.component is filled in
identically by the
+ // CycloneDX plugin) still receive distinct serialNumbers as required
by the CycloneDX
+ // specification. Including projectPath preserves reproducibility
because the same
+ // project path + same content always yields the same UUID across
rebuilds. See
+ // https://cyclonedx.org/docs/1.6/json/#serialNumber and PR #15614.
+ bom['serialNumber'] = ''
+ def withoutSerial = JsonOutput.prettyPrint(JsonOutput.toJson(bom))
+ def uuidSeed = "${projectPath}\n${withoutSerial}"
+ def uuid =
UUID.nameUUIDFromBytes(uuidSeed.getBytes(StandardCharsets.UTF_8))
+ bom['serialNumber'] = "urn:uuid:$uuid".toString()
+
+ f.setText(JsonOutput.prettyPrint(JsonOutput.toJson(bom)),
StandardCharsets.UTF_8.name())
+
+ logger.info('Rewrote JSON SBOM ({}) for reproducibility', projectPath)
}
private static void configureNormalization(Project project) {
project.normalization { handler ->
handler.runtimeClasspath {
+ // The per-module SBOM lives at META-INF/sbom.json; ignore it
so that SBOM-only
+ // changes (e.g. dependency version bumps that flip a license
id) do not invalidate
+ // the runtime classpath fingerprint of downstream projects.
it.ignore("META-INF/sbom.json")
+ // META-INF/sbom/** would only appear if Spring Boot 4 or a
future plugin re-injected
+ // an aggregate SBOM despite disableAggregateSbomGeneration;
keep this entry as a
+ // safety net so such a file would not silently start
invalidating consumer caches.
+ it.ignore("META-INF/sbom/**")
Review Comment:
This isn't the right solution
##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy:
##########
@@ -383,6 +436,13 @@ class SbomPlugin implements Plugin<Project> {
'sbom.json'
}
}
+ // Defense-in-depth against Spring Boot 4's
CycloneDxPluginAction
+ // (or any future Gradle plugin)
re-introducing an aggregate SBOM at
+ // META-INF/sbom/application.cdx.json.
Grails-plugin jars are libraries
+ // and ship only the per-module
META-INF/sbom.json above; the aggregate
+ // path under META-INF/sbom/ is intentionally
excluded. See
+ // disableAggregateSbomGeneration for the
primary disable.
+ jar.exclude('META-INF/sbom/**')
Review Comment:
This isn't the right solution, it masks the problem
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]