RailsアプリでのOSSライセンスチェックを自動化してみた

## はじめに

[DENSO Advent Calendar 2024](https://qiita.com/advent-calendar/2024/denso) の 12 日目の記事です。

Rails アプリケーション開発では、日々新たな Gem を導入し機能を拡張していきます。その中で、利用しているライブラリがどのような OSS ライセンスで提供されているかを常時把握することは、意外と見落とされがちです。

後からコンプライアンス上問題のあるライセンス（GPL、AGPL、SSPL など商用環境で制限がかかるもの）を混入させてしまったと分かった場合、後から外すコストが発生したり、ビジネス上の制約が生まれるリスクがあったりします。

上記の懸念から、Rails プロジェクトで OSS ライセンスチェックを自動化したいなと考えていました。今回は、CI パイプライン上で定常的にライセンスチェックを実施し、潜在的なライセンス問題を即座に検知してパイプラインを失敗させる手法について紹介したいと思います。

## [LicenseFinder](https://github.com/pivotal/LicenseFinder) とは

### 概要

LicenseFinder は、Ruby を中心とした環境でよく用いられる OSS ライセンスチェックツールです。Gemfile.lock をもとにアプリケーションが利用している Gem のライセンスを収集し、指定した許可ライセンス（whitelist）・禁止ライセンス（blacklist）に基づいて問題点を洗い出します。

### 主な特徴

- gem をインストールするだけですぐに利用可能
- ライセンス許可・非許可の設定を YAML ファイルでコード管理できる

## 導入方法

### 1. Gem のインストール

```bash
# 開発ツール群用にGemfileに追加
group :development do
 gem 'license_finder', require: false
end
```

```
bundle install
```

### 2. 実行

LicenseFinder を実行します。

```bash
bundle exec license_finder
```

これにより、利用中の Gem とそのライセンス一覧を取得します。
RailsTutorial のコードを格納したレポジトリで実行すると以下の出力になりました。
現状はどのライセンスも許可していないためすべて拒否されている状況になっています。

```bash
LicenseFinder::Bundler: is active for '/workspaces/rails-tutorial-7'

Dependencies that need approval:
actioncable, 7.0.4.3, MIT
actionmailbox, 7.0.4.3, MIT
actionmailer, 7.0.4.3, MIT
actionpack, 7.0.4.3, MIT
actiontext, 7.0.4.3, MIT
actionview, 7.0.4.3, MIT
activejob, 7.0.4.3, MIT
activemodel, 7.0.4.3, MIT
activerecord, 7.0.4.3, MIT
activestorage, 7.0.4.3, MIT
activesupport, 7.0.4.3, MIT
addressable, 2.8.7, "Apache 2.0"
ansi, 1.5.0, "Simplified BSD"
ast, 2.4.2, MIT
autoprefixer-rails, 10.4.19.0, MIT
backport, 1.2.0, MIT
bcrypt, 3.1.18, MIT
benchmark, 0.3.0, "Simplified BSD, ruby"
bindex, 0.8.1, MIT
bootsnap, 1.16.0, MIT
bootstrap-sass, 3.4.1, MIT
builder, 3.3.0, MIT
bundler, 2.4.19, MIT
capybara, 3.38.0, MIT
coderay, 1.1.3, MIT
concurrent-ruby, 1.3.4, MIT
crass, 1.0.6, MIT
csv, 3.3.0, "Simplified BSD, ruby"
date, 3.3.4, "Simplified BSD, ruby"
debug, 1.7.1, "Simplified BSD, ruby"
diff-lcs, 1.5.1, "Artistic-2.0, GPL-2.0-or-later, MIT"
e2mmap, 0.1.0, "Simplified BSD"
erubi, 1.13.0, MIT
execjs, 2.10.0, MIT
ffi, 1.17.0, "New BSD"
formatador, 1.1.0, MIT
globalid, 1.2.1, MIT
guard, 2.18.0, MIT
guard-compat, 1.2.1, MIT
guard-minitest, 2.4.6, MIT
i18n, 1.14.5, MIT
importmap-rails, 1.1.5, MIT
io-console, 0.7.2, "Simplified BSD, ruby"
irb, 1.10.0, "Simplified BSD, ruby"
jaro_winkler, 1.6.0, MIT
jbuilder, 2.11.5, MIT
json, 2.7.2, ruby
kramdown, 2.4.0, MIT
kramdown-parser-gfm, 1.1.0, MIT
language_server-protocol, 3.17.0.3, MIT
license_finder, 7.2.1, MIT
listen, 3.9.0, MIT
loofah, 2.22.0, MIT
lumberjack, 1.2.10, MIT
mail, 2.8.1, MIT
marcel, 1.0.4, "Apache 2.0, MIT"
matrix, 0.4.2, "Simplified BSD, ruby"
method_source, 1.1.0, MIT
mini_mime, 1.1.5, MIT
minitest, 5.18.0, MIT
minitest-reporters, 1.6.0, MIT
msgpack, 1.7.2, "Apache 2.0"
nenv, 0.3.0, MIT
net-imap, 0.4.14, "Simplified BSD, ruby"
net-pop, 0.1.2, "Simplified BSD, ruby"
net-protocol, 0.2.2, "Simplified BSD, ruby"
net-smtp, 0.5.0, "Simplified BSD, ruby"
nio4r, 2.7.3, "MIT, Simplified BSD"
nokogiri, 1.16.7, MIT
notiffany, 0.1.3, MIT
parallel, 1.26.3, MIT
parser, 3.3.4.2, MIT
prism, 0.19.0, MIT
pry, 0.14.2, MIT
psych, 5.1.2, MIT
public_suffix, 6.0.1, MIT
puma, 5.6.8, "New BSD"
racc, 1.8.1, "Simplified BSD, ruby"
rack, 2.2.9, MIT
rack-test, 2.1.0, MIT
rails, 7.0.4.3, MIT
rails-controller-testing, 1.0.5, MIT
rails-dom-testing, 2.2.0, MIT
rails-html-sanitizer, 1.6.0, MIT
railties, 7.0.4.3, MIT
rainbow, 3.1.1, MIT
rake, 13.2.1, MIT
rb-fsevent, 0.11.2, MIT
rb-inotify, 0.11.1, MIT
rbs, 2.8.4, "Simplified BSD, ruby"
rdoc, 6.7.0, ruby
regexp_parser, 2.9.2, MIT
reline, 0.5.9, ruby
repl_type_completor, 0.1.2, MIT
reverse_markdown, 2.1.1, WTFPL
rexml, 3.3.6, "Simplified BSD"
rubocop, 1.65.1, MIT
rubocop-ast, 1.32.1, MIT
ruby-progressbar, 1.13.0, MIT
rubyzip, 2.3.2, "Simplified BSD"
sassc, 2.4.0, MIT
sassc-rails, 2.1.2, MIT
selenium-webdriver, 4.8.3, "Apache 2.0"
shellany, 0.0.1, MIT
solargraph, 0.50.0, MIT
sprockets, 4.2.1, MIT
sprockets-rails, 3.4.2, MIT
sqlite3, 1.6.1, "New BSD"
stimulus-rails, 1.2.1, MIT
stringio, 3.1.1, "Simplified BSD, ruby"
strscan, 3.1.0, "Simplified BSD, ruby"
thor, 1.3.1, MIT
tilt, 2.4.0, MIT
timeout, 0.4.1, "Simplified BSD, ruby"
tomlrb, 2.0.3, MIT
turbo-rails, 1.4.0, MIT
tzinfo, 2.0.6, MIT
unicode-display_width, 2.5.0, MIT
web-console, 4.2.0, MIT
webdrivers, 5.2.0, MIT
websocket, 1.2.11, MIT
websocket-driver, 0.7.6, "Apache 2.0"
websocket-extensions, 0.1.5, "Apache 2.0"
with_env, 1.1.0, MIT
xml-simple, 1.1.9, MIT
xpath, 3.2.0, MIT
yard, 0.9.36, MIT
zeitwerk, 2.6.17, MIT
```

### 3. 許可/非許可ライセンスの設定

許可するライセンスや禁止するライセンスはコマンドで追加/削除できます。たとえば MIT を許可したい場合は以下のコマンドを実行します。

```bash
bundle exec license_finder permitted_licenses add MIT
```

すると以下の様にライセンスの許可を完了した旨が表示されます。

```bash
Added MIT to the permitted licenses
```

これにより `/doc/dependency_decisions.yml` が作成され、許可するライセンス条件が追記されました。

```yaml
---
- - :permit
  - MIT
  - :who:
    :why:
    :versions: []
    :when: 2024-12-10 13:51:59.593556797 Z
```

次に例えば GPL を非許可と設定すると以下の様に変化します。

```bash
bundle exec license_finder restricted_licenses add GPL
```

```yaml
---
- - :permit
  - MIT
  - :who:
    :why:
    :versions: []
    :when: 2024-12-10 13:51:59.593556797 Z
- - :restrict
  - GPL
  - :who:
    :why:
    :versions: []
    :when: 2024-12-10 13:56:03.974654424 Z
```

この状態で `bundle exec license_finder` を実行すると以下の出力に変わります。

```bash
LicenseFinder::Bundler: is active for '/workspaces/rails-tutorial-7'

Dependencies that need approval:
addressable, 2.8.7, "Apache 2.0"
ansi, 1.5.0, "Simplified BSD"
benchmark, 0.3.0, "Simplified BSD, ruby"
csv, 3.3.0, "Simplified BSD, ruby"
date, 3.3.4, "Simplified BSD, ruby"
debug, 1.7.1, "Simplified BSD, ruby"
e2mmap, 0.1.0, "Simplified BSD"
ffi, 1.17.0, "New BSD"
io-console, 0.7.2, "Simplified BSD, ruby"
irb, 1.10.0, "Simplified BSD, ruby"
json, 2.7.2, ruby
matrix, 0.4.2, "Simplified BSD, ruby"
msgpack, 1.7.2, "Apache 2.0"
net-imap, 0.4.14, "Simplified BSD, ruby"
net-pop, 0.1.2, "Simplified BSD, ruby"
net-protocol, 0.2.2, "Simplified BSD, ruby"
net-smtp, 0.5.0, "Simplified BSD, ruby"
puma, 5.6.8, "New BSD"
racc, 1.8.1, "Simplified BSD, ruby"
rbs, 2.8.4, "Simplified BSD, ruby"
rdoc, 6.7.0, ruby
reline, 0.5.9, ruby
reverse_markdown, 2.1.1, WTFPL
rexml, 3.3.6, "Simplified BSD"
rubyzip, 2.3.2, "Simplified BSD"
selenium-webdriver, 4.8.3, "Apache 2.0"
sqlite3, 1.6.1, "New BSD"
stringio, 3.1.1, "Simplified BSD, ruby"
strscan, 3.1.0, "Simplified BSD, ruby"
timeout, 0.4.1, "Simplified BSD, ruby"
websocket-driver, 0.7.6, "Apache 2.0"
websocket-extensions, 0.1.5, "Apache 2.0"
```

MIT のライセンスが許可され、許可が必要なライセンスのリストに挙がってこなくなったのが分かります。このコマンド出力で未許可のライセンスリストがなくなると以下の出力に変わります。

```bash
All dependencies are approved for use
```

この状態であれば、CI は失敗せずに完了します。逆に違反ライセンスを含んでいたり、未許可のライセンスを含んでいる場合には CI は失敗してパイプラインを終了させることができます。

### 4. CI への組み込み

ライセンスチェックを CI に組み込むことで、Pull Request ごとにライセンス問題がないか自動検証できます。以下は GitHub Actions の例です。

```yaml
name: CI
on: [push, pull_request]

jobs:
build_and_test:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4

    - name: Set up Ruby
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: "3.3.4"

    - name: Install dependencies
      run: bundle install --jobs 4 --retry 3

    - name: Check licenses
      run: bundle exec license_finder
# LicenseFinderがNGライセンスを発見した場合、コマンドは失敗コードを返し、CIがRedになる
```

ここで、license_finder が禁止ライセンスを検知すれば、コマンドは非ゼロ終了コードを返し、CI ジョブが失敗（Red ビルド）します。これにより、問題のある依存関係がマージされる前にブロックできます。

## トラブルシューティングとベストプラクティス

- 不明瞭なライセンスへの対処: `/doc/dependency_decisions.yml` で明示的に承認（approve）を行い、チーム内で確認します
- ポリシー設定: コントリビューションガイドラインやチーム内ルールドキュメントで、導入可能なライセンス条件を明文化しておくとスムーズです

## 今後の展望

今回の例では、LicenseFinder + GitHub Actions という組み合わせで OSS ライセンスチェックを自動化する手法を紹介しました。これは小規模な Rails アプリケーションや、Gem 中心のワークフローに適したシンプルで効果的なアプローチです。

しかし、プロジェクトが成長したり、別の言語やパッケージマネージャを取り込んだり、より複雑なライセンス管理ニーズが発生した場合、他のツールを検討する必要が出てくるかもしれません。

**licensed**
GitHub 製のツールで、LicenseFinder 同様 Ruby だけでなく npm にも対応可能。Python, Golang の依存関係を対象としたチェックも可能なため、一度習得すれば別プロジェクトでも使える知見になる可能性が高そう。GitHub Actions との相性も良好。

**FOSSA**
商用の SaaS ツールで、ライセンスチェックに加えセキュリティスキャン、法務向けの包括的なダッシュボード、ポリシー管理などが充実。大規模・複雑なプロジェクトや、コンプライアンス要件が厳しい企業で重宝されそう。

このようなツールの存在を知っておくことで、将来的なプロジェクト拡大や多言語化に備えたスケーラビリティを確保できます。最初は LicenseFinder で始めて、必要に応じて licensed や FOSSA に移行・拡張する戦略も十分考えられるでしょう。

## まとめ

本記事では、Rails アプリ開発における OSS ライセンスチェックを LicenseFinder で実施し、GitHub Actions に組み込む具体的な手法を解説しました。これにより、問題のあるライセンスを早期に発見し、コンプライアンスリスクを低減できます。

将来的には、プロジェクト規模や要件の変化に合わせて、licensed や FOSSA といった他のツールの検討も視野に入れるとよいでしょう。まずは LicenseFinder で簡易な仕組みを構築し、必要なときに段階的なアップグレードを行うことで、長期的な OSS ライセンス管理戦略を築いていくことが可能です。