RemitAid Tech Blog
🔍

Semgrepの柔軟な静的解析でマルチテナントアプリの実装ミスを防ぐ

に公開

はじめに

RemitAid ソフトウェアエンジニアの inari111 です。

RemitAidではマルチテナント構成でアプリケーションを構築しています。
異なるテナントのデータに誤ってアクセスすることを防ぐため、Repositoryのinterfaceでは必ず merchantID を引数に受け取るようにしています。

// 例
type UserRepository interface {
    List(ctx context.Context, merchantID MerchantID, tx *sql.Tx) ([]User, error)
}

マルチテナントの実装詳細については本記事では触れませんが、Semgrepを活用してRepositoryのinterfaceに merchantID が漏れなく含まれていることを自動検証する仕組みをご紹介します。

Semgrepとは

Semgrepは、"Code scanning at ludicrous speed" をスローガンに掲げる、オープンソースの静的解析ツールです。Go、Python、JavaScript、TypeScriptなど30以上の言語に対応しています。

通常のgrepが文字列の完全一致でしか検索できないのに対し、Semgrepはコードの意味を理解して検索します。例えば以下のコードに対して「2」を検索すると、Semgrepは y = x + 1 にマッチします。

x = 1
y = x + 1

また、ルールは実際のコードに似た記法で書けるため、抽象構文木(AST)や複雑なDSLの知識がなくても直感的にカスタムルールを作成できます。

Semgrep CLI の使い方

今回は Semgrep CLI を使っていきます。
インストールは README に書いてあるのでお好みの方法でインストールしてください。

今回、静的解析でチェックしたいことは2点あります。

  • 第二引数が merchantID であること
  • 最後の引数が tx であること

これらをチェックするためのルールをYAMLに記載します。

# 例
rules:
  # ルール1: 第二引数がmerchantID MerchantID型ではない、または最後の引数がtx *sql.Txではない場合を検出
  - id: require-merchant-id-second-argument
    message: "第二引数は 'merchantID MerchantID' に、最後の引数は 'tx *sql.Tx' にしてください"
    severity: ERROR
    languages: [go]
    patterns:
      # インターフェース定義内のメソッドシグネチャにマッチ
      - pattern-inside: |
          type $INTERFACE interface {
            ...
          }
      # 2つ以上の引数を持つメソッド
      - pattern: $METHOD($CTX $CTX_TYPE, $ARG $ARG_TYPE, ...) $RET
      # 正しいパターンを除外(MerchantID型 + 最後がtx *sql.Tx)
      - pattern-not: $METHOD($CTX $CTX_TYPE, merchantID MerchantID, ..., tx *sql.Tx) $RET

  # ルール2: 引数が1つのみ(contextのみ)の場合を検出
  - id: require-merchant-id-missing
    message: "第二引数に 'merchantID MerchantID' がありません"
    severity: WARNING
    languages: [go]
    patterns:
      - pattern-inside: |
          type $INTERFACE interface {
            ...
          }
      # contextのみを引数に持つメソッド
      - pattern: $METHOD($CTX $CTX_TYPE) $RET

実行方法

今回はRepositoryが対象なので、Repositoryのファイルが置いてあるディレクトリを指定しています。

semgrep --config {yamlファイル} {ターゲットディレクトリ} \
    --error \
    --include '**/domain/repository/*.go'

例外を除外

例外を除外するにはコメントに nosemgrep: {ルールID} を記載します。

// nosemgrep: require-merchant-id-second-argument

Semgrep導入の効果

導入時には、過去のコードで merchantID が漏れていた箇所を発見することができました。

日々の運用では、レビュー時に見る観点が少なくなり、コアロジックのレビューに時間を割けるようになりました。CIに組み込むことでレビュー時の抜け漏れがなくなり、実装が統一されたのも効果として感じています。

さらに、READMEなどのドキュメントに細かくコーディング規約を書くよりも、Semgrepのルールとしてコードで明文化されている方が、新しいメンバーにも伝わりやすいと感じています。

ルールの書き方に柔軟性があるため、他の箇所にも使えそうです。
今後も積極的に使っていきたいと考えています。

まとめ

本記事では、Semgrepを活用してマルチテナントアプリケーションのRepository interfaceに merchantID が漏れなく含まれていることを自動検証する仕組みをご紹介しました。

より厳密に対応するのであれば、interfaceだけでなく実装側もチェックする必要がありますが、静的解析の第一弾として手軽に導入できたのは良かったと思います。Semgrepはカスタムルールを直感的に書けるため、チーム固有のコーディング規約を自動検証したい場合にもおすすめです。

Podcast 「RemiTalk」を配信していますので、もし良ければ聴いてみてください!

https://podcasts.apple.com/jp/podcast/remitalk/id1826516525

RemitAid Tech Blog
RemitAid Tech Blog

Discussion