CycloneDX / cyclonedx-gradle-plugin

Creates CycloneDX Software Bill of Materials (SBOM) from Gradle projects
https://cyclonedx.org/
Apache License 2.0
157 stars 76 forks source link

BOM generation does not honor overridden dependency versions #87

Open kuporific opened 3 years ago

kuporific commented 3 years ago

Gradle allows you to specify a version of a transitive dependency if, for example, you need to use a newer version due to a 3rd party vulnerability. However, the generated BOM does not reflect the overridden version.

Here is a complete example:

build.gradle

buildscript {
    repositories {
        mavenCentral()
        maven { url 'https://jitpack.io' }
    }
}
plugins {
    id 'java'
    id 'maven-publish'
    id 'org.cyclonedx.bom' version "1.2.0"
}

group 'com.example'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    compile("com.amazonaws:amazon-sqs-java-messaging-lib:1.0.8") {
        // the amazonaws lib depends on httpclient 4.5.2 which has a vulnerability, 
        // override it to depend on version 4.5.13 which doesn't have vulnerabilities.
        implementation("org.apache.httpcomponents:httpclient:4.5.13") {
            because "Older versions have CVEs"
        }
    }
}

publishing {
    publications {
        example(MavenPublication) {
            from components.java
        }
    }
}

settings.gradle

rootProject.name = 'example'

To demonstrate, if I run ./gradlew dependencies, it shows that the transitive dependency version has been overridden with the desired version (as expected):

...
runtimeClasspath - Runtime classpath of source set 'main'.
+--- com.amazonaws:amazon-sqs-java-messaging-lib:1.0.8
|    +--- com.amazonaws:aws-java-sdk-sqs:1.11.106
|    |    +--- com.amazonaws:aws-java-sdk-core:1.11.106
|    |    |    +--- commons-logging:commons-logging:1.1.3 -> 1.2
|    |    |    +--- org.apache.httpcomponents:httpclient:4.5.2 -> 4.5.13
...

Furthermore, the build/publications/example/pom-default.xml that's generated by running ./gradlew generatePomFileForExamplePublication also shows the overridden version (as expected):

<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <!-- This module was also published with a richer model, Gradle metadata,  -->
  <!-- which should be used instead. Do not delete the following line which  -->
  <!-- is to indicate to Gradle or any Gradle module metadata file consumer  -->
  <!-- that they should prefer consuming it instead. -->
  <!-- do_not_remove: published-with-gradle-metadata -->
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>example</artifactId>
  <version>1.0-SNAPSHOT</version>
  <dependencies>
    <dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>amazon-sqs-java-messaging-lib</artifactId>
      <version>1.0.8</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.httpcomponents</groupId>
      <artifactId>httpclient</artifactId>
      <version>4.5.13</version>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
</project>

(Note that Maven dependency resolution uses the "nearest first" strategy while Gradle selects the highest version, so in any case, the desired overridden version will be used instead of the old vulnerable one.)

However, the build/reports/bom.xml generated by ./gradlew cyclonedxBom shows the wrong version (unexpected behavior):

<component type="library">
    <group>org.apache.httpcomponents</group>
    <name>httpclient</name>
    <version>4.5.2</version>
    <description><![CDATA[Apache HttpComponents Client]]></description>
    <scope>required</scope>
    <hashes>
        <hash alg="MD5">e0a45df625cb96b69505e59bb25a0189</hash>
        <hash alg="SHA-1">733db77aa8d9b2d68015189df76ab06304406e50</hash>
        <hash alg="SHA-256">0dffc621400d6c632f55787d996b8aeca36b30746a716e079a985f24d8074057</hash>
        <hash alg="SHA-384">6fc7880af7c7f91cdfc81e8c47f84b92c4bc4379ae6c4148be0ece4b14badf48d255093ac6bfa20eb38a612496eca104</hash>
        <hash alg="SHA-512">c75a4027ca5fe08a1d2b5ac1f632df2fa6d18725dcd45735ac021e19ba24f0438b53f34ee72282f5895a25d3493499bb60d03ccc215797413ca8613ac0918431</hash>
    </hashes>
    <purl>pkg:maven/org.apache.httpcomponents/httpclient@4.5.2?type=jar</purl>
</component>

I believe this is a bug in the CycloneDx Gradle plugin, and that the BOM should reflect the overridden version.

stevespringett commented 3 years ago

Thanks for the detailed report. I've been able to reproduce. In the resulting bom, it appears that both versions of httpclient appear. Version 4.5.2 and 4.5.13 are both in the resulting bom, which is obviously not correct.

stevespringett commented 3 years ago

https://github.com/CycloneDX/cyclonedx-gradle-plugin/blob/master/src/main/java/org/cyclonedx/gradle/CycloneDxTask.java#L136

final ResolvedConfiguration resolvedConfiguration = configuration.getResolvedConfiguration();
...
for (final ResolvedArtifact artifact : resolvedConfiguration.getResolvedArtifacts()) {
}

The gradle API method getResolvedArtifacts() returns BOTH versions of the component. This is unexpected and varies from how most dependency management systems work. I would assume there's some missing logic that gradlew dependencies implements that needs to be incorporated into this plugin. I'm just not sure what that is.

fanovilla commented 3 years ago

Playing with this scenario a bit, found that having the same dependency config type in the override seems to work. E.g. either of below will generate a bom with only httpclient@4.5.13

compile("com.amazonaws:amazon-sqs-java-messaging-lib:1.0.8") {
    compile("org.apache.httpcomponents:httpclient:4.5.13") {
        because "Older versions have CVEs"
    }
}

implementation("com.amazonaws:amazon-sqs-java-messaging-lib:1.0.8") {
    implementation("org.apache.httpcomponents:httpclient:4.5.13") {
        because "Older versions have CVEs"
    }
}

Also, gradle docs seem to favour use of constraints to control transitive dependency versions (albeit this is probably only available in recent versions) - https://docs.gradle.org/current/userguide/dependency_constraints.html

kuporific commented 3 years ago

Thanks @fanovilla, seems kind of obvious in hindsight not to mix the Gradle configuration types. That did the trick for me.

jensborrmann commented 3 years ago

Would you consider the description as good enough (and close) or will there be activity for improvement?

tbroyer commented 3 years ago

The problems here are that the Gradle snippet is not doing what you think it does, coupled with how the CycloneDX plugin works.

First,

dependencies {
  compile("foo") {
    implementation("bar")
  }
}

actually works exactly the same as

dependencies {
  compile("foo")
  implementation("bar")
}

so there's no "overriding for a given transitive dependency" in this case (Gradle provides some means of doing such things, but that's not what's done here), but just adding a direct dependency with a more recent version so Gradle will prefer it over the transitive one.

Now couple that with how the plugin resolves each configuration that can be resolved separately, which means resolving e.g. compileClasspath (which extends implementation, which in turn extends compile) separately from, say, runtime (which extends compile directly, without extending implementation; runtimeClasspath does extend both though). So when resolving a configuration that extends implementation, you'll get the new direct dependency that's preferred over the older transitive one; but when resolving a configuration that extends compile without also extending implementation you don't get the direct dependency and thus have the "original" version of the transitive.

As a workaround, configure the Gradle configurations you want CycloneDX to resolve/process (e.g. includeConfigs = ["runtimeClasspath"] if you want a SBOM only including the runtime dependencies)