静的コード分析を CI/CD パイプラインに統合するにはどうすればよいですか?

静的コード分析を CI/CD パイプラインに統合するにはどうすればよいですか?

インコム 2026 年 6 月 11 日

本番環境に到達するコードはすべて、それが正しいと信じてコミットされたものです。静的コード分析は、コードを実行するのではなく、その構造を読み取り、データフローを追跡し、既知の脆弱性、バグ、品質違反のパターンと比較することで、その信念が正しかったかどうかを検証する自動化システムです。問題は、静的コード分析を実行するかどうかではなく、パイプラインのどの段階で実行するか、どのツールを使用するか、どのような閾値を適用するか、そして開発者がフィードバックを無視するのではなく、実際に対応できるように、いかに迅速にフィードバックを提供するかということです。

静的解析を単なるチェックボックス作業として捉える場合と、真の品質ゲートとして機能させる場合との違いは、設定にあります。実行して誰も読まないレポートを生成するだけのスキャナは、見せかけに過ぎません。一方、新たな重大な脆弱性が発見された場合にビルドを失敗させ、カバレッジが閾値を下回った場合にマージをブロックし、プルリクエストのレビューインターフェースでファイルと行ごとの詳細なフィードバックを表示する品質ゲートは、意思決定が行われた瞬間に動作が変化するシステムです。

パイプラインのどの段階で静的解析を実行するか

静的解析は複数の箇所で実行すべきであり、それぞれ異なる目的を持つべきである。一箇所だけで実行すると盲点が生じ、あらゆる箇所で均等に実行すると、開発者が迂回するような低速なパイプラインが生成される。

パイプラインステージ何を走るかレイテンシーバジェット目的
事前コミット(ローカル)高速リンターのみ (ESLint、Clippy、rustfmt)5秒未満明らかな問題がリポジトリに反映される前に阻止する
プルリクエスト/マージリクエストフルリンティング + SAST + 増分SonarQubeスキャン3分未満ブロックは新たな重要課題について統合する
メインブランチのビルドカバレッジ、重複、技術的負債を含む詳細な分析10分未満品質トレンドを追跡し、ダッシュボードを更新する
毎晩予定詳細スキャン、依存関係監査、コンプライアンスチェック予算なし生産遅延問題とサプライチェーンリスクを把握する

最も重要な段階はプルリクエストです。メインブランチへのプッシュ時に実行される分析は、マージの決定が下された後に行われます。プルリクエスト時に実行され、正確なファイルと行のコンテキストを含むインラインコメントを投稿する分析は、開発者がまだコードに取り組んでいる段階で行われ、修正コストが最も低くなります。

保存のたびにコミット前に大規模な分析を実行すると、開発者のエクスペリエンスが著しく低下します。5秒以内に完了する高速なリンターは必要ですが、それ以外はすべてCI(継続的インテグレーション)で行うべきです。

GitHub Actions: 完全な動作構成

GitHub Actionsは、最も広く利用されているCIプラットフォームです。以下のワークフローでは、フォーマットチェック、リンティング、ビルド、SASTセキュリティスキャン、SonarCloud品質ゲートといった、階層化された品質パイプラインを実行します。

ヤムル

# .github/workflows/quality.yml
name: Code Quality

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  # ── Layer 1: Fast checks (under 60 seconds) ─────────────────────────────
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - run: npm ci

      - name: Lint (fail on warnings)
        run: npx eslint src/ --max-warnings 0

      - name: Format check
        run: npx prettier --check src/

      - name: TypeScript type check
        run: npx tsc --noEmit

  # ── Layer 2: Security SAST (Semgrep) ────────────────────────────────────
  sast:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4

      - name: Semgrep SAST scan
        uses: semgrep/semgrep-action@v1
        with:
          config: p/javascript p/nodejs p/owasp-top-ten
        env:
          SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}

  # ── Layer 3: SonarCloud quality gate ────────────────────────────────────
  sonar:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # full history required for blame data

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - run: npm ci

      - name: Run tests with coverage
        run: npm test -- --coverage --coverageReporters=lcov

      - name: SonarCloud scan
        uses: SonarSource/sonarcloud-github-action@master
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

プロパティ

# sonar-project.properties
sonar.projectKey=my-org_my-project
sonar.organization=my-org
sonar.sources=src
sonar.tests=tests
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.coverage.exclusions=**/*.test.js,**/*.spec.js

このワークフローにおける重要な構成上の決定事項:

  • --max-warnings 0 ESLintでは、どんな警告もビルドの失敗とみなされます。警告を許容するチームは、誰も読まなくなるまで警告を蓄積し続けます。
  • fetch-depth: 0 チェックアウト時: SonarCloud は、発生した問題の原因を特定し、新規コード メトリクスを正しく計算するために、完全な Git 履歴を必要とします。
  • Semgrepはlintの後にSonarと並行して実行されます。セキュリティと品質のスキャンは独立した懸念事項であるため、これらを並行して実行することでパイプライン全体の時間を短縮できます。
  • テストは以下で実行されます lcov カバレッジ出力:SonarCloudはこの出力を読み取り、ファイルごとのカバレッジを表示し、品質ゲートでカバレッジのしきい値を適用します。

GitLab CI/CD:品質ゲートを備えた品質パイプライン

ヤムル

# .gitlab-ci.yml
stages:
  - lint
  - test
  - analyze
  - security

variables:
  SONAR_HOST_URL: "https://sonarcloud.io"

lint:
  stage: lint
  image: node:20
  cache:
    paths: [node_modules/]
  script:
    - npm ci
    - npx eslint src/ --max-warnings 0
    - npx prettier --check src/
    - npx tsc --noEmit
  rules:
    - if: $CI_MERGE_REQUEST_IID
    - if: $CI_COMMIT_BRANCH == "main"

test:
  stage: test
  image: node:20
  script:
    - npm ci
    - npm test -- --coverage --coverageReporters=lcov cobertura
  coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

sonarcloud:
  stage: analyze
  image:
    name: sonarsource/sonar-scanner-cli:latest
    entrypoint: [""]
  variables:
    SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
    GIT_DEPTH: "0"
  cache:
    key: "${CI_JOB_NAME}"
    paths: [.sonar/cache]
  script:
    - sonar-scanner
  rules:
    - if: $CI_MERGE_REQUEST_IID
    - if: $CI_COMMIT_BRANCH == "main"

semgrep:
  stage: security
  image: returntocorp/semgrep:latest
  script:
    - semgrep scan --config=p/owasp-top-ten --config=p/javascript
      --sarif --output=semgrep.sarif src/
  artifacts:
    reports:
      sast: semgrep.sarif
  rules:
    - if: $CI_MERGE_REQUEST_IID
    - if: $CI_COMMIT_BRANCH == "main"

その artifacts: reports: sast: このブロックは、SemgrepからのSARIF出力をGitLabのセキュリティダッシュボードに直接統合し、検出結果を生のCIログ出力としてではなく、マージリクエストのセキュリティレポートに表示します。

Jenkins: SonarQubeを使用した宣言型パイプライン

グルービー

// Jenkinsfile
pipeline {
    agent any

    tools {
        nodejs "NodeJS-20"
    }

    environment {
        SONAR_TOKEN = credentials('sonar-token')
    }

    stages {
        stage('Lint') {
            steps {
                sh 'npm ci'
                sh 'npx eslint src/ --max-warnings 0'
                sh 'npx tsc --noEmit'
            }
        }

        stage('Test') {
            steps {
                sh 'npm test -- --coverage --coverageReporters=lcov'
            }
            post {
                always {
                    publishHTML([
                        allowMissing: false,
                        reportDir: 'coverage/lcov-report',
                        reportFiles: 'index.html',
                        reportName: 'Coverage Report'
                    ])
                }
            }
        }

        stage('SonarQube Analysis') {
            steps {
                withSonarQubeEnv('SonarQube') {
                    sh '''
                        npx sonar-scanner \
                          -Dsonar.projectKey=my-project \
                          -Dsonar.sources=src \
                          -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
                    '''
                }
            }
        }

        stage('Quality Gate') {
            steps {
                timeout(time: 5, unit: 'MINUTES') {
                    waitForQualityGate abortPipeline: true
                }
            }
        }
    }

    post {
        failure {
            emailext(
                subject: "Quality Gate FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                body: "Build failed quality gate. Review: ${env.BUILD_URL}",
                to: "${env.CHANGE_AUTHOR_EMAIL}"
            )
        }
    }
}

waitForQualityGate abortPipeline: true これは重要な行です。分析が完了するまでSonarQubeをポーリングし、品質ゲートが満たされない場合はビルドを失敗させます。これがないと、分析がまだ実行中の状態でパイプラインが完了し、品質ゲートの結果が適用されなくなります。

チームが実際に尊重する品質ゲートを設定する

しきい値の設定が厳しすぎるためにコミットごとに失敗する品質ゲートは無効化される。一方、しきい値が緩すぎるために決して失敗しない品質ゲートは価値がない。目標は、プロジェクトの実際のリスクプロファイルに合わせて調整されたゲートを設定することである。

SonarQube品質ゲートを新規に導入する際の推奨初期設定:

メトリック状態しきい値
新しいバグ越える0
新たな脆弱性越える0
新たなセキュリティ上のホットスポットを検証以下100%
新しいコードカバレッジ以下80%
新しい重複行越える3%
新しいコードの臭い越える10

これらのしきい値は新規コード(SonarQubeの「新規コード」期間)にのみ適用してください。レガシープロジェクトのコードベース全体に適用しないでください。5年間蓄積された技術的負債のためにビルドが毎回失敗すると、チームはゲートを完全に無効にしてしまう可能性があります。新規コードアプローチを採用することで、既存の負債を別途対処しながら、問題の発生を食い止めることができます。

段階的に基準値を引き上げていきましょう。最初は控えめな基準値から始め、チームがバックログを解消していくにつれて、四半期ごとに基準値を厳しくしていきます。例えば、現在75%のカバー率で合格しているゲートは、ベースラインが80%を十分に上回った時点で、80%に引き上げることができます。完璧を目指すよりも、段階的に基準値を厳しくしていく方が持続可能です。

大規模コードベースにおける静的解析パフォーマンスの処理

チームがCIで静的解析を無効化したり回避したりする最も一般的な理由は、パイプラインの処理速度が遅くなりすぎるためです。フィーチャーブランチへのプッシュごとに15分もの品質スキャンを実行すると、開発者の作業フローが阻害されます。解析を高速化するための戦略はいくつかあります。

増分分析(新規コードのみ)。 SonarQube およびほとんどのエンタープライズ静的解析ツールは、コードベース全体ではなく、プルリクエスト内の変更されたファイルのみを分析することをサポートしています。 sonar.pullrequest.base, sonar.pullrequest.branch, sonar.pullrequest.key PR固有の増分スキャンを有効にするパラメータ。メインブランチへのマージ時には引き続きフル分析が実行されます。PRスキャンはほとんどのコードベースで2分以内に完了します。

キャッシュ。多くの静的解析実行において最もコストがかかるのは、ツールとそのルール定義のダウンロードです。スキャナーバイナリ、ルールデータベース、および解決済みの依存関係を、実行間でキャッシュします。

ヤムル

# GitHub Actions: cache SonarCloud scanner
- name: Cache SonarCloud packages
  uses: actions/cache@v4
  with:
    path: ~/.sonar/cache
    key: ${{ runner.os }}-sonar
    restore-keys: ${{ runner.os }}-sonar

並列処理。リンティング、セキュリティスキャン、品質ゲート分析はそれぞれ独立した処理です。これらを順次実行するのではなく、並列ジョブとして実行してください。パイプライン全体の実行時間は、すべてのジョブの合計ではなく、最も時間のかかるジョブによって決まります。

ツールの選択的有効化。すべてのツールをすべてのブランチで実行する必要はありません。プルリクエストとメインブランチでは完全なセキュリティスキャンとコンプライアンスチェックを実行し、マージ前のブランチでは高速リンターのみを実行できます。CI構成でブランチフィルタを使用して、異なる分析プロファイルを適用します。

セキュリティ優先パイプラインにおける静的解析:SASTと依存関係スキャン

セキュリティ重視のパイプラインは、標準的な品質分析に加えて、アプリケーションコードの脆弱性を検出するためのSAST(静的アプリケーションセキュリティテスト)と、サードパーティライブラリにおける既知のCVE(共通脆弱性識別子)を検出するための依存関係スキャンという2つのレイヤーを追加します。

SASTツール(Semgrep、CodeQL、Snyk Code、Checkmarxなど)は、信頼できないデータがアプリケーションコードをどのように流れて機密性の高い操作に到達するかを分析します。これらのツールは、標準的な品質リンターでは検出できないSQLインジェクション、XSS、コマンドインジェクション、安全でない逆シリアル化、ハードコードされた認証情報などを検出します。

ヤムル

# GitHub Actions: CodeQL analysis for deep vulnerability detection
- name: Initialize CodeQL
  uses: github/codeql-action/init@v3
  with:
    languages: javascript-typescript

- name: Perform CodeQL Analysis
  uses: github/codeql-action/analyze@v3
  with:
    category: "/language:javascript-typescript"

依存関係スキャン チェック package.json, pom.xml, requirements.txt、および脆弱性データベースに対する同等のマニフェストファイル:

ヤムル

# Dependency audit in the pipeline
- name: Dependency audit
  run: |
    npm audit --audit-level=high
    # Fail if high or critical vulnerabilities are found

階層型セキュリティモデルでは、開発者への即時フィードバックのために、すべてのプルリクエストに対して高速なパターンベースのSAST(Semgrep)を実行し、徹底的なカバレッジのために定期的に詳細なセマンティックSAST(CodeQL)を実行します。新しいCVEが毎日公開されるため、依存関係のスキャンはすべてのビルドで実行されます。

よくある統合ミスとその回避方法

メインブランチでのみ分析を実行します。静的分析の真価は、マージ後ではなく、マージ前に問題を検出することにあります。マージ後にのみ実行される分析は、ゲートではなくレポートとして機能します。

品質ゲートを失敗ではなく警告に設定する。パイプラインをブロックしない警告は、開発者が無視するようになる通知だ。失敗しないゲートでは、品質文化は構築されない。

テストファイルをカバレッジしきい値から除外しないと、テストコードが他のテストコードをテストすることでカバレッジ値が過大評価されます。本番コードのカバレッジを正確に測定するには、テストディレクトリをカバレッジ計算から除外してください。

既存コードベースの分析バックログを無視するのは危険です。100万行にも及ぶ既存コードベースで初めてSonarQubeを起動し、4万件もの課題が見つかるのは、モチベーションを低下させ、逆効果です。新規コードベースラインアプローチを採用しましょう。「新規コード」の開始日を定義し、その日以降に書かれたコードにのみゲートを適用し、既存の課題バックログは別の技術的負債削減プログラムとして扱います。

結果をプルリクエストのコメントに書き込まない。 開発者がアクセスする必要のある別のダッシュボードに投稿された分析結果は無視されます。プルリクエストレビューインターフェース内で、問題を引き起こした特定の行にインラインコメントとして投稿された分析結果は対処されます。設定 sonar.pullrequest.* GitHub、GitLab、またはBitbucketとの連携に関するパラメータを設定することで、コードレビューが行われる場所に発見事項が表示されるようになります。

認定条件 SMART TS XL CI/CDパイプラインと統合

CI/CDパイプラインで使用される静的解析ツールのほとんどは、一度に1つの言語しか認識しません。ESLintはJavaScriptを認識します。SonarQubeはJava、Python、JavaScript、C#を認識します。しかし、COBOL、JCL、RPG、あるいはメインフレームのバッチプログラムと、その出力を処理するJavaマイクロサービス、そしてそれを表示するJavaScriptフロントエンドを接続するような、言語間の依存関係は、これらのツールでは認識されません。

SMART TS XL CI/CD パイプラインに、企業環境のすべての言語を同時にカバーするクロス言語静的解析レイヤーとして統合されます。Java サービスが変更されると、 SMART TS XLさん 影響分析 変更が展開される前に、依存関係グラフをトレースして、どの COBOL プログラム、データベース スキーマ、および下流サービスがその変更の影響を受けるかを特定します。COBOL コピーブックが変更されると、そのコピーブックを含むアプリケーション ポートフォリオ全体にわたる、検証が必要なすべてのプログラムを特定します。

この言語間の依存関係の認識は、 SMART TS XLの CI/CD 統合は、SonarQube に別の言語を追加するのとは異なります。変更されたファイルのコードを分析するだけではなく、その変更がシステム内の他の部分にどのような影響を与えるかを理解することが重要です。つまり、変更を行う前に、変更の真の範囲を決定するアーキテクチャ分析を行う必要があるのです。

レガシースタックとモダンスタックの両方で運用するエンタープライズ開発チームの場合、 SMART TS XLさん DevOps統合 この機能は、構造分析をパイプラインレビューのワークフローに組み込むことで、言語固有のツールでは実現できないアーキテクチャの可視性を提供します。その結果、言語の境界内だけでなくシステム全体にわたって標準を強制する品質ゲートが構築され、いずれかのコンポーネントの変更が、それに依存するコンポーネントに予期せぬ障害を引き起こさないことが保証されます。

静的解析をパイプラインの恒久的な構成要素として位置づける

静的解析を正しく実践しているチームは、テストと同様に、静的解析を単なる通過点としてではなく、すべてのコミットに組み込まれた継続的な実践として捉えています。解析はすべてのプルリクエストで実行され、品質ゲートの結果はコードレビューと連動して表示されます。コードベースの改善に伴い、しきい値は厳しくなり、チームのセキュリティと品質の成熟度が高まるにつれて、新たな解析カテゴリが追加されます。

この記事で紹介するパイプライン構成テンプレートはあくまで出発点です。具体的なツール、しきい値、ゲート条件は、プロジェクトの言語スタック、リスクプロファイル、チームの成熟度に合わせて調整する必要があります。ただし、以下の原則は変えてはなりません。マージをゲートしない分析は動作を変えず、コード品質プログラムが変えようとしているのはまさにその動作なのです。