sbomを埋め込んだjavaのネイティブビルドバイナリを生成して取り出してみた

# tl;dr

GraalVM 21.0.1でJavaのバイナリを生成して、バイナリにSBOMを埋めこんで取り出してみた。

```bash
vim HelloWorld.java
javac HelloWorld.java
native-image HelloWorld --enable-sbom
native-image-inspect helloworld --sbom | jq .
```

# 概要

Java言語は通常、JDKなどを使用して、jarファイルを生成して、JREを使って実行します。しかし、GraalVMというJDKを使うと、ネイティブビルド(実行ファイル)を生成できます。そのため、実行時に、JREなどが不要になります。また実行ファイルにSBOMを埋め込むことができます。埋めこまれたSBOMの情報を取りだすこともできます。本記事でこれらの技術を試しています。

## 環境

実行した環境は次のとおりです。

- macOS sonoma 14.1.1 (intel mac)
  - file コマンド
  - jq コマンド
- Oracle GraalVM 21.0.1+12.1

`file`コマンドは macOSに付属のものを使用しています。`jq`コマンドは `homebrew`由来のものを使用しています。必要であればインストールしてください。

### GraalVMのバージョン

2023年11月15日現在、Graal VMは 17系と21系が配布されています。本記事で21系を選択しています。

### 二種類のGraalVM

GraalVM はライセンスの異なる二つの GraalVM があるので注意してください。

- Oracle GraalVM
    - License : [GraalVM Free Terms and Conditions](https://www.oracle.com/downloads/licenses/graal-free-license.html) 
    - [ダウンロードページ](https://www.graalvm.org/downloads/)
- GraalVM Community Edition
    - License : [GPL-2.0 with Classpath exception](https://raw.githubusercontent.com/oracle/graal/master/LICENSE) 
    - [ダウンロードページ](https://github.com/graalvm/graalvm-ce-builds/releases/)

Oracle GraalVMとGraalVM Community Editionはほぼ同じですが、Polyglot Sandboxing  機能と、Managed Execution機能はOracle GraalVMにしかありません。詳細については、[こちら](https://www.graalvm.org/latest/security-guide/#graalvm-community-edition-downgrade)を参照してください。また、ライセンスが異なります。インストールする前にライセンスをよく比較して選択してください。なお、GraalVM Community Edition のライセンスは、今までの[OpenJDK](https://adoptopenjdk.net/releases.html)と同じライセンスです。これらのライセンスについては、[こちら](https://blogs.oracle.com/java/post/graalvm-free-license)に説明があります。

今回は、ただのおためしで、成果物の配布も行う予定もなかったので、Oracle GraalVMを選択したが、利益を産むような商用利用は契約が必要かもしれません。

これらのGraalVMの[インストール方法](https://www.graalvm.org/latest/docs/getting-started/)を参照してください。

## GraalVM JDKの選択

GraalVM は JDK ですので、通常どおり `java_home` コマンドで表示・選択できます。インストール済みのJDKと選択されているJDKを表示するコマンドの実行例です。

```bash
/usr/libexec/java_home -V
```

実行結果の例です。実際にインストールされているJDKによって表示内容は異なります。

```
Matching Java Virtual Machines (5):
    21.0.1 (x86_64) "Oracle Corporation" - "Oracle GraalVM 21.0.1+12.1" /Library/Java/JavaVirtualMachines/graalvm-jdk-21.0.1+12.1/Contents/Home
    21.0.1 (x86_64) "Eclipse Adoptium" - "OpenJDK 21.0.1" /Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home
    17.0.9 (x86_64) "Eclipse Adoptium" - "OpenJDK 17.0.9" /Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home
    11.0.21 (x86_64) "Eclipse Adoptium" - "OpenJDK 11.0.21" /Library/Java/JavaVirtualMachines/temurin-11.jdk/Contents/Home
    1.8.0_392 (x86_64) "Eclipse Temurin" - "Eclipse Temurin 8" /Library/Java/JavaVirtualMachines/temurin-8.jdk/Contents/Home
/Library/Java/JavaVirtualMachines/graalvm-jdk-21.0.1+12.1/Contents/Home
```

また、このJAVA_HOMEの下のbinディレクトリを環境変数 PATH に追加されていることを確認してください。

参考までに現在のJAVA_HOMEディレクトリ直下のbinディレクトリをPATHの先頭に追加するには次のコマンドを実行します。

```bash
export PATH=$(/usr/libexec/java_home)/bin:$PATH
```

さらに、javacコマンドのバージョンは次のコマンドで確認できます。説明どおりであれば、`javac 21.0.1`と出力されるはずです。

```bash
javac -version
```

# サンプルJavaのコード

今回は実行するJavaのコードは、よくある `Hello World!` を表示する簡単なコードです。任意のエディタやIDEで次の内容の`HelloWorld.java`ファイルを作成してください。

```java
// HelloWorld.java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}
```

# GraalVMのjavacでコンパイル

通常 Javaのビルドには `gradle`や `mvn` コマンドを使います。しかし、ここでは簡単のために、javacコマンドで、`HelloWorld.java` Java ソースファイルから `HelloWorld.class` Java クラスファイルを生成します。次のコマンドを実行すると、`HelloWorld.java`のあるディレクトリに`HelloWorld.class`が生成されます。

```bash
javac HelloWorld.java
```

## ファイルの確認

ソースコードとクラスファイルの内容を `file` コマンドで確認します。

```bash
file HelloWorld.*
```

実行結果の例です。 `HelloWorld.class` が生成されており、Java のクラスファイルであることが確認できました。

```
HelloWorld.class: compiled Java class data, version 65.0
HelloWorld.java:  Java source text, ASCII text
```

## ネィティブイメージの生成(ネイティブバイナリの生成)

GraalVMの`native-image` コマンドを使って Java クラスファイルから ネイティブイメージを生成します。Javaクラスファイルには `HelloWorld`クラスがありますので、クラス名を指定して実行します。

このコマンドは`HelloWorld.class`ファイルと同じディレクトリで実行してください。

```bash
native-image HelloWorld
```

実行結果の例です。

```
================================================================================
GraalVM Native Image: Generating 'helloworld' (executable)...
================================================================================
[1/8] Initializing...                                           (16.1s @ 0.07GB)
 Java version: 21.0.1+12, vendor version: Oracle GraalVM 21.0.1+12.1
 Graal compiler: optimization level: 2, target machine: x86-64-v3, PGO: off
 C compiler: cc (apple, x86_64, 15.0.0)
 Garbage collector: Serial GC (max heap size: 80% of RAM)
 1 user-specific feature(s):
 - com.oracle.svm.thirdparty.gson.GsonFeature
--------------------------------------------------------------------------------
Build resources:
 - 12.09GB of memory (75.6% of 16.00GB system memory, determined at start)
 - 8 thread(s) (100.0% of 8 available processor(s), determined at start)
[2/8] Performing analysis...  [*****]                           (13.9s @ 0.18GB)
    2,111 reachable types   (60.8% of    3,471 total)
    2,002 reachable fields  (45.6% of    4,392 total)
    9,600 reachable methods (38.1% of   25,170 total)
      754 types,   109 fields, and   475 methods registered for reflection
       49 types,    33 fields, and    48 methods registered for JNI access
        4 native libraries: -framework Foundation, dl, pthread, z
[3/8] Building universe...                                       (2.3s @ 0.21GB)
[4/8] Parsing methods...      [*]                                (1.6s @ 0.25GB)
[5/8] Inlining methods...     [***]                              (1.4s @ 0.26GB)
[6/8] Compiling methods...    [*****]                           (27.2s @ 0.28GB)
[7/8] Layouting methods...    [*]                                (1.9s @ 0.23GB)
[8/8] Creating image...       [**]                               (2.4s @ 0.29GB)
   3.19MB (45.18%) for code area:     4,519 compilation units
   3.74MB (52.92%) for image heap:   57,264 objects and 43 resources
 137.56kB ( 1.90%) for other data
   7.06MB in total
--------------------------------------------------------------------------------
Top 10 origins of code area:            Top 10 object types in image heap:
   1.72MB java.base                      837.42kB byte[] for code metadata
   1.24MB svm.jar (Native Image)         720.85kB byte[] for java.lang.String
  85.61kB com.oracle.svm.svm_enterprise  436.26kB heap alignment
  25.54kB org.graalvm.nativeimage.base   382.90kB java.lang.String
  21.67kB org.graalvm.collections        334.62kB java.lang.Class
  20.21kB jdk.internal.vm.ci             157.69kB java.util.HashMap$Node
  18.80kB jdk.proxy3                     114.01kB char[]
  17.00kB jdk.proxy1                      99.64kB byte[] for reflection metadata
  13.10kB jdk.internal.vm.compiler        88.66kB java.lang.Object[]
   7.27kB jdk.proxy2                      82.46kB c.o.s.c.h.DynamicHubCompanion
  389.00B for 1 more packages            573.49kB for 562 more object types
          Use '-H:+BuildReport' to create a report with more details.
--------------------------------------------------------------------------------
Security report:
 - Binary does not include Java deserialization.
 - Use '--enable-sbom' to embed a Software Bill of Materials (SBOM) in the binary.
--------------------------------------------------------------------------------
Recommendations:
 PGO:  Use Profile-Guided Optimizations ('--pgo') for improved throughput.
 INIT: Adopt '--strict-image-heap' to prepare for the next GraalVM release.
 HEAP: Set max heap for improved and more predictable memory usage.
 CPU:  Enable more CPU features with '-march=native' for improved performance.
 QBM:  Use the quick build mode ('-Ob') to speed up builds during development.
--------------------------------------------------------------------------------
    2.7s (3.8% of total time) in 303 GCs | Peak RSS: 0.73GB | CPU load: 3.72
--------------------------------------------------------------------------------
Produced artifacts:
 /Users/fu7mu4/Documents/hello-graalvm/helloworld (executable)
================================================================================
Finished generating 'helloworld' in 1m 7s.
```

## 確認

同じように `file` コマンドでネィティブの実行ファイルが生成されているか確認します。

```bash
file helloworld
```

実行結果は次のとおりです。ただし、intel macでの実行結果のため M1, M2, M3 macでは異なる結果になるでしょう。

```
helloworld: Mach-O 64-bit executable x86_64
```

実行可能なファイル形式であることが確認できたので実行します。

```bash
./helloworld
```

実行結果は次のとおりです。確かに `Hello World!` と表示されました。

```
Hello World!
```

### native-imageコマンドはどこから?

`native-image` コマンドは GraalVM v21に付属していました。v17系の場合は `gu` コマンド(graalvm updater)を使って別途インストールが必要かもしれません。詳細は[Install Native Image](https://www.graalvm.org/22.0/reference-manual/native-image/#install-native-image)を確認してください。



# SBOMの埋め込み

先程のネイティブバイナリを生成したときに`Software Bill of Materials`(SBOM)を埋めこみについて書かれていました。

```
--------------------------------------------------------------------------------
Security report:
 - Binary does not include Java deserialization.
 - Use '--enable-sbom' to embed a Software Bill of Materials (SBOM) in the binary.
--------------------------------------------------------------------------------
```

SBOMとは、ソフトウェアの部品表のことです。ここでは、ネイティブの実行ファイルにどんなコンポーネント(ライブラリなど)が含まれているかを示す情報のことです。SBOMがあれば、そのバイナリにどのようなライブラリが使用されているかがわかるので、例えば、含まれているライブラリに既知の脆弱性があるかどうかなどの調査ができるようになります。

SBOMのよく知られたフォーマットには、SPDXとCychloneDXがあります。GrralVMのサポートしている SBOM は CychloneDX形式だでです。

GraalVM のSBOM埋め込みの説明は [こちら](https://www.graalvm.org/latest/reference-manual/native-image/overview/HostedvsRuntimeOptions/#software-bill-of-materials-sbom) にあります。

## SBOM 埋め込みのネイティブバイナリの生成

SBOMを埋めこんだネイティブバイナリを生成します。先程のコマンドに、`--enable-sbom`を追加するだけで生成できます。ネイティブバイナリを生成する際に、GraalVMがどのようなライブラリをリンクするかなどを調査して自動的にSBOMを埋めこむようです。

次のようなコマンドを実行します。

```bash
native-image HelloWorld --enable-sbom
```

次のように標準出力がでて、`hellowworld`ファイルが生成されます。同名のファイルがあった場合は上書きされす。

```
================================================================================
GraalVM Native Image: Generating 'helloworld' (executable)...
================================================================================
[1/8] Initializing...                                           (11.1s @ 0.07GB)
 Java version: 21.0.1+12, vendor version: Oracle GraalVM 21.0.1+12.1
 Graal compiler: optimization level: 2, target machine: x86-64-v3, PGO: off
 C compiler: cc (apple, x86_64, 15.0.0)
 Garbage collector: Serial GC (max heap size: 80% of RAM)
 1 user-specific feature(s):
 - com.oracle.svm.thirdparty.gson.GsonFeature
--------------------------------------------------------------------------------
Build resources:
 - 12.09GB of memory (75.6% of 16.00GB system memory, determined at start)
 - 8 thread(s) (100.0% of 8 available processor(s), determined at start)
Warning: We could not find any version info for the following classes:
HelloWorld
[2/8] Performing analysis...  [*****]                           (10.4s @ 0.19GB)
    2,111 reachable types   (60.8% of    3,471 total)
    2,002 reachable fields  (45.6% of    4,392 total)
    9,600 reachable methods (38.2% of   25,159 total)
      754 types,   109 fields, and   475 methods registered for reflection
       49 types,    33 fields, and    48 methods registered for JNI access
        4 native libraries: -framework Foundation, dl, pthread, z
[3/8] Building universe...                                       (1.8s @ 0.21GB)
[4/8] Parsing methods...      [*]                                (1.4s @ 0.25GB)
[5/8] Inlining methods...     [***]                              (1.3s @ 0.25GB)
[6/8] Compiling methods...    [*****]                           (25.0s @ 0.33GB)
[7/8] Layouting methods...    [*]                                (1.3s @ 0.31GB)
[8/8] Creating image...       [**]                               (2.3s @ 0.27GB)
   3.20MB (45.16%) for code area:     4,519 compilation units
   3.74MB (52.86%) for image heap:   57,283 objects and 43 resources
 143.88kB ( 1.98%) for other data
   7.08MB in total
--------------------------------------------------------------------------------
Top 10 origins of code area:            Top 10 object types in image heap:
   1.73MB java.base                      838.19kB byte[] for code metadata
   1.24MB svm.jar (Native Image)         720.85kB byte[] for java.lang.String
  85.61kB com.oracle.svm.svm_enterprise  439.08kB heap alignment
  25.54kB org.graalvm.nativeimage.base   382.90kB java.lang.String
  21.67kB org.graalvm.collections        334.62kB java.lang.Class
  20.21kB jdk.internal.vm.ci             157.69kB java.util.HashMap$Node
  18.80kB jdk.proxy3                     114.01kB char[]
  17.00kB jdk.proxy1                      99.64kB byte[] for reflection metadata
  13.10kB jdk.internal.vm.compiler        88.75kB java.lang.Object[]
   7.27kB jdk.proxy2                      82.46kB c.o.s.c.h.DynamicHubCompanion
  389.00B for 1 more packages            573.82kB for 562 more object types
          Use '-H:+BuildReport' to create a report with more details.

GraalVM Native Image (https://www.graalvm.org/native-image/)

This tool can ahead-of-time compile Java code to native executables.

Usage: native-image [options] class [imagename] [options]
           (to build an image for a class)
   or  native-image [options] -jar jarfile [imagename] [options]
           (to build an image for a jar file)
   or  native-image [options] -m <module>[/<mainclass>] [options]
       native-image [options] --module <module>[/<mainclass>] [options]
           (to build an image for a module)

where options include:

    @argument files       one or more argument files containing options
    -cp <class search path of directories and zip/jar files>
    -classpath <class search path of directories and zip/jar files>
    --class-path <class search path of directories and zip/jar files>
                          A : separated list of directories, JAR archives,
                          and ZIP archives to search for class files.
    -p <module path>
    --module-path <module path>...
                          A : separated list of directories, each directory
...skipping...
--------------------------------------------------------------------------------
Security report:
 - Binary does not include Java deserialization.
 - Embedded SBOM contains 4 component(s) and is 325.00B in size.
--------------------------------------------------------------------------------
Recommendations:
 PGO:  Use Profile-Guided Optimizations ('--pgo') for improved throughput.
 INIT: Adopt '--strict-image-heap' to prepare for the next GraalVM release.
 HEAP: Set max heap for improved and more predictable memory usage.
 CPU:  Enable more CPU features with '-march=native' for improved performance.
 QBM:  Use the quick build mode ('-Ob') to speed up builds during development.
--------------------------------------------------------------------------------
    2.3s (4.1% of total time) in 260 GCs | Peak RSS: 0.77GB | CPU load: 4.46
--------------------------------------------------------------------------------
Produced artifacts:
 /Users/fu7mu4/Documents/hello-graalvm/helloworld (executable)
================================================================================
Finished generating 'helloworld' in 55.2s.
```

標準出力の `Security report`のセクションには4つのコンポーネントが含まれているとの説明がありました。

# 埋めこんだSBOM情報の取り出し

埋めこんだSBOM情報は `native-image-inspect` コマンドを使ってバイナリファイルから取り出します。SBOMのCychronDX形式は`JSON`形式をベースにしているので、`jq`で整形します。

```bash
native-image-inspect helloworld --sbom | jq .
```

出力結果の例です。`helloworld`バイナリファイルには、4つのコンポーネント(ライブラリ)が含まれていることがわかりました。

```json
{
  "bomFormat": "CycloneDX",
  "specVersion": "1.4",
  "version": 1,
  "components": [
    {
      "type": "library",
      "group": "com.oracle.svm",
      "name": "svm",
      "version": "21.0.1+12",
      "properties": [
        {
          "name": "syft:cpe23",
          "value": "cpe:2.3:a:oracle:graalvm-native-image:21.0.1+12:*:*:*:*:*:*:*"
        }
      ]
    },
    {
      "type": "library",
      "group": "org.graalvm",
      "name": "collections",
      "version": "21.0.1+12",
      "properties": [
        {
          "name": "syft:cpe23",
          "value": "cpe:2.3:a:collections:collections:21.0.1+12:*:*:*:*:*:*:*"
        },
        {
          "name": "syft:cpe23",
          "value": "cpe:2.3:a:graalvm:collections:21.0.1+12:*:*:*:*:*:*:*"
        }
      ]
    },
    {
      "type": "library",
      "group": "org.graalvm",
      "name": "nativeimage",
      "version": "21.0.1+12",
      "properties": [
        {
          "name": "syft:cpe23",
          "value": "cpe:2.3:a:graalvm:nativeimage:21.0.1+12:*:*:*:*:*:*:*"
        },
        {
          "name": "syft:cpe23",
          "value": "cpe:2.3:a:nativeimage:nativeimage:21.0.1+12:*:*:*:*:*:*:*"
        }
      ]
    },
    {
      "type": "library",
      "group": "org.graalvm",
      "name": "word",
      "version": "21.0.1+12",
      "properties": [
        {
          "name": "syft:cpe23",
          "value": "cpe:2.3:a:graalvm:word:21.0.1+12:*:*:*:*:*:*:*"
        },
        {
          "name": "syft:cpe23",
          "value": "cpe:2.3:a:word:word:21.0.1+12:*:*:*:*:*:*:*"
        }
      ]
    }
  ],
  "serialNumber": "urn:uuid:74294682-8186-3741-baa7-1034007108a4"
}
```

確かに、名前が`svm`, `collections`, `nativeimage`, `word`の4つのコンポーネントが含まれているようです。これらのグループを見ると、graalvm などの由来のようです。

# まとめ

GraalVMを使って、Javaファイルから実行ファイルを作成する方法を確認しました。また、実行ファイルに SBOMを埋めこみ、実行ファイルからSBOMを抽出する方法を確認しました。