CI/CDの中でライセンススキャンを実行する


Pivotal(現VMware)が開発したライセンスチェック用OSSに[License Finder](https://github.com/pivotal/LicenseFinder)というものがある。
GitLabのLicenseチェック機能(※ULTIMATE版のみ利用可能)にも[組み込まれている](https://docs.gitlab.com/ee/user/compliance/license_compliance/)ようで、製品のライセンスチェックに使えるのではないかと思い、GitLabのCI/CDのパイプラインに組み込んだ時のメモ。

なお、調べたのはv7.0.1の時のものであり、バージョンが異なる場合は内容に不整合が生じる可能性がある点はご承知おきを。

## License Finderとは
公式ページの説明によると、「License Finder はパッケージマネージャと連携し、依存関係を検出し、そのパッケージのライセンスを検出し、それらのライセンスをユーザ定義の許可されたライセンスのリストと比較し、実用的な例外レポートを提供します」とのこと。\
単に検出に留まらず、ユーザ定義の許可ライセンスリストと比較してくれるのが大きな特徴だと思われる。

対応している言語・パッケージマネージャーは以下。

|Project Type|	Package Manager|
|---|---|
|Ruby Gems	|bundler|
|Python 2.7 Eggs	|pip2|
|Python 3.5 Eggs	|pip3|
|Node.js	|npm|
|Bower	|bower|
|Nuget (without license discovery)	|nuget|
|Godep	|Godep|
|Go workspace	|Go lang|
|Go modules	|Go lang|
|Java	|maven|
|Java	|gradle|

また、Experimental扱いだが、以下にも対応している。
- Erlang (rebar, Erlang.mk)
- Objective-C, Swift (Carthage, CocoaPods ※0.39以下, Swift Package Manager(SPM))
- Elixir (mix)
- Golang (gvt, glide,dep, trash, govendor)
- JavaScript (yarn)
- C++/C (conan)
- Scala (sbt)
- Rust (cargo)
- PHP (composer)
- Python (Conda, pipenv)
- Flutter (flutter pub)

## 手元で動作確認
### インストール
前提として、Ruby2.4.0以上が導入されている必要がある。
インストール方法は[いくつか用意されている](https://github.com/pivotal/LicenseFinder#installation)が、今回はCI/CDパイプラインに組み込むことを見据えて、Linux上でgemコマンドでインストールすることにする。
```
sudo apt install ruby-rubygems -y
sudo gem install license_finder
```

### スキャンとチェック
色んな言語のサンプルが揃っている、paketo-buildpacksの[sample](https://github.com/paketo-buildpacks/samples)のmavenに対して実行してみる。\
まずはソースをDownloadし、mavenプロジェクト下に移動する。
```
git clone https://github.com/paketo-buildpacks/samples.git /tmp/paketo-sample
cd /tmp/paketo-sample/java/maven
```
実行すると30秒ほどで結果が出力される。
```
$ license_finder
LicenseFinder::Maven: is active

Dependencies that need approval:
HdrHistogram, 2.1.12, "Public Domain, per Creative Commons CC0, Simplified BSD"
LatencyUtils, 2.0.3, "Public Domain, per Creative Commons CC0"
accessors-smart, 2.4.8, "Apache 2.0"
android-json, 0.0.20131108.vaadin1, "Apache 2.0"
apiguardian-api, 1.1.2, "Apache 2.0"
asm, 9.1, "New BSD"
assertj-core, 3.22.0, "Apache 2.0"
byte-buddy, 1.12.11, "Apache 2.0"
byte-buddy-agent, 1.12.11, "Apache 2.0"
hamcrest, 2.2, "BSD License 3"
jackson-annotations, 2.13.3, "Apache 2.0"
jackson-core, 2.13.3, "Apache 2.0"
jackson-databind, 2.13.3, "Apache 2.0"
jackson-datatype-jdk8, 2.13.3, "Apache 2.0"
jackson-datatype-jsr310, 2.13.3, "Apache 2.0"
jackson-module-parameter-names, 2.13.3, "Apache 2.0"
jakarta.activation-api, 1.2.2, "EDL 1.0"
jakarta.annotation-api, 1.3.5, "EPL 2.0, GPL2 w/ CPE"
jakarta.xml.bind-api, 2.3.3, "Eclipse Distribution License - v 1.0"
json-path, 2.7.0, "Apache 2.0"
json-smart, 2.4.8, "Apache 2.0"
jsonassert, 1.5.0, "Apache 2.0"
jul-to-slf4j, 1.7.36, MIT
junit-jupiter, 5.8.2, "Eclipse Public License v2.0"
junit-jupiter-api, 5.8.2, "Eclipse Public License v2.0"
junit-jupiter-engine, 5.8.2, "Eclipse Public License v2.0"
junit-jupiter-params, 5.8.2, "Eclipse Public License v2.0"
junit-platform-commons, 1.8.2, "Eclipse Public License v2.0"
junit-platform-engine, 1.8.2, "Eclipse Public License v2.0"
log4j-api, 2.17.2, "Apache 2.0"
log4j-to-slf4j, 2.17.2, "Apache 2.0"
logback-classic, 1.2.11, "Eclipse Public License 1.0, GNU Lesser General Public License"
logback-core, 1.2.11, "Eclipse Public License 1.0, GNU Lesser General Public License"
micrometer-core, 1.9.1, "Apache 2.0"
mockito-core, 4.5.1, MIT
mockito-junit-jupiter, 4.5.1, MIT
netty-buffer, 4.1.78.Final, "Apache 2.0"
netty-codec, 4.1.78.Final, "Apache 2.0"
netty-codec-dns, 4.1.78.Final, "Apache 2.0"
netty-codec-http, 4.1.78.Final, "Apache 2.0"
netty-codec-http2, 4.1.78.Final, "Apache 2.0"
netty-codec-socks, 4.1.78.Final, "Apache 2.0"
netty-common, 4.1.78.Final, "Apache 2.0"
netty-handler, 4.1.78.Final, "Apache 2.0"
netty-handler-proxy, 4.1.78.Final, "Apache 2.0"
netty-resolver, 4.1.78.Final, "Apache 2.0"
netty-resolver-dns, 4.1.78.Final, "Apache 2.0"
netty-resolver-dns-classes-macos, 4.1.78.Final, "Apache 2.0"
netty-resolver-dns-native-macos, 4.1.78.Final, "Apache 2.0"
netty-transport, 4.1.78.Final, "Apache 2.0"
netty-transport-classes-epoll, 4.1.78.Final, "Apache 2.0"
netty-transport-native-epoll, 4.1.78.Final, "Apache 2.0"
netty-transport-native-unix-common, 4.1.78.Final, "Apache 2.0"
objenesis, 3.2, "Apache 2.0"
opentest4j, 1.2.0, "Apache 2.0"
reactive-streams, 1.0.4, MIT-0
reactor-core, 3.4.19, "Apache 2.0"
reactor-netty-core, 1.0.20, "Apache 2.0"
reactor-netty-http, 1.0.20, "Apache 2.0"
reactor-test, 3.4.19, "Apache 2.0"
slf4j-api, 1.7.36, MIT
snakeyaml, 1.30, "Apache 2.0"
spring-aop, 5.3.21, "Apache 2.0"
spring-beans, 5.3.21, "Apache 2.0"
spring-boot, 2.7.1, "Apache 2.0"
spring-boot-actuator, 2.7.1, "Apache 2.0"
spring-boot-actuator-autoconfigure, 2.7.1, "Apache 2.0"
spring-boot-autoconfigure, 2.7.1, "Apache 2.0"
spring-boot-starter, 2.7.1, "Apache 2.0"
spring-boot-starter-actuator, 2.7.1, "Apache 2.0"
spring-boot-starter-json, 2.7.1, "Apache 2.0"
spring-boot-starter-logging, 2.7.1, "Apache 2.0"
spring-boot-starter-reactor-netty, 2.7.1, "Apache 2.0"
spring-boot-starter-test, 2.7.1, "Apache 2.0"
spring-boot-starter-webflux, 2.7.1, "Apache 2.0"
spring-boot-test, 2.7.1, "Apache 2.0"
spring-boot-test-autoconfigure, 2.7.1, "Apache 2.0"
spring-context, 5.3.21, "Apache 2.0"
spring-core, 5.3.21, "Apache 2.0"
spring-expression, 5.3.21, "Apache 2.0"
spring-jcl, 5.3.21, "Apache 2.0"
spring-test, 5.3.21, "Apache 2.0"
spring-web, 5.3.21, "Apache 2.0"
spring-webflux, 5.3.21, "Apache 2.0"
xmlunit-core, 2.9.0, "Apache 2.0"
```
せっかくなので、GoModulesのサンプルでも試してみる。
```
$ cd ../../go/mod/
$ license_finder action_items
LicenseFinder::GoModules: is active

Dependencies that need approval:
github.com/gorilla/mux, v1.8.0, "New BSD"
```
ここで注目してほしいのは、"Dependencies that need approval"であり、license_finderはあくまでもapprovalされていないライセンスのみを出力している点である。\
approvalしたらどうなるかを見てみる。

```
$ license_finder approvals add github.com/gorilla/mux
The github.com/gorilla/mux dependency has been approved!
$ license_finder
LicenseFinder::GoModules: is active

All dependencies are approved for use
```
このように、表示されなくなる。

## CI/CDパイプラインへの組み込み
公式からイメージが[提供されている](https://hub.docker.com/r/licensefinder/license_finder/)が、ちょっと癖がある。\
イメージの[Dockerfile](https://github.com/pivotal/LicenseFinder/blob/master/Dockerfile)を見ると`CMD`が
```
CMD cd /scan && /bin/bash -l
```
となっており、`bash -l`で.bash_profile等を読み込むようになっており、これを実施しないとスキャン出来ない。(-lオプションの詳細は[こちら](https://linuxjm.osdn.jp/html/GNU_bash/man1/bash.1.html)参照)\
そのため、パイプライン内で実行する場合は.profileを読み込んで環境変数等を設定するようにする。
動作確認で作った`.gitlab-ci.yml`の中身はこちらになる。
```yaml
license-scan:
  image: licensefinder/license_finder
  stage: build
  rules:
  - if: '$CI_PIPELINE_SOURCE == "push"'
  script:
  - |
    . ~/.profile
    /LicenseFinder/bin/license_finder | tee license_list.txt || true
  artifacts:
    paths:
    - license_list.txt
    expire_in: 1 days
```
license_finderはapproveされていないライセンスを検知するとexit 1で終了するため、強制的にtrueにしている。\
実行した結果がこちら。適切に取得できているのが分かる。
![1660704287273.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/b74d3d82-f63c-900d-e446-1a3c2a622b08.png)

実際にパイプライン内でデプロイをガードさせたい場合は、パイプライン内の`|| true`を外してApproveしていないライセンスがいると止めるようにすると良い。

## Gradleスキャン時の注意点
[公式サイトの記載](https://github.com/pivotal/LicenseFinder#gradle-projects)にあるように、[license-gradle-plugin](https://github.com/hierynomus/license-gradle-plugin)の設定を入れる必要があるが、license-gradle-pluginのREADMEの通りに設定しても以下のエラーが出る。
```
FAILURE: Build failed with an exception.

* What went wrong:
Some problems were found with the configuration of task ':downloadLicenses' (type 'DownloadLicenses').
  - In plugin 'com.github.hierynomus.license-report' type 'nl.javadude.gradle.plugins.license.DownloadLicenses' property 'html' has redundant getters: 'getHtml()' and 'isHtml()'.
```
この場合は以下の設定をbuild.gradleに設定することで回避できる。（引用元：[stackoverflow](https://stackoverflow.com/questions/67393005/gradle-get-list-of-dependencies-and-licenses))
```
plugins {
  id "com.github.hierynomus.license-report" version"0.16.1"
}

/**
 * You will have to specify for which configuration dependencies should be resolved.
 * Note check in Gradle documentation about configuration types that can be resolved.
 * https://docs.gradle.org/current/userguide/declaring_dependencies.html#sec:resolvable-consumable-configs
 *
 * Following are resolvable by default:
 * - compileClasspath
 * - runtimeClasspath
 */
downloadLicenses {
  includeProjectDependencies = true
  dependencyConfiguration = 'runtimeClasspath'
  // or
  // dependencyConfiguration = 'compileClasspath'
}
```