How to generate an Android (React Native) SBOM CycloneDX format

Let’s walk through the steps on how to generate a mobile SBOM for an Android app and compare the results from a source code vs. binary analysis.

So that everyone can follow along, I decided to use the excellent open source note-taking app Joplin. The source code is available of GitHub and there are multiple flavors of the app including Android and iOS apps. The project also has a great article on building the Joplin applications which you can follow instead of the below directions if you prefer.

For this tutorial, we’re going to generate a list of dependencies and also a SBOM in both CycloneDX and PDF formats for the debug build of the Android app.

For the list of dependencies, we’re going to use a gradle task called dependencies and then we’ll leverage NowSecure’s (full disclosure: I’m the co-foudner) binary SBOM service. The gradle task is built-in and you can sign up for 10 free SBOMs from NowSecure.

The general steps will be:

  1. clone the Joplin repo
  2. install java and other build dependencies
  3. install Joplin app packages
  4. run gradle dependencies task
  5. build debug version of the app
  6. upload to NowSecure
  7. pull CycloneDX from REST API and PDF from custom cli

Download Joplin source code

If you already have git installed and configured, you can simple clone the repo with:

$ mkdir -p ~/spfexpert
$ cd ~/spfexpert
$ git clone https://github.com/laurent22/joplin.git

Other options include GitHub Desktop, the GitHub CLI or you can simply download it over https and unzip it on your computer.

Install Java and Joplin dependencies

Unfortunately if you’ve not a regular (Android) developer, there will be quite a few software tools that need to be installed, including:

  • XCode and Xcode Command Line Tools
  • Java
  • Node (and the yarn package manager)
  • Android Studio (plus the Android SDK and Platform tools)
  • React Native dependencies

Install XCode + command line tools

The easier way to install XCode is via the Mac App Store. If you’re running on an older version of macOS, you can search and download from the Apple Developer download page (login required but free to register).

After XCode is installed, you can install the XCode command line tools by opening Xcode, selecting Preferences, Locations and then selecting the version from dropdown. Alternatively, you can install from the command line with:

$ xcode-select --install

Install Java

Update: here’s my blog+video on “3 ways to install Java on macOS [2023]

There are tons of ways to install Java but we’ll stick with Homebrew to keep this tutorial simple. This a pretty awesome gist that explains in detail how to install older versions of java but with brew installed you should be able to use the following commands (using JDK 11 as that’s what React Native recommends)

$ brew install java11
$ sudo ln -sfn /opt/homebrew/opt/openjdk@11/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-11.jdk

You can see a list of all installed JDKs with this command:

$ /usr/libexec/java_home -V

which returns the following for my system:

Matching Java Virtual Machines (2):
    11.0.16.1 (arm64) "Homebrew" - "OpenJDK 11.0.16.1" /opt/homebrew/Cellar/openjdk@11/11.0.16.1/libexec/openjdk.jdk/Contents/Home
    1.8.0_332-b09 (arm64) "BellSoft" - "BellSoft Liberica JDK 1.8.0_332-b09" /Library/Java/JavaVirtualMachines/liberica-jdk-8-full.jdk/Contents/Home
/opt/homebrew/Cellar/openjdk@11/11.0.16.1/libexec/openjdk.jdk/Contents/Home

If you have at least one Java JDK installed, you can see the currently active JDK with:

$ java -version

which will return:

openjdk version "11.0.16.1" 2022-08-12
OpenJDK Runtime Environment Homebrew (build 11.0.16.1+0)
OpenJDK 64-Bit Server VM Homebrew (build 11.0.16.1+0, mixed mode)

Finally, you can export the JDK you want to use for your current environment with:

$ export JAVA_HOME=`/usr/libexec/java_home -v 11.0.16.1`

Doing this frequently is a hassle so there are other tools you might look into over time including jEnv and SDKMAN! that might be worth checking out.

Node

Next you need to make sure Node.js is installed which I documented in the previous article. If you installed SDKMAN, you could use that as well or simply download the Node.js installer directly from their site.

React Native is a Facebook project and they wrote an alternative node package manager called yarn. While yarn isn’t always installed by default with Node.js, you can try enabling or install it with the following:

$ nvm use node
$ brew install corepack
$ brew install vips # if on a M1 Mac

Android Studio

Next we need to install and then initialize Android Studio. In the past, you could just download command line utilities from Google but they have been deprecated and while I hate installing a full development environment, you’re just pushing a rock up a hill if you fight it. :-)

You can use the above link to install the latest version or use Homebrew!

$ brew install --cask android-studio

After it’s installed, run Android Studio and follow the Setup Wizard which will download the Android SDK components needed to build the Joplin Android app. For React Native, apparently they specifically require Android 12 SDKs (I don’t remember doing this) so you might want to follow their “Install the Android SDK” step (near the top select React Native CLI Quickstart and then Android as the Target OS)

Finally, set the needed Android environment variables as follows:

$ export ANDROID_SDK_ROOT=$HOME/Library/Android/sdk
$ export PATH=$PATH:$ANDROID_SDK_ROOT/emulator  
$ export PATH=$PATH:$ANDROID_SDK_ROOT/platform-tools

Install Joplin app packages

We’re close! We’ll now use yarn to install the various packages Joplin requires:

$ cd ~/spfexpert/joplin
$ yarn install

Run gradle dependencies task

And we’re finally ready use gradle to list the Android app’s dependencies. If you new to gradle, it can be a bit confusing starting out (and I’m still a noob). There is a shell scrip called gradlew that you invoke to run various tasks in project. For an Android app, there are many tasks that are standard and you can be reasonable sure they exist for the app you are test. You can get a list of projects and tasks with these commands (the first time will download the configured gradle distribution and start the daemon):

$ cd packages/app-mobile/android
$ ./gradlew projects
$ ./gradlew tasks

and the you can launch tasks at a project or a specific task level. For example, if you wanted to just see dependencies for the app project, you could target it with ./gradlew app:dependencies.

We’ll actually get even more specific and target only the debug build of the app with this command:

$ ./gradlew :app:dependencies --configuration debugRuntimeClasspath

which will return a bunch of output (here’s a snippet of it):

> Configure project :app
WARNING:: The option setting 'android.jetifier.ignorelist=bcprov' is experimental.

> Configure project :react-native-securerandom
WARNING:: API 'variant.getJavaCompile()' is obsolete and has been replaced with 'variant.getJavaCompileProvider()'.
It will be removed in version 7.0 of the Android Gradle plugin.
For more information, see https://d.android.com/r/tools/task-configuration-avoidance.
To determine what is calling variant.getJavaCompile(), use -Pandroid.debug.obsoleteApi=true on the command line to display more information.

> Task :app:dependencies

------------------------------------------------------------
Project ':app'
------------------------------------------------------------

debugRuntimeClasspath - Resolved configuration for runtime for variant: debug
+--- com.facebook.flipper:flipper:0.99.0
|    +--- com.facebook.soloader:soloader:0.10.1
|    |    +--- com.facebook.soloader:annotation:0.10.1
|    |    \--- com.facebook.soloader:nativeloader:0.10.1
|    +--- com.google.code.findbugs:jsr305:3.0.2
|    +--- androidx.appcompat:appcompat:1.3.0
|    |    +--- androidx.annotation:annotation:1.1.0 -> 1.2.0
|    |    +--- androidx.core:core:1.5.0
|    |    |    +--- androidx.annotation:annotation:1.2.0
|    |    |    +--- androidx.lifecycle:lifecycle-runtime:2.0.0 -> 2.3.1
|    |    |    |    +--- androidx.arch.core:core-runtime:2.1.0

<snip>

You can see the full output from the above task in this gist. It’s really useful to be able to target specific builds as well as view the dependencies hierarchically. There is also an androidDependencies task which isn’t hierarchical nor able to target a specific build configuration but can target a project. For example, you can run this command: ./gradlew :app:androidDependencies and view module dependencies as documented by Android.

One final note: when you upload an Android app to Google Play, by default your dependency info in encrypted and including in the signing block of you app. Google is then able to provide actionable feedback on issues which seems like a real bonus for devs and users!

Android SBOM via binary analysis

Since the entire dev environment is setup, let’s go ahead and build a debug apk and the generate an SBOM via binary analysis through NowSecure.

Build debug version of the app

Since we’re already in the Android app direction, build the debug apk is simple!

$ ./gradlew assembleDebug

The debug apk will be located in the app/build/outputs/apk/debug/ folder:

$ ls -lh app/build/outputs/apk/debug/
total 165856
-rw-r--r--  1 hiro  staff    66M Sep 12 09:50 app-debug.apk
-rw-r--r--  1 hiro  staff   325B Sep 12 09:50 output-metadata.json

Upload to NowSecure

NowSecure (full disclosure: I’m the co-foudner) provides static and dynamic testing for mobile apps and includes a binary SBOM service. You can sign up for 10 free SBOMs from NowSecure if you’d like to follow these steps.

I’m not going to detail the sign up process but after you have an account, you can create a JWT token you then use for API access (there’s a full Web UI as well).

Here are the steps to upload the binary and kick off a scan:

$ export API_TOKEN=<your token here>
$ curl -H "Authorization: Bearer ${API_TOKEN}" -X POST https://lab-api.nowsecure.com/build/?group=ad2c4c53-7fbf-4f81-8170-ee9b97a1ea5c --data-binary @app/build/outputs/apk/debug/app-debug.apk

which returns:

{
  "ref": "884a9f76-2f94-11ed-8006-bf3e96875a84",
  "application": "b8d29eca-2552-11ec-802b-0706d3896177",
  "group": "ad2c4c53-7fbf-4f81-8170-ee9b97a1ea5c",
  "account": "<snip>",
  "platform": "android",
  "package": "net.cozic.joplin",
  "task": 1662996718029,
  "creator": "<snip>",
  "created": "2022-09-12T15:31:58.179Z",
  "favorite": false,
  "binary": "8d1f8023da6900e6b937323a73c77dd8ed2f3bb2a010ec054d7c1787bf557b54",
  "config": { <snip> },
  "status": {
    "static": {
      "state": "pending"
    },
    "dynamic": {
      "state": "pending"
    }
  },
  "cancelled": false,
  "task_status": "pending",
  "events": {
    "dynamic": []
  }
}

After the scan is complete, you can use the assessment reference (884a9f76-2f94-11ed-8006-bf3e96875a84 in this example) to pull the results.

Pull CycloneDX from REST API

NowSecure provides a REST API to transform the binary component analysis into CycloneDX format:

$ curl -H "Authorization: Bearer ${API_TOKEN}" https://api.nowsecure.com/assessment/884a9f76-2f94-11ed-8006-bf3e96875a84/cyclonedx/ > joplin-bom.xml

and here’s a snippet of the results:

<?xml version="1.0"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" version="1">
  <metadata>
    <timestamp>2022-09-08T16:52:40.226Z</timestamp>
    <tools>
      <tool>
        <vendor>NowSecure</vendor>
        <name>Platform</name>
      </tool>
    </tools>
    <component type="application" bom-ref="net.cozic.joplin@2.9.2">
      <name>Joplin</name>
      <version>2.9.2</version>
    </component>
  </metadata>
  <components>
    <component type="library">
      <name>OpenSSL</name>
      <version>1.1.0h</version>
      <description>/lib/x86_64/libflipper.so</description>
      <purl>pkg:generic/openssl@1.1.0h</purl>
    </component>
</bom>

You can then import the CycloneDX BOM into your analysis/monitoring tool of choice. One great option is Dependency Track, another flagship OWASP project.

Comparing source and binary SBOM results

To wrap things up, let’s take a look at the difference between the results. Instead of an in-depth analysis, we’ll just point of a few obvious items:

  • Component versions: the list of dependencies from gradle contain full version info while binary analysis only has version info for some components
  • Transitive dependencies: while gradle lists some transitive dependencies, it misses important opens like OpenSSL that was caught with binary analysis. It was included in the debug build (not production!) as part of Flipper and it has 8 known vulnerabilities including CVE-2018-0732 (7.5) and CVE-2019-1543 (7.4)
  • You can leverage binary analysis on all the software you use (or build) but you need the source code for source analysis
  • I spent several days trying various tooling to output a list of Android and/or iOS dependencies. It was a frustrating and nearly unsuccessful effort that was incredibly costly in terms of my time!

In the end, the best results are probably the combination of source and binary analysis but to date, I am not aware of a tool that automatically provides both capabilities in a single tool.

Next Steps

If you’re new to Software Bill of Materials, I hope you found this blog useful. I’m planning on a number of follow up parts to this series including:

  1. Technical introduction to Software Bill of Materials (SBOMs)
  2. How to generate a Node.JS SBOM in CycloneDX format
  3. Source code vs binary analysis for SBOMs
  4. How to generate an Android (React Native) SBOM in CycloneDX format
  5. Generating an Android SBOM on each build of your mobile app with GitHub Actions
  6. Generating an iOS SBOM on each build of your mobile app with GitHub Actions
  7. Leveraging Dependency-Track to continuously analyze your mobile SBOMs

If you have any suggestions for other topics or feedback in general, please connect with me and let me know!