こちらの記事はカケハシ Advent Calendar 2023の7日目の記事になります。
こんにちは、カケハシで Musubi 開発チームのバックエンドエンジニアをしている関です。
Musubi の開発工程でたまに発生するヒューマンエラーやルーチンワークを Semgrep を利用して削減することができたので記事を書かせていただきます。
Semgrepとは
Semgrep は、コードのセキュリティやバグを見つけるための静的解析ツールで、パターンマッチングとプログラムの抽象構文木(AST)を使用して、検索パターンや条件に一致するコードを見つけます。これにより、コード内の様々なセキュリティホールやバグを特定し、修正するための支援を行います。
特定の言語やフレームワークに対する規則やカスタムルールを作成できる柔軟性があり、Python やJavaScript、Java、Goなどの多くの言語をサポートしています。また、様々なコーディングベストプラクティスをチェックするためのルールも提供されています。
そもそもどんなヒューマンエラーやルーチンワークが発生したのか
Musubi は Python で開発しているのですが、Python は動的型付け言語であり、変数や引数の型を宣言する必要がない特性を持っています。
今回コードの品質向上とバグの早期発見のため、mypy を導入し、変数や引数に型アノテーションを付与するように対応していきました。 その際に、ヒューマンエラーやルーチンワークが発生しました。それが以下2点です。
- mypy_boto3 (Boto3の型アノテーションを提供するライブラリ)のインポートに関するコーディングのミスとそれによるデプロイエラーが発生
- 定数に
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
どのようなチェックをしているのか詳しく解説していきます。
from $MYPY_BOTO3_PACKAGE.$PACKAGE import $X
またはfrom $MYPY_BOTO3_PACKAGE import $X
というパターンのインポート文を検出します。 ($MYPY_BOTO3_PACKAGE
はmypy_boto3_
で始まる文字列を示すメタ変数です。$PACKAGE
と$X
は任意の文字列や変数を表します。 "mypy_boto3_[a-z0-9]+$" という正規表現パターンに一致するパッケージ名を持つインポート文を検出します。)mypy_boto3_ で始まり、その後に小文字のアルファベットまたは数字が続くパッケージ名をチェックします。
paths
セクションで指定された除外パス("musubi/typing/boto3/*.py"
)はチェック対象外です。このようなパスに含まれるファイルのインポート文はチェックされません。チェックの結果、ルールが違反した場合に message で指定されたエラーメッセージが出力されます。
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つルールを設定しているのですが、どういうチェックをしているのかルールごとに詳しく解説していきます。
add-final-to-constants-having-type
ルール:'$M: $X = $V'
というパターンの定数定義を検出します。$M
は大文字で始まり、大文字とアンダースコアまたは数字の組み合わせで構成されるメタ変数です。$X
は任意の型を表すメタ変数で、定数の型を示します。$V
は変数の値を表します。Final
という文字列がパターンに含まれていない場合にマッチします。 マッチした場合、修正内容として$M: Final[$X] = $V
が提案されます。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
を利用して、開発工程でたまに発生するヒューマンエラーやルーチンワークを削減するために私が取り組んだ内容をご紹介しました。
この記事がみなさんのご活躍に貢献できたら幸いです!
株式会社カケハシでは、一緒に開発していただけるエンジニアを募集しております。 興味がありましたら、ぜひ下記をご覧になってください!