こんにちは、テクマトリックスの酒井です。

昨今の多くのソフトウェアは OSS を利用して開発されています。その一方で、意図しないコード流用やライセンス違反のリスクが年々無視できなくなっています。これまではリリース直前にまとめて OSS チェックを行う運用も多く見られましたが、その場合、修正コストが高く、原因調査も困難になりがちです。

そこで本記事では、FossID を Jenkins に組み込み、OSS チェックを CI/CD パイプラインの一部として自動化してみた事例を紹介します。

なぜ FossID + Jenkins なのか

今回 FossID を Jenkins に組み込んだ目的はシンプルです。

  • OSS 混入をできるだけ早い段階で検知したい
  • 人手によるチェックを減らし、属人性を排除したい
  • 開発者が自然に OSS リスクを意識できる仕組みを作りたい
  • リリース候補のビルド時に SBOM も生成したい

Jenkins は現在でも弊社のお客様に非常に多く利用されている CI/CD ツールであり、パイプラインの中に FossID を組み込むことで「いつもの開発フローの延長線」として OSS 管理を行うことが狙いです。

全体構成

アプリケーションの構成は次のようになります。

構成図

ソースコードリポジトリを更新するたびに、Jenkins でパイプラインが自動的に実行されます。リポジトリの操作内容に応じてパイプライン内の処理を切り替えています。

変更用ブランチへのプッシュおよびプルリクエスト時の処理

変更用ブランチへのプッシュおよびプルリクエスト時には、次の処理を実行します。

  1. ソースコードをチェックアウト
  2. ビルドやテストを実行
  3. FossID Toolbox の diffscan コマンドを実行
  4. ライセンスポリシー違反またはセキュリティ脆弱性が検出されればパイプラインを失敗にする

FossID Toolbox の diffscan コマンドは、 base と compare の 2 つのコミットを比較し、ライセンスポリシー違反やセキュリティ脆弱性を含んだコードが検出された場合にパイプラインを失敗させることができます。ライセンスポリシーを指定するには、利用を許可するライセンスをリポジトリルートの .fossidpolicy ファイルに次のように記述します。

[
  {
    "id":"MIT",
    "blocked": false,
    "reason": "Permissive License"
  },
  {
    "id":"Apache-2.0",
    "blocked": false,
    "reason": "Permissive License"
  }
]

メインブランチへのマージ時およびタグ作成時の処理

メインブランチへのマージ時およびタグ作成時には、次の処理を実行します。

  1. ソースコードをチェックアウト
  2. ビルドやテストを実行
  3. FossID Workbench にソースコードをアップロードしてスキャンを実行
  4. 「識別待ち(※)」が存在する場合は識別が完了するのを待機(人が FossID Workbench で「識別」を実施後にパイプラインを続行)
  5. 次のいずれかであればパイプラインを失敗
    • 深刻度が MEDIUM またはより深刻な脆弱性が検出された
    • 「識別待ち」が存在する
    • ライセンスポリシー違反が存在する
  6. SBOM を生成
  7. バイナリ等と SBOM を配布場所に保存

(※)オープンソースにマッチしていてもクリア済みでない(識別済みとしてマークされていない)ファイル。

ソースコードを FossID Workbench にアップロードしてスキャンを実行することで、 FossID Workbench 上でメインブランチやタグのスキャン結果を確認・識別できます。さらにスキャン後に SBOM を生成できます。ただし、ソースコード全体をスキャンするため diffscan コマンドと比べて時間がかかります。

パイプラインコードの例

実行可能な Jenkinsfile の例を以下に記します。「OSSスキャン」ステージではプルリクエスト等で diffscan コマンドを実行します。「SBOM生成」ステージではメインブランチにマージした時等で FossID Workbench でスキャンを実行し、最終的に SBOM を生成します。

pipeline {
    agent none
    stages {
        stage('ビルド') {
            agent any
            steps {
                sh './scripts/build.sh'
            }
            post {
                success {
                    archiveArtifacts artifacts: 'ci-demo.out'
                    stash name: 'binaries', includes: 'ci-demo.out'
                }
            }
        }
        stage('テスト') {
            parallel {
                stage('単体テスト') {
                    agent any
                    steps {
                        sh './scripts/unit-tests.sh'
                    }
                }
                stage('静的解析') {
                    agent any
                    steps {
                        sh './scripts/static-analysis.sh'
                    }
                }
                stage('OSSスキャン') {
                    when {
                        beforeAgent true
                        not {
                            anyOf {
                                branch 'main'
                                buildingTag()
                            }
                        }
                    }
                    agent {
                        dockerfile {
                            dir 'jenkins/fossid/fossid-toolbox'
                            registryUrl 'https://quay.io'
                            registryCredentialsId 'fossid-quay-user-password'
                            args '--entrypoint='
                        }
                    }
                    environment {
                        FOSSID_HOST = 'fossid-workbench.example.com'
                        FOSSID_TOKEN = credentials('fossid-cli-token')
                        HOME = '/tmp'
                    }
                    steps {
                        sh label: 'FossID DiffScanを実行', script: '''
                            fossid \
                                diffscan \
                                --base-ref "${CHANGE_TARGET:-main}" \
                                --compare-ref HEAD \
                                --license-mode new \
                                --vsf-mode new \
                                --format json \
                                --fail \
                                > fossid-diffscan.json
                        '''
                    }
                    post {
                        always {
                            sh label: 'レポートを変換', script: '''
                                apk add --no-cache python3
                                python jenkins/fossid/convert_diffscan_report.py --input fossid-diffscan.json --output fossid-issues.json
                            '''
                            recordIssues tool: issues(id: 'fossid', name: 'FossID', pattern: 'fossid-issues.json'), enabledForFailure: true
                        }
                    }
                }
            }
        }
        stage('SBOM生成') {
            when {
                beforeAgent true
                anyOf {
                    branch 'main'
                    buildingTag()
                }
            }
            agent {
                docker {
                    image 'ghcr.io/tomgonzo/workbench-cli:latest'
                    args '--entrypoint='
                }
            }
            environment {
                GIT_REPOSITORY_OWNER   = "${GIT_URL.replaceFirst('^.*/([^/]+)/[^/]+?(?:\\.git)?$', '$1')}"
                GIT_REPOSITORY_NAME    = "${GIT_URL.replaceFirst('^.*/([^/]+?)(?:\\.git)?$', '$1')}"
                WORKBENCH_URL          = 'https://fossid-workbench.example.com/'
                WORKBENCH_CRED         = credentials('fossid-workbench-user-token')
                WORKBENCH_USER         = "${WORKBENCH_CRED_USR}"
                WORKBENCH_TOKEN        = "${WORKBENCH_CRED_PSW}"
                WORKBENCH_PROJECT_NAME = "${GIT_REPOSITORY_OWNER}"
                WORKBENCH_SCAN_NAME    = "${GIT_REPOSITORY_OWNER}/${GIT_REPOSITORY_NAME}@${GIT_BRANCH}"
            }
            steps {
                sh label: 'スキャンを実行・自動識別', script: '''
                    rm -f fossid-scan-result.json
                    workbench-cli \
                        scan \
                        --project-name "${WORKBENCH_PROJECT_NAME}" \
                        --scan-name "${WORKBENCH_SCAN_NAME}" \
                        --path . \
                        --id-reuse \
                        --id-reuse-type project \
                        --id-reuse-source "${WORKBENCH_PROJECT_NAME}" \
                        --run-dependency-analysis \
                        --delta-scan \
                        --show-scan-metrics \
                        --path-result fossid-scan-result.json
                '''
                <em>// 未識別があれば識別を待機</em>
                script {
                    def pending = sh label: '未識別の数を取得', returnStdout: true, script: '''\
                        python -c 'with open("fossid-scan-result.json") as f: import json; print(json.load(f)["scan_metrics"]["pending_identification"])'
                        '''
                    if (pending.trim() != '0') {
                        timeout(time: 1, unit: 'DAYS') {
                            input message: '未識別のコンポーネントが存在します。FossID Workbenchで識別を完了してから「続行」をクリックしてください。', ok: '続行'
                        }
                    }
                }
                sh label: '自動品質ゲート', script: '''
                    workbench-cli \
                        evaluate-gates \
                        --project-name "${WORKBENCH_PROJECT_NAME}" \
                        --scan-name "${WORKBENCH_SCAN_NAME}" \
                        --fail-on-vuln-severity medium \
                        --fail-on-pending \
                        --fail-on-policy
                '''
                sh label: 'レポートを生成', script: '''
                    rm -rf fossid-reports/
                    workbench-cli \
                        download-reports \
                        --project-name "${WORKBENCH_PROJECT_NAME}" \
                        --scan-name "${WORKBENCH_SCAN_NAME}" \
                        --report-save-path fossid-reports/
                '''
            }
            post {
                success {
                    archiveArtifacts artifacts: 'fossid-reports/'
                    stash name: 'fossid-reports', includes: 'fossid-reports/'
                }
            }
        }
        stage('パッケージング') {
            when {
                beforeAgent true
                anyOf {
                    branch 'main'
                    buildingTag()
                }
            }
            agent any
            steps {
                unstash 'binaries'
                unstash 'fossid-reports'
                sh label: 'パッケージを生成', script: './scripts/packaging.sh'
            }
            post {
                success {
                    archiveArtifacts artifacts: 'ci-demo-*.tar.gz, ci-demo-*.tar.gz.*'
                }
            }
        }
    }
}

なお OSS スキャンのステージ内の処理は、jenkins/fossid/fossid-toolbox/Dockerfile ファイルに基づくコンテナで実行しています。このコンテナは、FossID 社が提供する FossID Toolbox のコンテナイメージに、 FossID Toolbox の diffscan コマンドの出力を Jenkins で読み込むための変換スクリプトを実行するための Python の実行環境を追加しているだけです。Dockerfile の内容を以下に記します。

# FossID Toolbox の最新バージョン
FROM quay.io/fossid/fossid-toolbox:latest

# Pythonをインストール
RUN apk add --no-cache python3

実行結果

変更用ブランチで変更を加える

変更用のブランチを作成し、ライセンスポリシー違反や脆弱性を含むソースコードをプッシュしてプルリクエストを作成すると、パイプラインは diffscan の実行箇所で失敗します。 Jenkins のジョブの実行結果の画面では、ライセンスポリシー違反や脆弱性の検出の様子を確認できます。

プルリクエスト時のパイプラインの実行結果
プルリクエスト時に diffscan で検出された問題の詳細

検出された問題を修正すれば、もちろんパイプラインは成功し、メインブランチへのマージの準備が整います。

メインブランチにマージする

プルリクエストをメインブランチにマージすると、FossID Workbench でスキャンが実行されます。スキャン結果に「識別待ち」が存在する場合、パイプラインは一時停止し、人が FossID Workbench で「識別待ち」を対応してからパイプラインを再開します。ライセンスポリシー違反またはセキュリティ脆弱性が検出されなければ、SBOM が生成されてバイナリ等とともに配布できるようアーカイブされます。

マージ時のパイプラインの実行結果
マージ時の成果物

バイナリ等とともに SBOM を提供することにより、リリース後も SBOM の情報を基に適切な脆弱性対応、ライセンスコンプライアンス対応が可能になります。

まとめ

FossID は CI/CD と自然に統合できます。今回は FossID と Jenkins の統合の例をご説明しました。FossID Toolbox の diffscan と FossID Workbench でのスキャンを使い分けることで、開発者の負担を抑えつつ OSS コンプライアンスを継続的に担保できます。

要望によっては、変更用ブランチも FossID Workbench でスキャンして結果を Workbench で参照できるようにしたり、パイプラインの実行中に人が「識別」作業を実施する代わりに「自動識別」を有効にしてさらに自動化を推進したりすることも可能です。この記事が読者の方の OSS の管理とソフトウェア開発のお役に立てれば幸いです。

By tsakai

Jenkins関連のサービスやCloudBees製品を主に担当しています。 Certified CloudBees Jenkins Engineer (CCJE) および CloudBees CI DevOps Associate です。