-
-
Notifications
You must be signed in to change notification settings - Fork 969
fix(sbom): mix projectPath into deterministic UUID seed #15614
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 8.0.x
Are you sure you want to change the base?
Changes from 3 commits
d6a80e0
462b2f8
4d1a78f
23f586d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -36,6 +36,7 @@ import org.cyclonedx.model.OrganizationalEntity | ||||||||||||||||
| import org.gradle.api.GradleException | |||||||||||||||||
| import org.gradle.api.Plugin | |||||||||||||||||
| import org.gradle.api.Project | |||||||||||||||||
| import org.gradle.api.Task | |||||||||||||||||
| import org.gradle.api.file.CopySpec | |||||||||||||||||
| import org.gradle.api.file.RegularFile | |||||||||||||||||
| import org.gradle.api.java.archives.Manifest | |||||||||||||||||
|
|
@@ -142,6 +143,7 @@ class SbomPlugin implements Plugin<Project> { | ||||||||||||||||
|
|
|||||||||||||||||
| // sboms are only published to Grails jar files at this time | |||||||||||||||||
| publishSbomForJarProjects(project, sbomOutputLocation) | |||||||||||||||||
| publishSbomForShadowJarProjects(project, sbomOutputLocation) | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| private static void configureSbomTask(Project project, Provider<RegularFile> sbomOutputLocation) { | |||||||||||||||||
|
|
@@ -261,10 +263,20 @@ class SbomPlugin implements Plugin<Project> { | ||||||||||||||||
| } | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| // force the serialNumber to be reproducible by removing it & recalculating | |||||||||||||||||
| // 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 uuid = UUID.nameUUIDFromBytes(withOutSerial.getBytes(StandardCharsets.UTF_8.name())) | |||||||||||||||||
| 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()) | |||||||||||||||||
|
|
@@ -384,4 +396,53 @@ class SbomPlugin implements Plugin<Project> { | ||||||||||||||||
| } | |||||||||||||||||
| } | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| /** | |||||||||||||||||
| * Wires this project's own SBOM into the shadow jar produced by the | |||||||||||||||||
| * com.gradleup.shadow plugin. | |||||||||||||||||
| * | |||||||||||||||||
| * Without this, shadow's first-wins merge picks up a META-INF/sbom.json | |||||||||||||||||
| * from one of the bundled transitive jars (typically grails-shell-cli's), | |||||||||||||||||
| * giving the fat jar the wrong serialNumber and metadata.component | |||||||||||||||||
| * (it ends up describing grails-shell-cli rather than the fat jar's own | |||||||||||||||||
| * project). Two fat jars that both bundle grails-shell-cli (e.g. | |||||||||||||||||
| * :grails-cli and :grails-cli-shadow) then end up with byte-identical | |||||||||||||||||
| * META-INF/sbom.json entries and identical urn:uuid serialNumbers, | |||||||||||||||||
| * which violates the CycloneDX 1.6 specification. | |||||||||||||||||
| * | |||||||||||||||||
| * The fix is symmetrical with publishSbomForJarProjects: we exclude any | |||||||||||||||||
| * META-INF/sbom.json that arrives from transitive dependencies during | |||||||||||||||||
| * the shadow merge, then re-introduce this project's own SBOM (whose | |||||||||||||||||
| * serialNumber is project-path-seeded and unique per fix(sbom): mix | |||||||||||||||||
| * projectPath into deterministic UUID seed). | |||||||||||||||||
| * | |||||||||||||||||
| * Uses the broad Task type to avoid a compile-time dependency on | |||||||||||||||||
| * com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar from | |||||||||||||||||
| * build-logic; the cast is safe because ShadowJar extends Jar. | |||||||||||||||||
| */ | |||||||||||||||||
| private static void publishSbomForShadowJarProjects(Project project, Provider<RegularFile> sbomOutputLocation) { | |||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think ti's correct configure these is the sbom plugin. Why aren't we using a transform on the project in question?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call - moved the entire shadow jar SBOM wiring out of SbomPlugin and into Where the logic lives now (inside the existing TaskProvider<CyclonedxDirectTask> cyclonedxDirectBomTask = tasks.named('cyclonedxDirectBom', CyclonedxDirectTask)
shadowJarTask.configure { ShadowJar it ->
// ...existing transforms / mergeServiceFiles / excludes...
it.exclude(
// ...
'META-INF/sbom.json',
// ...
)
if (!project.findProperty('skipJavaComponent')) {
it.from(cyclonedxDirectBomTask.flatMap { CyclonedxDirectTask t -> t.jsonOutput }) { CopySpec spec ->
spec.into('META-INF')
spec.rename { 'sbom.json' }
}
it.manifest { Manifest manifest ->
manifest.attributes('Sbom-Location': 'META-INF/sbom.json')
manifest.attributes('Sbom-Format': 'CycloneDX')
}
}
}Why this fits better than a hook in
Verified on Gradle 9.4.1 with
Distinct |
|||||||||||||||||
| project.plugins.withId('com.gradleup.shadow') { | |||||||||||||||||
| project.afterEvaluate { | |||||||||||||||||
| if (!project.findProperty('skipJavaComponent')) { | |||||||||||||||||
| project.tasks.named('shadowJar').configure { Task t -> | |||||||||||||||||
| Jar shadowJar = (Jar) t | |||||||||||||||||
| shadowJar.dependsOn('cyclonedxDirectBom') | |||||||||||||||||
| // Drop any META-INF/sbom.json that comes in via transitive jars during the merge. | |||||||||||||||||
| shadowJar.exclude('META-INF/sbom.json') | |||||||||||||||||
| // Re-introduce this project's own SBOM after the merge. | |||||||||||||||||
| shadowJar.from(sbomOutputLocation) { CopySpec spec -> | |||||||||||||||||
| spec.into('META-INF') | |||||||||||||||||
| spec.rename { | |||||||||||||||||
| 'sbom.json' | |||||||||||||||||
| } | |||||||||||||||||
| } | |||||||||||||||||
| shadowJar.manifest { Manifest manifest -> | |||||||||||||||||
| manifest.attributes('Sbom-Location': 'META-INF/sbom.json') | |||||||||||||||||
| manifest.attributes('Sbom-Format': 'CycloneDX') | |||||||||||||||||
| } | |||||||||||||||||
| } | |||||||||||||||||
| } | |||||||||||||||||
| } | |||||||||||||||||
| } | |||||||||||||||||
| } | |||||||||||||||||
| } | |||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -77,6 +77,12 @@ shadowJarTask.configure { ShadowJar it -> | |
|
|
||
| it.exclude( | ||
| 'META-INF/DEPENDENCIES', // until we publish our own SBOM, this won't be correct so exclude | ||
| // This module does not apply org.apache.grails.buildsrc.sbom (it's an intermediate build | ||
| // artifact, not published). Without this exclude, shadow's first-wins merge picks one of | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we dont need such a long comment: this is an intermediate build, exclude conflicting filed |
||
| // the bundled transitive META-INF/sbom.json files (typically grails-shell-cli's), | ||
| // producing a fat jar whose SBOM describes the wrong module and shares its serialNumber | ||
| // with whichever sibling jar happens to win the merge - violating CycloneDX 1.6. | ||
| 'META-INF/sbom.json', | ||
| 'about.html' // restatement of the Eclipse Distribution License - Version 1.0 for jakarta | ||
| ) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We had a compile dependency before that was removed as part of the gradle 9 update, this should remain. We're complicated a generic plugin specifically to work around one project instead of configuring the individual gradle project
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the missing-compile-dependency angle: I went back through
build-logic/plugins/build.gradleand the git history (git log -S 'shadow' -- build-logic/) and could not find a prior commit where build-logic had a compile dependency oncom.gradleup.shadow. The only build-logic implementation deps that have ever been there aregrails-publish-plugin,org.gradle.crypto.checksum, andorg.cyclonedx.bom.Either way, the new approach side-steps that question entirely:
grails-cli/build.gradlealready pulls incom.gradleup.shadowvia its own plugins block, so the shadow types are on the buildscript classpath at the point where they are actually used, with no need to add anything to build-logic.If a second module ever ends up needing both
com.gradleup.shadowandorg.apache.grails.buildsrc.sbom, the natural next step would be a small dedicated convention plugin (e.g.org.apache.grails.buildsrc.shadow-sbom) that explicitly depends on shadow, rather than re-adding shadow knowledge to the generic SBOM plugin. Today there is exactly one such project, so a per-project configuration is the simplest fit.