KAKEHASHI Tech Blog

カケハシのEngineer Teamによるブログです。

Semgrepでちょっとしたルーチンワークやヒューマンエラーを削減できた話

こちらの記事はカケハシ Advent Calendar 2023の7日目の記事になります。

adventar.org

こんにちは、カケハシで Musubi 開発チームのバックエンドエンジニアをしている関です。

Musubi の開発工程でたまに発生するヒューマンエラーやルーチンワークを Semgrep を利用して削減することができたので記事を書かせていただきます。

Semgrepとは

Semgrep は、コードのセキュリティやバグを見つけるための静的解析ツールで、パターンマッチングとプログラムの抽象構文木(AST)を使用して、検索パターンや条件に一致するコードを見つけます。これにより、コード内の様々なセキュリティホールやバグを特定し、修正するための支援を行います。

特定の言語やフレームワークに対する規則やカスタムルールを作成できる柔軟性があり、Python やJavaScript、Java、Goなどの多くの言語をサポートしています。また、様々なコーディングベストプラクティスをチェックするためのルールも提供されています。

そもそもどんなヒューマンエラーやルーチンワークが発生したのか

Musubi は Python で開発しているのですが、Python は動的型付け言語であり、変数や引数の型を宣言する必要がない特性を持っています。

今回コードの品質向上とバグの早期発見のため、mypy を導入し、変数や引数に型アノテーションを付与するように対応していきました。 その際に、ヒューマンエラーやルーチンワークが発生しました。それが以下2点です。

  1. mypy_boto3 (Boto3の型アノテーションを提供するライブラリ)のインポートに関するコーディングのミスとそれによるデプロイエラーが発生
  2. 定数に Final を付与するというルーチンワークが発生

今回この2つを Semgrep を使って解決できるようにしていきました。

補足

上記1についての補足ですが、mypy の静的型検査のために mypy_boto3 を利用していますが、ライブラリ自体はローカル環境にしか入れていないため、以下のように TYPE_CHECKING で開発環境や本番環境で参照してしまうのを回避する必要がありました。

# AWS Cognito用の型アノテーションの定義
if TYPE_CHECKING:  # mypy 実行時のみ True
    from mypy_boto3_cognito_idp.client import CognitoIdentityProviderClient

else:
    from typing import Any
    CognitoIdentityProviderClient = Any

そして、この CognitoIdentityProviderClient を import して利用する際のコードのNG、OKパターンが以下になります。

NGパターン

from mypy_boto3_cognito_idp.client import CognitoIdentityProviderClient

OKパターン

from musubi.typing.boto3.cognito import CognitoIdentityProviderClient

Semgrep の導入

こちらに従って好きな方法でインストールできますが、今回は以下のコマンドでインストールしました。

インストール

pip install semgrep

また、 pre-commit で Linter や Formatter 、mypy の型のチェックなどを行なっており、Semgrep によるチェックも pre-commit に導入しました。

pre-commitの設定ファイル に Semgrep の設定を追加

以下のように Semgrep の設定を追加しました。 (semgrep/check-mypy-boto3-to-typing.yml については後述します。)

# .pre-commit-config.yaml
- repo: https://github.com/returntocorp/semgrep
  rev: v1.43.0
  hooks:
    - id: semgrep
      args: ['--config', 'semgrep/check-mypy-boto3-to-typing.yml', '--error']
      files: |
        (?x)^(
          musubi/(|
            services/.*|
            entities/.*|
            util/.*|
            views/.*|
            :
            :
          ).py$

args--configオプションは、特定の設定ファイルを指定し、--errorはエラーがあればそれを表示するように指示しています。

Semgrepのチェックルールを定義

mypy_boto3 の型情報を利用する際のインポート文のチェックルール

まずは、mypy_boto3 のインポートに関するコーディングミスの回避のためのチェックルールを定義します。( 上記にて pre-commit でチェックするようにしたルールです。)

rules:
  - id: check-mypy-boto3-to-typing
    languages:
      - python
    patterns:
      - pattern-either:
          - pattern: from $MYPY_BOTO3_PACKAGE.$PACKAGE import $X
          - pattern: from $MYPY_BOTO3_PACKAGE import $X
      - metavariable-regex:
          metavariable: $MYPY_BOTO3_PACKAGE
          regex: 'mypy_boto3_[a-z0-9]+$'
    paths:
      exclude:
        - "musubi/typing/boto3/*.py"
    message: check import mypy boto3 typing packages
    severity: ERROR

どのようなチェックをしているのか詳しく解説していきます。

  1. from $MYPY_BOTO3_PACKAGE.$PACKAGE import $X または from $MYPY_BOTO3_PACKAGE import $X というパターンのインポート文を検出します。 ($MYPY_BOTO3_PACKAGEmypy_boto3_ で始まる文字列を示すメタ変数です。 $PACKAGE$X は任意の文字列や変数を表します。 "mypy_boto3_[a-z0-9]+$" という正規表現パターンに一致するパッケージ名を持つインポート文を検出します。)

  2. mypy_boto3_ で始まり、その後に小文字のアルファベットまたは数字が続くパッケージ名をチェックします。 paths セクションで指定された除外パス("musubi/typing/boto3/*.py")はチェック対象外です。このようなパスに含まれるファイルのインポート文はチェックされません。

  3. チェックの結果、ルールが違反した場合に message で指定されたエラーメッセージが出力されます。

  4. severity はエラーの深刻度を示し、ここでは ERROR と設定されています。つまり、ルールに違反した場合にはエラーとして扱われます。

定数に Final を付与するチェックルール

次に、定数にFinalが付いていない場合に、Finalを付与するチェックルールを定義します。

rules:
  - id: add-final-to-constants-having-type
    languages:
      - python
    patterns:
      - pattern: '$M: $X = $V'
      - metavariable-regex:
          metavariable: $M
          regex: '[A-Z][_A-Z0-9]*$'
      - pattern-not-regex: Final
    fix: '$M: Final[$X] = $V'
    message: check constants forgetting add "Final[X]"
    severity: WARNING
  - id: add-final-to-constants
    languages:
      - python
    patterns:
      - pattern: $M = $V
      - metavariable-regex:
          metavariable: $M
          regex: '[A-Z][_A-Z0-9]*$'
      - pattern-not-regex: Final
    fix: '$M: Final = $V'
    message: check constants forgetting add "Final"
    severity: WARNING

2つルールを設定しているのですが、どういうチェックをしているのかルールごとに詳しく解説していきます。

  1. add-final-to-constants-having-type ルール: '$M: $X = $V' というパターンの定数定義を検出します。 $M は大文字で始まり、大文字とアンダースコアまたは数字の組み合わせで構成されるメタ変数です。 $X は任意の型を表すメタ変数で、定数の型を示します。 $V は変数の値を表します。 Final という文字列がパターンに含まれていない場合にマッチします。 マッチした場合、修正内容として $M: Final[$X] = $V が提案されます。

  2. add-final-to-constants ルール: $M = $V というパターンの定数定義を検出します。 $M は大文字で始まり、大文字とアンダースコアまたは数字の組み合わせで構成されるメタ変数です。 $V は変数の値を表します。 Final という文字列がパターンに含まれていない場合にマッチします。 マッチした場合、修正内容として $M: Final = $V が提案されます。

これらのルールは、大文字で表される定数が Final 型アノテーションを持っていないかを検出し、もし持っていない場合は修正を提案します。これにより、コード内の定数の型安全性を向上させることができます。

追加した Semgrep のチェックルールを利用するタイミング

mypy_boto3 の型情報を利用する際のインポート文のチェックルールは、pre-commitでhookするように設定しており、commit時に気付けるようにしました。

定数に Final を付与するチェックルールのほうは、pre-commit には入れずに、手動で実行してチェックするようにしました。

手動で実行する時のコマンドはこちらです。

# 修正すべき箇所を表示したい場合
semgrep --config semgrep/add-final-to-constants.yml [対象ファイル]

# 修正まで実施する場合は、`--autofix`を付けて実行
semgrep --config semgrep/add-final-to-constants.yml [対象ファイル] --autofix

まとめ

以上、Semgrep に関して紹介をさせていただきました。

今回、Semgrep を使って mypy_boto3 のインポートに関するコーディングミスのチェックと、定数に Final を付与するというルーチンワークの作業を削減することができました。

特に、mypy_boto3 のインポートに関するコーディングミスに関しては、開発環境へデプロイするまで気付けず、その度に修正作業とデプロイのやり直しが発生し、復旧できるまで1時間近くかかっていたので、そういう事態を回避できるようにチェックを自動化できた事は、チームのパフォーマンス改善に繋がったのではないかと感じています。

もし、ソースコードのバグや脆弱性などを人力でチェックしているような事があれば Semgrep で自動化することができるかもしれないので、ぜひ試してみてください。 ルールを書く際は、公式ドキュメントの Writing rules が参考になります。

最後に

最後まで記事をご覧いただき、ありがとうございます。

本記事では、Semgrep を利用して、開発工程でたまに発生するヒューマンエラーやルーチンワークを削減するために私が取り組んだ内容をご紹介しました。

この記事がみなさんのご活躍に貢献できたら幸いです!

株式会社カケハシでは、一緒に開発していただけるエンジニアを募集しております。 興味がありましたら、ぜひ下記をご覧になってください!