👁

SAST概要

この章では、SASTツールSemgrepを使ってCI/CDパイプラインでソースコードの脆弱性を自動検出する仕組みの構築をハンズオン形式で学習します。これにより、PR時のマージ前チェックとして脆弱性を検知する運用ができるようになります。

1. 事前準備

1.1 前提となる講座

この章では、以下の知識を前提としています。自信がない場合は先に関連講座を実施してみましょう。

講座名 必要な知識
Git入門 リポジトリ作成、コミット、Pushなど、Gitの基本操作
GitHub Actionsの概要 GitHub Actionsの基本構造、ワークフローファイルの読み方
GitHub Actionsで簡単なワークフローを作ろう ワークフローファイルの作成・実行と結果の確認方法

1.2 必要なツール

この章では、以下のツールを使用します。まだインストールしていない場合は、リンク先の手順に沿って準備をお願いします。

ツール名 関連箇所 理由
Visual Studio Code Visual Studio Codeのインストール ワークフローファイルとサンプルコードを編集するエディタとして使用する
Git Git/GitHubのセットアップ ワークフローファイルをGitHubに反映するために使用する

1.3 必要なアカウント

この章では、以下のアカウントを使用します。まだ用意していない場合は、リンク先の手順に沿って準備をお願いします。

アカウント名 関連箇所 理由
GitHubアカウント Git/GitHubのセットアップ SAST用ワークフローを動かすリポジトリのホスティング先として使用する

2. ハンズオンの概要

DevSecOpsの基本で、SASTがソースコードを静的に解析してSQLインジェクション・XSS・ハードコード認証情報などの脆弱性パターンを検出する手法であることを学びました。このセクションでは、代表的なSASTツール Semgrep を使い、実際にGitHub Actionsのワークフローに組み込んで動作を確認します。

2.1 Semgrepとは

Semgrepは、Semgrep, Inc.が開発したオープンソースのSASTツールです。30以上の言語に対応しており、OWASP Top 10やCWEに準拠した豊富なルールセットを持っています。

Semgrepの特徴:

  • Python / JavaScript / TypeScript / Go / Java / Ruby / PHP / C# など30言語以上をサポートしている
  • YAMLでカスタムルールを書けるため、社内ルールへの拡張が容易にできる
  • semgrep/semgrep 公式 Docker イメージが提供されており、GitHub Actions のジョブのコンテナとして指定するだけでワークフローに組み込める
  • --config auto オプションで、検知対象の言語を自動判定して適切なルールセットを適用してくれる

詳しい仕様はSemgrep 公式ドキュメントに記載があります。

📝 SASTと静的解析の違い
Pythonで静的解析をしようで学んだFlake8のような静的解析ツールは「コードスタイル」のチェックが主目的です。Semgrepも静的解析ツールですが、「セキュリティ脆弱性」の検出に特化している点が異なります。両方を併用することで、品質とセキュリティを並行してチェックできます。

2.2 ハンズオンの進め方

このセクションは、学習しやすさを重視して以下の2ステップで進めます。GitHub Actions に Semgrep を組み込んでプルリクエストベースでマージ前チェックとして動かし、最後に運用調整を行う流れです。

SASTをGitHub Actionsに組み込む

Semgrep を GitHub Actions のワークフローに組み込み、プルリクエストごとに自動実行されるようにします。1件だけ修正して残りの脆弱性を残した状態でプルリクエストを出し、ワークフローがエラーになること、そして指摘事項をすべて修正するとワークフローが成功することを順番に確認します。

実運用に向けた調整

実運用で直面しがちな3つの問題(軽微な指摘が多く検知される/過去の資材に対する指摘が検知される/エラーが出てもマージを止められない)に対する解決策と、Semgrepやリポジトリ側での具体的な設定を整理します。最初は緩めに導入し、徐々に厳しくしていく段階的な進め方を身につけます。

3. 環境構築

ハンズオンを始める前に、Semgrepの検知対象となるサンプルアプリケーションを用意しておきます。本セクションでは AWS は使わず、ローカルのファイルとGitHubリポジトリだけで完結します。

3.1 サンプルアプリケーションの準備

SASTの効果を体感するには、まず「SASTで検出されるべき脆弱性が含まれたコード」が必要です。実務のコードには脆弱性が無いことが理想ですが、本セクションではテスト用にあえて脆弱性を組み込んだサンプルアプリケーションを準備します。SASTが代表的に検出する脆弱性をまとめて埋め込んでおき、Semgrepが何を検出してくれるのかを後の手順で確かめられるようにします。

以下のボタンからサンプルアプリケーションのZIPファイルをダウンロードしてください。

ダウンロードしたZIPファイルを任意の場所に解凍すると、sast-handson という名前のフォルダができます。このフォルダの中身は以下のようになっています。

sast-handson/
├── vulnerable_app.py
└── .gitignore

Visual Studio Codeの「ファイル」→「フォルダーを開く」から、解凍した sast-handson フォルダを開きます。以降の操作は、Visual Studio Codeのターミナルから行います。

vulnerable_app.py の中身は以下のとおりです。SAST が検出する代表的なパターンを意図的に埋め込んだコードであり、実務では絶対に使ってはいけないコードである点に注意してください。

import hashlib
import os
import subprocess


# ハードコードされた認証情報
DATABASE_PASSWORD = "admin123"
API_KEY = "sk-1234567890abcdef"


def hash_password(password: str) -> str:
    """パスワードをハッシュ化する"""
    return hashlib.md5(password.encode()).hexdigest()


def execute_user_command(user_input: str) -> None:
    """ユーザ入力に基づいてシェルコマンドを実行"""
    subprocess.run(user_input, shell=True)


def run_user_code(code: str):
    """ユーザ提供のコードを実行"""
    return eval(code)


def get_user_by_id(user_id: str) -> str:
    """ユーザIDでユーザ情報を取得するクエリを生成"""
    query = f"SELECT * FROM users WHERE id = {user_id}"
    return query


def write_log(message: str) -> None:
    """ログをファイルに書き込む"""
    log_path = "/tmp/app.log"
    os.system(f"echo {message} >> {log_path}")

このコードには以下の脆弱性が意図的に埋め込まれています。

脆弱性 内容
DATABASE_PASSWORD ハードコードされた認証情報 パスワードがソースコードに直書きされている
API_KEY ハードコードされた認証情報 APIキーがソースコードに直書きされている
hash_password 弱いハッシュ関数の利用 MD5はパスワードハッシュとしての安全性が不十分で非推奨
execute_user_command コマンドインジェクション shell=True でユーザ入力を直接シェルに渡している
run_user_code 任意コード実行 eval でユーザ入力を任意のPythonコードとして実行
get_user_by_id SQLインジェクション SQL文字列にユーザ入力を直接連結している
write_log コマンドインジェクション os.system でユーザ入力を含む文字列を実行

これらは、SASTツールが代表的に検出する脆弱性パターンです。次のセクションでGitHub Actions上のSemgrepにこのコードを読み込ませ、これらが実際に検出されるかを確かめていきます。

4. SASTをGitHub Actionsに組み込む

サンプルコードが用意できたので、Semgrep を GitHub Actions のワークフローに組み込み、プルリクエスト時にマージ前チェックとして機能する 状態まで作っていきます。GitHub Actionsはリポジトリ上のコードを起点に動くため、まずローカルのコードをGitHub上に置く準備から始めます。

4.1 GitHubリポジトリの準備

GitHub Actionsを動かすには、その入れ物となるGitHubリポジトリにコードが乗っている必要があります。本セクション専用の検証用リポジトリを新規に作成し、ローカルのサンプルコードをPushします。

リポジトリの新規作成

GitHubの画面右上の「+」ボタンから「New repository」をクリックします。

設定項目 設定の基準
Repository name sast-handson 識別しやすい名前にする
Visibility Private 検証用のため外部公開する必要はない
Add a README file チェックしない ローカルにすでにファイルがあるため初期化不要

「Create repository」をクリックします。

💡 ポイント
GitHub Actions は、PrivateリポジトリでもGitHub Free プランで月2,000分まで無料で利用できます。Semgrepジョブは1回あたり1〜2分なので、本セクションの数回分の実行であれば十分無料枠に収まります。さらに、GitHubにクレジットカードを未登録の状態であっても無料枠の範囲で実行できます(無料枠を使い切ると自動停止し、追加課金は発生しません)。詳しい料金体系はAbout billing for GitHub Actions(GitHub公式ドキュメント)に記載があります。

ローカルリポジトリをGitHubにPush

続いて、サンプルコードをGitHubにPushしていきます。

sast-handson フォルダで、Gitリポジトリとして初期化します。

git init

ファイルをコミットします。

git add vulnerable_app.py .gitignore
git commit -m "Add vulnerable app for SAST handson"

リモートリポジトリを設定します(<your-username> をご自身のGitHubユーザ名に置き換えてください)。

git remote add origin https://github.com/<your-username>/sast-handson.git

mainブランチを作成してPushします。

git branch -M main
git push -u origin main

GitHubのリポジトリページで vulnerable_app.py がコミットされていれば、リポジトリの準備は完了です。

4.2 ワークフロー追加用ブランチの作成

ワークフローファイルを追加するための新しいブランチを作成します。

git switch -c add-sast

4.3 ワークフローファイルの作成

Semgrepを動作させるためのワークフローを作成していきます。

Visual Studio Codeのエクスプローラーで sast-handson フォルダを右クリックし、「新しいフォルダー」から .github/workflows を作成し、その中に sast.yml を作成します。

sast-handson
├── vulnerable_app.py
├── .gitignore
└── .github/
    └── workflows/
        └── sast.yml  ← このファイルを作成

作成したファイルに以下の内容を記述して保存します。

name: SAST

on:
  pull_request:
    branches:
      - main

jobs:
  semgrep:
    name: Semgrep scan
    runs-on: ubuntu-latest
    container:
      image: semgrep/semgrep
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Run Semgrep
        run: semgrep scan --config auto --error .

ワークフローを解説します。

on:
  pull_request:
    branches:
      - main

main ブランチをターゲットにしたプルリクエストの作成・更新時にこのワークフローが実行されます。本セクションでは「mainへの変更は必ずプルリクエスト経由」というチーム開発のスタイルを前提にしているため、mainへのプルリクエスト時のチェックさえ通っていれば品質は担保できます。push: トリガーは付けず、branches: で対象を main に絞ることで、mainに入るコードだけを確実にチェックする構成にしています。

container:
  image: semgrep/semgrep

ジョブの実行環境としてSemgrep公式のDockerイメージを指定しています。これによりSemgrepが事前にインストールされた状態で実行できるため、追加のインストール処理が不要になります。

- name: Run Semgrep
  run: semgrep scan --config auto --error .

semgrep scan でリポジトリ全体(.)をスキャンします。--config auto で対象言語のルールセットを自動適用し、--error検知が1件でもあれば終了コード1で終了 します。これによりSemgrepの検知がそのままワークフローの失敗に繋がり、マージ前チェックの仕組みが成立します。

4.4 ブランチのコミットとPush

作成したワークフローファイルをGitHubにPushしていきます。

git add .github/workflows/sast.yml
git commit -m "Add Semgrep workflow"
git push -u origin add-sast

4.5 プルリクエストの作成

ブランチがGitHub上に上がったので、main に取り込むためのプルリクエストを作ります。今回のワークフローはプルリクエスト時にだけ動く設定にしているため、このプルリクエストが作られた瞬間に 初めてSemgrepが起動 し、現在のリポジトリ状態に対する初回スキャン結果が出てきます。GitHubのリポジトリページに表示される「Compare & pull request」ボタンをクリックし、プルリクエストを作成します。

プルリクエストが作成されると、ワークフローが自動で実行されます。続いてこの初回ワークフロー実行結果を読み解き、Semgrepが何を検出したかを確認します。

4.6 初回ワークフロー実行結果と検出内容の確認

プルリクエストには、ワークフロー追加の変更と、まだ手付かずの脆弱なコードの両方が含まれた状態でSemgrepが実行されるため、結果はFailureで完了します。プルリクエスト画面下部の「Checks」セクション、または「Actions」タブで実行された「SAST」ワークフローを開き、「Semgrep scan」ジョブの Run Semgrep ステップを展開すると、以下のようなログが表示されます。

💡 ポイント
実際の出力はバージョンによって若干異なります。

                                                                               
┌────────────────┐
│ 7 Code Findings │
└────────────────┘

    vulnerable_app.py
   ❯❱ generic.secrets.security.detected-generic-api-key.detected-generic-api-key
          Detected a generic API key. Verify whether this is a real credential.

            8┆ API_KEY = "sk-1234567890abcdef"

   ❯❱ python.lang.security.audit.dangerous-system-call.dangerous-system-call
          Found user-controlled data in system call. This could lead to a command
          injection vulnerability.

           35┆ os.system(f"echo {message} >> {log_path}")

   ❯❱ python.lang.security.audit.eval-detected.eval-detected
          Detected the use of eval(). eval() can be dangerous if used to evaluate
          dynamic content.

           23┆ return eval(code)

   ❯❱ python.lang.security.audit.formatted-sql-query.formatted-sql-query
          Detected possible formatted SQL query. Use parameterized queries instead.

           28┆ query = f"SELECT * FROM users WHERE id = {user_id}"

   ❯❱ python.lang.security.audit.hardcoded-password.hardcoded-password
          A hardcoded password was detected. Never hardcode passwords.

            7┆ DATABASE_PASSWORD = "admin123"

   ❯❱ python.lang.security.audit.hashing.weak-hash.weak-hash
          Detected MD5 hash algorithm which is considered insecure. MD5 is not
          collision resistant and is therefore not suitable as a cryptographic
          signature.

           13┆ return hashlib.md5(password.encode()).hexdigest()

   ❯❱ python.lang.security.audit.subprocess-shell-true.subprocess-shell-true
          Found 'subprocess' function with 'shell=True'. This is dangerous because
          it can lead to command injection.

           18┆ subprocess.run(user_input, shell=True)

Ran 1000+ rules on 1 file: 7 findings.

検出結果を解説します。

ハードコードされた認証情報の検出

python.lang.security.audit.hardcoded-password.hardcoded-password
generic.secrets.security.detected-generic-api-key.detected-generic-api-key

パスワードやAPIキーといった認証情報が、ソースコードに直接書き込まれている箇所が検出されています。ソースコードはGit履歴やリポジトリに残り続けるため、漏洩リスクが高くなります。例えば、リポジトリが公開された場合や開発者の端末が侵害された場合に、認証情報がそのまま外部に流出してしまう恐れがあります。認証情報は環境変数やシークレット管理サービスから読み込む形に変更するなどの対策が必要です。

任意コード実行(eval)の検出

python.lang.security.audit.eval-detected.eval-detected

ユーザから受け取った文字列を、そのままプログラムのコードとして実行している箇所が検出されています。例えば、画面の入力フォームから悪意のあるコードを入力されると、それがそのままサーバ上で実行されてしまう恐れがあります。外部入力をそのままコードとして実行しないように制御するなどの対策が必要です。

SQLインジェクションの検出

python.lang.security.audit.formatted-sql-query.formatted-sql-query

f-stringで構築されたSQLクエリが検出されています。文字列補間でクエリを構築すると、ユーザ入力に含まれる '; などが解釈され、SQLインジェクションが可能になります。例えば、ユーザ入力をクエリ文字列に直接埋め込まず、クエリ実行時に安全に展開する仕組みを使うなどの対策が必要です。

弱いハッシュ関数(MD5)の検出

python.lang.security.audit.hashing.weak-hash.weak-hash

パスワードのハッシュ化に、安全性が不十分とされる古いハッシュアルゴリズム(MD5)の使用が検出されています。攻撃者にパスワードを解読されやすくなる恐れがあります。パスワード保存用に設計された強度の高いハッシュアルゴリズムに置き換えるなどの対策が必要です。

コマンドインジェクション(subprocess)の検出

python.lang.security.audit.subprocess-shell-true.subprocess-shell-true

ユーザから受け取った文字列を、サーバ上のコマンド(OSの操作)として実行している箇所が検出されています。悪意のあるコマンドを混入されると、サーバ上で意図しない操作(ファイル削除や情報の盗み出しなど)を実行されてしまう恐れがあります。シェルを介さずに目的の処理を実行する形に変更するなどの対策が必要です。

コマンドインジェクション(os.system)の検出

python.lang.security.audit.dangerous-system-call.dangerous-system-call

こちらも、ユーザから受け取った文字列をサーバ上のコマンドとして実行している箇所が検出されています。前述のコマンドインジェクションと同様に、悪意のあるコマンドを混入されてサーバ上で意図しない操作を実行されてしまう恐れがあります。シェルを介さずに目的の処理を実行できる安全な仕組みに置き換えるなどの対策が必要です。

ワークフローの実行結果に7件の脆弱性が表示され、ワークフロー全体のステータスが Failure になっていれば、Semgrepが脆弱性を検知してワークフローをエラーにしてくれることが確認できました。ここから先は脆弱性を1つずつ修正してワークフローが成功するまでの流れを体験していきます。

💡 ポイント
本セクションではワークフローが失敗してもプルリクエストのマージボタン自体は押せる状態のままです。main ブランチへのマージを物理的にブロックしたい場合は、別セクションGitHubで保護ブランチを設定しようで扱う保護ブランチ設定(必須ステータスチェックの指定)と組み合わせることで、「Semgrepが失敗している間はマージボタンが押せない」状態を作れます。

4.7 脆弱性の修正

ワークフローがエラーになる挙動は確認できたので、次は逆に「指摘事項をすべて修正するとワークフローが成功する」ところまで体験して、修正→再検知のサイクルがワークフロー上で正しく回ることを確かめます。vulnerable_app.py を以下の内容で全置換します。

import ast
import hashlib
import os
import subprocess


# 認証情報は環境変数から読み込む
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
API_KEY = os.environ.get("API_KEY")


def hash_password(password: str, salt: bytes) -> str:
    """パスワードを安全にハッシュ化する"""
    return hashlib.scrypt(password.encode(), salt=salt, n=16384, r=8, p=1).hex()


def execute_user_command(allowed_command: list[str]) -> None:
    """事前に許可されたコマンドのみ実行"""
    subprocess.run(allowed_command, shell=False, check=True)


def run_user_code(code: str):
    """ユーザ提供のコードを安全に評価"""
    return ast.literal_eval(code)


def get_user_by_id(user_id: int, cursor) -> None:
    """パラメータ化クエリでSQLインジェクションを防止"""
    cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))


def write_log(message: str, log_path: str) -> None:
    """ファイルAPIでログを書き込む"""
    with open(log_path, "a") as f:
        f.write(message + "\n")

それぞれ、Semgrepが検知していた問題を以下のように直しています。

  • DATABASE_PASSWORD / API_KEY: ハードコードを止め、環境変数から読み込む形に変更した
  • hash_password: MD5を、ソルト付きのscryptに置き換えてパスワードハッシュとして安全な形にした
  • execute_user_command: shell=True で受けていたユーザ入力を、許可済みコマンドのリストに限定してshell=Falseで実行する形に変更した
  • run_user_code: eval を、Pythonリテラルしか評価しない ast.literal_eval に置き換えた
  • get_user_by_id: f-stringによるSQL文字列構築を、パラメータ化クエリ(%sプレースホルダ)に変更した
  • write_log: os.system でのシェル実行を、Python標準のファイルAPIに置き換えた

修正をコミットして、同じ add-sast ブランチにPushします。同じブランチへのPushはプルリクエストに自動で反映されるため、Pushを契機にプルリクエストのワークフローが再実行されます。

git add vulnerable_app.py
git commit -m "Fix all SAST findings"
git push origin add-sast

プルリクエストページの「Checks」セクションでワークフローが再実行され、今度はSemgrepの検出が0件になり、ワークフローが「Success」に変われば、検出 → 修正 → 再検知でグリーン、というサイクルがワークフロー上で正しく回っていることが確認できます。

5. 実運用に向けた調整

ここまでで「プルリクエストで脆弱性が検知されたらワークフローをエラーにする」基本動作は体験できました。ただし実際のプロジェクトに同じ設定をそのまま入れると、開発が止まったりチェックがすり抜けられたりといった問題が出てきます。ここからは、実運用でよく直面する3つの問題と、その解決策・具体的な設定を順に見ていきます。

5.1 軽微な指摘が多く検知される

よくある問題

Semgrepは、以下のような重要度(Severity)を持っています。

  • ERROR: 即座に対処すべき重大な脆弱性が含まれる(SQLインジェクション可能箇所など)
  • WARNING: 潜在的なリスクが含まれる(例外メッセージにユーザ入力をそのまま含めるなど)
  • INFO: コード品質寄りの軽微な指摘が含まれる(変数名の命名スタイルなど)

これらの性質の違う指摘が同じ仕組みで一律に検知されます。

重要度に関係なく 検知された全件をワークフロー失敗扱い にしてしまうと、致命的な脆弱性とコード品質寄りの好みが同列に扱われ、プルリクエストごとに大量の軽微な指摘でワークフローがエラーとなります。本当に対応すべき重大な問題が埋もれるうえに、軽微な指摘を直すだけのコミットが量産されてマージサイクルが大きく遅れます。「セキュリティチェックのせいでマージが進まない」という空気ができると、SAST導入そのものが「使いづらい仕組み」と見なされて、最終的にワークフローから外されて頓挫します。

解決策

Semgrepには検知ルールごとに重要度(Severity)が設定されており、これを使って「ワークフローをエラーにする対象」を絞り込めます。よく取られる方針は、ワークフローエラーの対象を ERROR レベルの問題だけ に限定し、それ以下のレベル(WARNING / INFO)は検知結果として表示はしてもワークフローは通すという形です。これにより、致命的な問題はそのままブロックしつつ、軽微な指摘で開発が止まることを防げます。

導入初期はこの「ERROR だけブロック」状態でチームに慣れてもらい、SASTのフィードバックが日常の開発フローに馴染んできたら、徐々に対象レベルを下げて検知範囲を広げていきます。最初から全件をブロックするより、開発を止めずにSAST運用を定着させる方が、結果として最終的に守れる範囲が広がります。

具体的な設定方法

Semgrepには --severity オプションがあり、検知する重要度をフィルタできます。ERROR レベルだけでワークフローを失敗させたい場合は、ワークフローを以下のように修正します。

- name: Run Semgrep
  run: semgrep scan --config auto --error --severity ERROR .

これにより、WARNINGINFO の問題は検知結果に表示はされるものの、ワークフローは失敗しなくなります。新規コードでは厳しく、既存コードでは緩やかに、というメリハリのついた運用が可能になります。

5.2 過去の資材に対する指摘が検知される

よくある問題

SASTを既存のコードベースに後から導入すると、これまで蓄積されてきた大量のコードが一度に検知対象になります。何年も運用されてきたプロジェクトでは、重要度フィルタで ERROR に絞ってもなお数十件〜数百件残る ということも珍しくありません。

これらは過去に書かれたコードであって、今このプルリクエストで悪化させたわけではありません。しかしワークフローの仕組み上は「現在のコードに含まれている検知」として扱われるため、自分のプルリクエストと無関係な既存コードの問題まで直さないとマージできない 状態になります。担当者にとっては理不尽な体験で、「自分が触ってもいないコードまで巻き取らされるのか」という不満につながり、結局プルリクエストが詰まって新機能の開発が事実上ストップしてしまいます。

解決策

理想を言えば、SAST導入時にすべての指摘事項を一度きれいに潰しておく のがベストです。現在のコードに脆弱性がない状態を起点にできれば、それ以降の運用は「新しく入る変更だけを止める」というシンプルなかたちになり、変な抜け道を考える必要がありません。長期運用するプロジェクトであれば、可能な限りこの状態を目指す価値があります。

ただし、長く運用されてきたプロジェクトでは検知件数が膨大で、すべて直すための工数とリリーススケジュールがそもそも折り合わない場合がほとんどです。そういった現実的なケースでは、「プルリクエストで変更されたファイルだけ」をスキャン対象にし、それ以外のファイルには触れない方針にします。git diff で取得したプルリクエストの変更ファイル一覧をSemgrepに渡し、変更されたファイルだけをスキャンする方式です。変更ファイル内に含まれる検知は、過去から残っているものも含めてすべてワークフローエラー対象になります。

シナリオ 扱い
プルリクエストで触ったファイルに以前からあった指摘 対象(触った機会に拾い直す)
プルリクエストで追加・変更した部分の新しい指摘 対象
プルリクエストで触っていないファイルの指摘 対象外

これは「自分が対応した部分には責任を持つ」という方針です。触っていないファイルは責任範囲の外として扱い、触った機会にはそのファイルに残っている既存問題も含めて拾い直すかたちです。これにより、自分のプルリクエストと無関係なファイルの問題に巻き込まれずに済みつつ、コードベース全体は触る機会ごとに少しずつきれいになっていきます。

具体的な設定方法

git diff で変更ファイル一覧を取得し、それを Semgrep に渡します。差分を取るために actions/checkout のステップで fetch-depth: 0 を指定し、ベースブランチを含む全履歴を取得しておく必要があります。

- name: Checkout
  uses: actions/checkout@v4
  with:
    fetch-depth: 0

- name: Get changed files
  id: changed
  run: |
    echo "files=$(git diff --name-only --diff-filter=ACMR origin/${{ github.base_ref }}...HEAD | tr '\n' ' ')" >> $GITHUB_OUTPUT

- name: Run Semgrep on changed files
  if: steps.changed.outputs.files != ''
  run: semgrep scan --config auto --error ${{ steps.changed.outputs.files }}

--diff-filter=ACMR で削除済みファイル(D)を除外しているのは、プルリクエストで削除されたファイルがリストに残っているとSemgrepがファイルを見つけられずエラーになるためです。if: steps.changed.outputs.files != '' は、変更ファイルが0件のプルリクエスト(ドキュメントのみの変更など)でステップを空打ちしないためのガードです。

💡 ポイント
もうひとつのアプローチに「ベースライン方式」があります。Semgrepの --baseline-commit オプションに基準点となるコミットSHAを渡し、ベースライン時点と比較して 新しく増えた検知だけ をワークフロー失敗対象にする方式です。「触ったファイルかどうか」ではなく「悪化させていなければOK」という基準で、既存コードに完全にノータッチで運用できる利点はあります。ただし、プルリクエストで触ったファイルに既存問題があっても無視されるため、コードベース全体が少しずつきれいになっていく動きが起きにくく、既存問題が長く放置されやすい傾向があります。「現状維持で十分」と割り切れる場合に選ぶ選択肢で、本セクションでは汎用性の高い変更ファイル方式を中心に紹介しています。

5.3 エラーが出てもマージを止められない

よくある問題

プルリクエストに対してSASTがワークフローで失敗する状態を作っても、リポジトリ側にルールを追加していないと、現実にはチェックがすり抜けられる経路がいくつも残ります。例えば、リリース直前で時間が無いときに「今回は急ぎなのでワークフローがエラーのままマージしてしまおう」と判断されたり、緊急の hotfix で main に直接Pushされたり、Push前にSASTのワークフローを一時的にコメントアウトされたりといった具合です。

このような運用上の判断は 悪意なく日常的に起こります。GitHubのデフォルト設定ではこれらをすべて許してしまうため、SASTのチェックが仕組みとして存在していても、「いざという時には回避できる」状態が続いてしまい、長期的に見ると脆弱性混入の防壁としては機能しません。SASTを実効性のある防壁にするには、運用の善意に頼らず、リポジトリのルールとして物理的に止める 仕組みが別途必要になります。

解決策

GitHubには 保護ブランチ(Branch protection rules / Rulesets)という機能があり、特定のブランチに対して「プルリクエストを通さずに直接Pushすることを禁止する」「指定したワークフローがすべて成功するまでマージボタンを押せなくする」「Force pushを禁止する」といったルールを、リポジトリ管理者が宣言的に設定できます。

main ブランチにこれらのルールを適用すれば、SAST のチェックを通らない限り main にコードが入らない状態を リポジトリ自身のルールとして強制 できます。担当者の善意や運用ルールに頼ることなく、ワークフローすり抜けマージの経路をGitHub側で物理的に塞ぐかたちです。SASTを「動いているけれど誰も困っていないから無視される」状態にしないために、ワークフローへの組み込みと合わせて入れておきたい設定です。

具体的な設定方法

これはSemgrep側ではなくGitHub側の設定で行います。main ブランチに対する保護ブランチを有効化し、必須ステータスチェック の対象として今回作成した Semgrep scan のジョブを指定するだけです。具体的な手順は別セクションGitHubで保護ブランチを設定しようで扱っているので、そちらを参考に設定してください。

💡 ポイント
マージ前チェックは「ぜんぶ失敗 vs ぜんぶ通す」の二択ではなく、重要度・変更ファイル範囲・ブランチ保護など多軸でチューニングできます。最初は重要度フィルタで ERROR のみエラー扱いにし、次に変更ファイル方式で触った範囲だけに絞り、最後に保護ブランチでマージ自体を物理的にブロックする、という順序で段階的に締めていくのが現実的です。

6. (参考)ローカルでSemgrepを実行する

GitHub Actionsで実行する前に、ローカルでもSemgrepを実行して事前に結果を確認することもできます。Push前に手元で検知件数や残っている問題を確認でき、修正サイクルを短縮できます。

ワークフローと同じ Semgrep の公式 Docker イメージを使えば、ローカルにPythonパッケージを追加インストールする必要がなく、ワークフローと完全に同じ環境・バージョンで実行できます。vulnerable_app.py があるフォルダで以下を実行します。

Macの場合:

docker run --rm -v "$(pwd):/src" semgrep/semgrep semgrep scan --config auto /src

Windowsの場合(PowerShell):

docker run --rm -v "${PWD}:/src" semgrep/semgrep semgrep scan --config auto /src

オプションを解説します。

--rm

実行後にコンテナを自動削除し、ローカル環境に不要ファイルを残さないようにします。

Macの場合:

-v "$(pwd):/src"

Windowsの場合(PowerShell):

-v "${PWD}:/src"

カレントディレクトリ(vulnerable_app.py が置かれているフォルダ)をコンテナ内の /src にマウントし、コンテナ内のSemgrepからローカルファイルを参照できるようにします。

semgrep scan --config auto /src

ワークフローと同じ semgrep scan コマンドで /src 配下をスキャンします。--config auto で対象言語のルールセットを自動適用するのも同じです。

ワークフローと同じ7件の検出が表示されれば、ローカル実行でもまったく同じ結果が得られていることが確認できます。あとは検出を1件ずつ修正しながら同じコマンドを再実行すれば、プルリクエストに上げる前に手元で「あと何件残っているか」をリアルタイムに把握できます。「ローカルである程度きれいにしてからプルリクエストを出し、最終確認はワークフローに任せる」という流れにすると、ワークフロー のフィードバックを毎回待つストレスがなくなります。

7. 不要リソースの削除

ハンズオンで作成したリポジトリには脆弱性を含むコードが残っているため、削除しておくことを推奨します。

GitHubのリポジトリページで「Settings」をクリックし、画面を一番下までスクロールします。「Danger Zone」セクションの「Delete this repository」をクリックします。確認のためリポジトリ名の入力を求められるので、<username>/sast-handson を入力して削除します。

ローカルフォルダも不要であれば削除しておきましょう。sast-handson フォルダの一つ上の階層に移動します。

cd ..

sast-handson フォルダを削除します。

rm -rf sast-handson

8. まとめ

この章では、SASTツール Semgrep をGitHub Actionsのワークフローに組み込む流れを体験しました。

  • semgrep/semgrep Dockerイメージと semgrep scan --error により、SemgrepをGitHub Actionsのワークフローに組み込める
  • eval / 弱いハッシュ関数 / SQLインジェクション / コマンドインジェクション / ハードコード認証情報など、代表的な脆弱性パターンをワークフローで検出できる
  • プルリクエスト時にSemgrepがマージ前チェックとして動作し、指摘事項を修正するとワークフローが成功する運用にできる
  • 重要度フィルタ(--severity)で ERROR レベルだけをエラー扱いにし、段階的に導入する運用が組める
  • 変更ファイル方式(git diff + Semgrep)により、過去の資材に触らず変更ファイルだけに責任を持つ運用にできる
  • 保護ブランチを設定し、ワークフローすり抜けマージを物理的に防ぐ構成にできる
  • ローカル環境でSemgrepの公式Dockerイメージを実行し、Push前に手元で検知結果を確認できる

次の章では、CI/CDパイプラインにSCA(依存パッケージの脆弱性スキャン)を導入する流れをハンズオン形式で体験します。

この教材は役に立ちましたか?

いいねをたくさんいただけると、制作者の励みになり、より多くのセクションが作れるようになります。

感想を一言(任意)

いただいたコメントは次の制作のヒントになります。ぜひお気軽にご投稿ください。

このコメントは他の受講生には公開されません。DevOps Camp運営が、教材改善のために確認します。

0 / 2000