Using Repository Artifact Safelists in Gradle

With JCenter going away, we are going to be peeking more at our repositories and artifacts. After all, we need to make sure that we will continue to get the libraries that we need from their new homes, for any that were published purely to JCenter.

The timing is interesting, as “supply-chain” attacks are on the rise. It is far too easy for somebody to publish a malware-laden library and have it be picked up automatically by developers. This can even affect private artifacts in private repositories.

Ideally, while we are cleaning up our Gradle scripts, we would lock down where we get our artifacts from. The good news is that modern versions of Gradle give us somewhat better options for this. The bad news is that implementing those options is rather painful.


The problem with the default way that we declare dependencies in an Android project is that we do not say where each dependency comes from. Gradle largely disassociates artifacts (e.g., all those implementation lines) from repositories. We do not say “get this artifact from this repository”. We just list the supported repositories and desired artifacts, and Gradle fulfills our requests on its own.

And, by default, all Gradle does is use a top-down search against each repository. In other words, it uses this algorithm:

For each artifact
  For each repository in the order they are listed
    If the repository offers this artifact, download it
  End
End

There is little stopping an artifact from being offered in more than one repository, which is at the root of some of these supply-chain attacks. Depending on the order of the repositories in your Gradle script, you might get different actual artifacts than another project requesting the same artifacts but using a different repository order.


Ideally, we would say “get this artifact from this repository”. We can do that, to an extent.

Starting with Gradle 5.1, we can safelist what artifacts we get from a given repository. To do this, we add a content {} closure to the repository declaration, and in there use include...() functions to stipulate what artifacts to obtain from that repository:

jcenter {
    content {
        includeModule("org.jetbrains.trove4j", "trove4j")
    }
}

Here, we are saying that the only thing that we want to obtain from JCenter is org.jetbrains.trove4j:trove4j. JCenter will not be used for other artifacts.

The three main safelist functions are:

  • includeModule(), where you provide the artifact group (org.jetbrains.trove4j) and artifact ID (trove4j)

  • includeGroup(), to support any artifact from a specified artifact group

  • includeGroupByRegex(), which allows you to specify a regular expression and support any artifact group that matches that expression (e.g., includeGroupByRegex("org\\.jetbrains\\..*"))

If all of your repository declarations include one or more include...() functions, then the build should work purely off of those safelists:

  • The artifacts that you are using will obtained from their associated repositories and nowhere else

  • No artifacts that are not on the safelist will be used in your build

See this blog post for a bit more on the options.


The problem is that the safelists not only need to handle your direct dependencies, but also all of the transitive dependencies.

That can get rather lengthy.

This sample project is based on the tutorial project that we build in Exploring Android. The project requests 33 artifacts for the module, plus three classpath entries for Gradle plugins.

After adding all of the necessary include...() functions, the top-level build.gradle file is over 150 lines long, mostly involving those functions. And that is with cheating and using includeGroup() and includeGroupByRegex(), both of which are a bit less secure than includeModule().

Basically, what happens is that you add include...() calls for all of your direct dependencies, then try doing a build. In particular, adding --refresh-dependencies to a command-line build (e.g., gradle --refresh-dependencies app:assembleDebug) will confirm that Gradle can download all of your dependencies. You will wind up with a bunch of errors:

* What went wrong:
A problem occurred configuring root project 'GradleSafelist'.
> Could not resolve all artifacts for configuration ':classpath'.
   > Could not resolve org.glassfish.jaxb:jaxb-runtime:2.3.1.
     Required by:
         project : > com.android.tools.build:gradle:4.1.2 > androidx.databinding:databinding-compiler-common:4.1.2
         project : > com.android.tools.build:gradle:4.1.2 > com.android.tools.build:builder:4.1.2 > com.android.tools:sdklib:27.1.2 > com.android.tools:repository:27.1.2
      > Could not resolve org.glassfish.jaxb:jaxb-runtime:2.3.1.
         > Could not parse POM https://repo.maven.apache.org/maven2/org/glassfish/jaxb/jaxb-runtime/2.3.1/jaxb-runtime-2.3.1.pom
            > Could not resolve com.sun.xml.bind.mvn:jaxb-runtime-parent:2.3.1.
               > Could not resolve com.sun.xml.bind.mvn:jaxb-runtime-parent:2.3.1.
                  > Could not parse POM https://repo.maven.apache.org/maven2/com/sun/xml/bind/mvn/jaxb-runtime-parent/2.3.1/jaxb-runtime-parent-2.3.1.pom
                     > Could not resolve com.sun.xml.bind.mvn:jaxb-parent:2.3.1.
                        > Could not resolve com.sun.xml.bind.mvn:jaxb-parent:2.3.1.
                           > Could not parse POM https://repo.maven.apache.org/maven2/com/sun/xml/bind/mvn/jaxb-parent/2.3.1/jaxb-parent-2.3.1.pom
                              > Could not find com.sun.xml.bind:jaxb-bom-ext:2.3.1.
   > Could not resolve com.google.auto.value:auto-value-annotations:1.6.2.
     Required by:
         project : > com.android.tools.build:gradle:4.1.2 > com.android.tools.build:bundletool:0.14.0
      > Could not resolve com.google.auto.value:auto-value-annotations:1.6.2.
         > Could not parse POM https://repo.maven.apache.org/maven2/com/google/auto/value/auto-value-annotations/1.6.2/auto-value-annotations-1.6.2.pom
            > Could not resolve com.google.auto.value:auto-value-parent:1.6.2.
               > Could not resolve com.google.auto.value:auto-value-parent:1.6.2.
                  > Could not parse POM https://repo.maven.apache.org/maven2/com/google/auto/value/auto-value-parent/1.6.2/auto-value-parent-1.6.2.pom
                     > Could not find com.google.auto:auto-parent:6.

The end leaf of each branch shows a transitive dependency that is not covered by your include...() functions — in this case, com.sun.xml.bind:jaxb-bom-ext:2.3.1 and com.google.auto:auto-parent:6. After adding those, you run the test again and get a bunch of fresh errors from the transitive dependencies of the transitive dependencies. And, as the shampoo instructions state, lather, rinse, repeat, until eventually you get no more errors.

This process sucked… and this is not a big project. With luck, we can create some tooling to help make generating these safelists easier.

But, this is a more secure build than what it started with. The more important your project, the more likely it is that you are going to want to explore options like this for ensuring that your artifacts come from where you expect them to.


Interested in Jetpack Compose? jetc.dev has a weekly newsletter of the latest articles, samples, and other details of Compose development!