Skip to content

Commit 271906c

Browse files
committed
feat: add support for configurable hash algorithms in SBOM generation
Signed-off-by: Cuong Tran <cuong.tran@gmail.com>
1 parent ee8e6a2 commit 271906c

File tree

6 files changed

+129
-1
lines changed

6 files changed

+129
-1
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ tasks.cyclonedxBom {
184184
| `schemaVersion` | `SchemaVersion` | `VERSION_16` | CycloneDX schema version to use |
185185
| `includeBomSerialNumber` | `boolean` | `true` | Include unique BOM serial number |
186186
| `includeLicenseText` | `boolean` | `false` | Include full license text in components |
187+
| `hashAlgorithms` | `List<String>` | `[]` (all algorithms) | Hash algorithms to include in components. Supported values: "MD5", "SHA-1", "SHA-256", "SHA-512", "SHA3-256", "SHA3-512". Leave empty for schema defaults |
187188
| `includeMetadataResolution` | `boolean` | `true` | Include complete metadata resolution for components |
188189
| `includeBuildEnvironment` | `boolean` | `false` | Include build environment dependencies (e.g. from buildscript) |
189190
| `includeBuildSystem` | `boolean` | `true` | Include build system URL from CI environment |
@@ -289,6 +290,25 @@ tasks.cyclonedxDirectBom {
289290
}
290291
```
291292

293+
#### Hash Algorithm Filtering
294+
295+
To reduce SBOM file size and generation time, you can limit the hash algorithms included in the SBOM:
296+
297+
```kotlin
298+
tasks.cyclonedxDirectBom {
299+
// Include only SHA-256
300+
hashAlgorithms = listOf("SHA-256")
301+
}
302+
303+
// Or include multiple algorithms
304+
tasks.cyclonedxDirectBom {
305+
// Include both SHA-256 and SHA-512 for additional verification
306+
hashAlgorithms = listOf("SHA-256", "SHA-512")
307+
}
308+
```
309+
310+
By default (when `hashAlgorithms` is empty), all hash algorithms supported by the selected CycloneDX schema version are included.
311+
292312
#### Excluding Projects from Aggregation
293313

294314
To exclude a specific project from SBOM generation (both direct and aggregate tasks), disable the task:

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ repositories {
2626
}
2727

2828
dependencies {
29-
api("org.cyclonedx:cyclonedx-core-java:11.0.1") {
29+
api("org.cyclonedx:cyclonedx-core-java:12.1.0") {
3030
exclude(group = "org.apache.logging.log4j", module = "log4j-slf4j-impl")
3131
}
3232
api("org.jspecify:jspecify:1.0.0")

src/main/java/org/cyclonedx/gradle/BaseCyclonedxTask.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,18 @@ public abstract class BaseCyclonedxTask extends DefaultTask {
186186
@Input
187187
public abstract Property<Boolean> getIncludeLicenseText();
188188

189+
/**
190+
* The hash algorithms to include in the SBOM components.
191+
* Supported values depend on the CycloneDX schema version.
192+
* Common values: "MD5", "SHA-1", "SHA-256", "SHA-512", "SHA3-256", "SHA3-512".
193+
* If not set, all algorithms supported by the schema version are included.
194+
*
195+
* @return the list of hash algorithms to include
196+
*/
197+
@Input
198+
@Optional
199+
public abstract ListProperty<String> getHashAlgorithms();
200+
189201
public BaseCyclonedxTask() {
190202
super();
191203
getComponentGroup().convention(getProject().getProviders().provider(() -> getProject()
@@ -204,5 +216,6 @@ public BaseCyclonedxTask() {
204216
getOrganizationalEntity().convention(getProject().getObjects().property(OrganizationalEntity.class));
205217
getLicenseChoice().convention(getProject().getObjects().property(LicenseChoice.class));
206218
getExternalReferences().convention(getProject().getObjects().listProperty(ExternalReference.class));
219+
getHashAlgorithms().convention(getProject().getObjects().listProperty(String.class));
207220
}
208221
}

src/main/java/org/cyclonedx/gradle/CyclonedxDirectTask.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ private void logParameters() {
235235
LOGGER.info("componentName : {}", getComponentName().get());
236236
LOGGER.info("componentVersion : {}", getComponentVersion().get());
237237
LOGGER.info("projectType : {}", getProjectType().get());
238+
LOGGER.info("hashAlgorithms : {}", getHashAlgorithms().get());
238239
LOGGER.info("------------------------------------------------------------------------");
239240
}
240241
}

src/main/java/org/cyclonedx/gradle/SbomBuilder.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.Set;
3434
import java.util.TreeSet;
3535
import java.util.UUID;
36+
import java.util.stream.Collectors;
3637
import org.cyclonedx.Version;
3738
import org.cyclonedx.gradle.model.ComponentComparator;
3839
import org.cyclonedx.gradle.model.DependencyComparator;
@@ -292,6 +293,13 @@ private Property buildIsTestProperty(final SbomComponent component) {
292293
private List<Hash> calculateHashes(final File artifactFile) {
293294
return artifactHashes.computeIfAbsent(artifactFile, f -> {
294295
try {
296+
List<String> configuredAlgorithms = task.getHashAlgorithms().get();
297+
if (!configuredAlgorithms.isEmpty()) {
298+
List<Hash.Algorithm> algorithms = configuredAlgorithms.stream()
299+
.map(Hash.Algorithm::fromSpec)
300+
.collect(Collectors.toList());
301+
return BomUtils.calculateHashes(f, version, algorithms);
302+
}
295303
return BomUtils.calculateHashes(f, version);
296304
} catch (IOException e) {
297305
LOGGER.error("{} Error encountered calculating hashes", LOG_PREFIX, e);

src/test/groovy/org/cyclonedx/gradle/DependencyResolutionSpec.groovy

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,92 @@ class DependencyResolutionSpec extends Specification {
145145
javaVersion = JavaVersion.current()
146146
}
147147

148+
def "should filter hashes by configured algorithms"() {
149+
given:
150+
String localRepoUri = TestUtils.duplicateRepo("local")
151+
152+
File testDir = TestUtils.createFromString("""
153+
plugins {
154+
id 'org.cyclonedx.bom'
155+
id 'java'
156+
}
157+
repositories {
158+
maven{
159+
url '$localRepoUri'
160+
}
161+
}
162+
group = 'com.example'
163+
version = '1.0.0'
164+
165+
dependencies {
166+
implementation("com.test:componenta:1.0.0")
167+
}
168+
169+
tasks.named("cyclonedxDirectBom") {
170+
hashAlgorithms = ['SHA-256', 'SHA-512']
171+
}""", "rootProject.name = 'simple-project'")
172+
173+
when:
174+
def result = GradleRunner.create()
175+
.withProjectDir(testDir)
176+
.withArguments(TestUtils.arguments("cyclonedxBom"))
177+
.withPluginClasspath()
178+
.build()
179+
180+
then:
181+
result.task(":cyclonedxBom").outcome == TaskOutcome.SUCCESS
182+
File jsonBom = new File(testDir, "build/reports/cyclonedx/bom.json")
183+
Bom bom = new ObjectMapper().readValue(jsonBom, Bom.class)
184+
Component componenta = bom.getComponents().find(c -> c.name == 'componenta')
185+
assert componenta.hashes != null
186+
assert componenta.hashes*.algorithm.sort() == ["SHA-256", "SHA-512"].sort()
187+
188+
where:
189+
javaVersion = JavaVersion.current()
190+
}
191+
192+
def "should return all hash algorithms when none are configured"() {
193+
given:
194+
String localRepoUri = TestUtils.duplicateRepo("local")
195+
196+
File testDir = TestUtils.createFromString("""
197+
plugins {
198+
id 'org.cyclonedx.bom'
199+
id 'java'
200+
}
201+
repositories {
202+
maven{
203+
url '$localRepoUri'
204+
}
205+
}
206+
group = 'com.example'
207+
version = '1.0.0'
208+
209+
dependencies {
210+
implementation("com.test:componenta:1.0.0")
211+
}""", "rootProject.name = 'simple-project'")
212+
213+
when:
214+
def result = GradleRunner.create()
215+
.withProjectDir(testDir)
216+
.withArguments(TestUtils.arguments("cyclonedxBom"))
217+
.withPluginClasspath()
218+
.build()
219+
220+
then:
221+
result.task(":cyclonedxBom").outcome == TaskOutcome.SUCCESS
222+
File jsonBom = new File(testDir, "build/reports/cyclonedx/bom.json")
223+
Bom bom = new ObjectMapper().readValue(jsonBom, Bom.class)
224+
Component componenta = bom.getComponents().find(c -> c.name == 'componenta')
225+
assert componenta.hashes != null
226+
// https://github.com/CycloneDX/cyclonedx-core-java/blob/master/src/main/java/org/cyclonedx/util/BomUtils.java#L59-L70
227+
assert componenta.hashes*.algorithm.sort() ==
228+
["MD5", "SHA-1", "SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384", "SHA3-512"].sort()
229+
230+
where:
231+
javaVersion = JavaVersion.current()
232+
}
233+
148234
def "should generate bom for non-jar artifacts"() {
149235
given:
150236
String localRepoUri = TestUtils.duplicateRepo("local")

0 commit comments

Comments
 (0)