マネックス証券 システム管理部のHです。
はじめに
日々アプリケーション開発をする上で使用しているOSSの脆弱性というのは気にすべきことではありますが、開発作業に集中しているとOSSの脆弱性を確認するというところまではなかなか手が届かないことと、インターネットに出ない閉じたネットワーク内でのシステムだからいいや・・・と思うことがあるかと思います。
そういった状況で自分の代わりに自動でチェックする仕組みがあるといいなと思い、CI/CD内で脆弱性のチェックを行う仕組みを検討します。
やりたいこと
CI/CDとしては、AWS CodePipelineを使用し、パイプライン内のビルドツールとしてAWS CodeBuildを使用します。今回はビルドの確認でAWSリソースへのリリースは行わないので、Deployステージは作成しません。
どうやって実現するか考える
作成するアプリケーションについて
- Spring BootでシンプルなREST APIを作成します。「http://localhost:8080/data」にアクセスすると、下記のようなレスポンスを返します。
{"id":1,"description":"サンプルデータ"}
- 脆弱性のチェックについては、OWASP(Open Worldwide Application Security Project)が提供しているDependency Checkを利用します。
OWASPはセキュリティに関するオープンソース・ソフトウェアコミュニティです。
実装してみる
実装してみます。Spring BootアプリケーションとCodeBuildで使用するbuildspec.ymlを作成します。
Spring Bootアプリケーションの作成
Spring BootアプリケーションはGradleプロジェクトとして作成します。
build.gradle
- Dependency-Checkプラグインの指定
id 'org.owasp.dependencycheck' version '8.2.1'
- dependencies
脆弱性があるライブラリとして、Log4j2を追加します。(まあ他にも検出されるのですが・・・)
implementation 'org.apache.logging.log4j:log4j-core:2.15.0'
- Dependency-Checkの設定
ここを参考にDependency-Checkの設定を入れます。
注意するポイントとして、autoUpdateをfalseにしています。
このフラグは、ローカルに保存したNVD CVE/CPEデータの自動更新を有効にするかどうかというもので、本来false(自動更新しない)というのは推奨されないのですが、後述する理由によりこの値を設定しています。
formatはチェック結果を出力するフォーマットを選択することができます。
選択肢としてはHTML、XML、CSV、JSON、JUNIT、ALLがあります。
今回はローカルではHTMLを使用し、CI/CDではJUNITを使用するのでALLにします。
dataはNVD CVE/CPEデータを保存する場所で、今回このデータをCodeCommitに保存するので、場所を指定しています。
dependencyCheck { autoUpdate = false format = 'ALL' data { directory = rootProject.file("data").absolutePath } }
Spring Bootアプリケーション
コントローラクラスを一つ作り、エンドポイント(/data)の実装をします。
- Controller.java
@RestController public class Controller { @GetMapping("/data") public Data getData() { return new Data(1, "サンプルデータ"); } @AllArgsConstructor @Getter class Data { int id; String description; } }
buildspec.ymlの実装
CodeBuildで使用するbuildspec.ymlについて実装します。dependencyCheckExampleフォルダ配下に、上記に記載したSpring BootのGradleプロジェクトがあるとします。
GradleのdependencyCheckAnalyzeタスクで脆弱性チェックを行います。JUnit形式で出力したファイルをCodeBuildのレポートとして、設定します。
version: 0.2 phases: install: runtime-versions: java: corretto17 build: on-failure: CONTINUE commands: - echo "build" - cd dependencyCheckExample - ./gradlew build dependencyCheckAnalyze ⇐ここで脆弱性チェック reports: junit-report: file-format: "JUNITXML" files: - dependencyCheckExample/build/reports/dependency-check-junit.xml
CodePipelineの実装
ステージはSource、Buildとします。SourceはCodeCommitからソースを取得します。BuildはCodeBuildのプロジェクトを実行します。
CodeBuildの実装
CodeBuildはbuildspec.yml内の処理を実行します。
確認してみる
まずはローカルで。
まずはローカルで確認します。
autoUpdateをfalseに設定しているので、手動で脆弱性データベースの作成を行います。自動ではなく手動にしたのは、CodeBuildでdependencyCheckAnalyzeタスクを実行した際に、CISAやNISTから脆弱性データのファイルダウンロードに失敗することがあるためです。ローカルでは失敗しないので、今回はローカルで脆弱性データベースの作成をして、そのファイルをCodeCommitにコミットします。CodeBuildではそのファイルを使用してチェックするようにします。
./gradlew dependencyCheckUpdate
大体5分位でデータベースが作成されました。
次に脆弱性のチェックを行います。
./gradlew dependencyCheckAnalyze
数秒で終わります。その際に脆弱性があれば以下に対象のOSSとCVE番号が表示されます。期待通りLog4j2が検出されました。もう一つのはSpring Bootの依存関係に入っているライブラリです。ちなみにSpring BootのGithubでIssueを見ると、検出されたもののSpring Boot自体に脆弱性はなく、誤検知の可能性が高いとのことです。
詳細は作成されるHTMLでも確認できます。
脆弱性の情報も出力されます。
ちなみに上記のコンソールでは、Gradleタスクは正常終了していますが、CVSSスコア(脆弱性の深刻度)により異常終了させることもできます。これにより脆弱性の検知がしやすくなります。
- build.gradleにfailBuildOnCVSSの設定を追加。
failBuildOnCVSSにはCVSSスコアのしきい値を設定します。しきい値を超えた脆弱性を検出した際にGradleのタスクを失敗させます。今回は9(緊急)を設定しています。
CVSSスコアについてはここが参考になるかと思います。
dependencyCheck { autoUpdate = false format = 'ALL' failBuildOnCVSS = 9 // ⇐ここを追加 data { directory = rootProject.file("data").absolutePath } }
再度脆弱性チェックを行います。Gradleタスクは失敗し、失敗した理由が表示されます。
./gradlew dependencyCheckAnalyze
CodePipelineで実行する。
次にCodePipelineで実行します。
failBuildOnCVSSは設定しない状態にします。
CodeBuildのビルドログには以下の表に表示されます。まあコンソールと同じように脆弱性の情報が表示されます。(当たり前か・・・)
JUnit形式で出力した情報はレポートから確認することができます。円グラフや一覧表で見れるので、コンソールやHTMLよりも見やすい感じがしますね。
ちなみにCodePipelineは正常終了しています。
今度はfailBuildOnCVSSを設定して、CodeBuildを実行します。(失敗します)ビルドログはローカルで失敗させた時と同様のログが表示されます。(こちらも当たり前か・・・)
レポートの表示はCodeBuild正常時と特に変わりません。
CodePipelineではBuildで失敗します。
おわりに
CI/CDに組み込むことで、いち早く脆弱性を検知することができます。
ただ今回のケースにおいては、脆弱性データベースをCI/CD内では更新できなかったり、ビルドの設定次第では脆弱性チェックに引っかかった場合にその脆弱性を解決しない限り他の機能のリリースができないことにもなりうるため、使い方については十分検討する必要があるかと思います。