SonarQubeカスタムルールプラグインの開発方法


# はじめに

SonarQubeは、プログラムソースを分析する品質評価ツールで、SAST（Static Application Security Testing）というカテゴリでも語られることがあります。ソフトウェアのバグ、脆弱性、技術的負債等を摘出してくれるツールです。

全てのシステム開発に使用できますが、とりわけ大規模システムにおいて品質を均質にするために欠かせないツールになりつつあります。

デフォルトでついてくるルールに対応するだけでもかなり効果はありますが、各プロダクト・各プロジェクト独自のルールを追加したくなることもあるかと思います。

例えば、そのプロダクト独自の仕様により障害が発生し、今後、その障害を発生させないようにチェックリストに追加する、といった運用をしているチームもあるかと思います。ただ、それを人手で目検でやるとなると大変です。なるべくチェックを自動化する流れに持っていきたいですね。

ということで、本記事ではSonarQubeのカスタマイズルールや、カスタマイズプラグインの作り方についてご紹介致します。

尚、本記事ではSonarQubeとは？であったり、SonarQubeのインストール方法・使い方については言及せず、カスタマイズルール・プラグインを開発するところに絞ってご説明致します。

# 0. 手許の環境
- MacBook Pro
- macOS Monterey v12.5.1
- SonarQube v8.9

# 1. カスタマイズプラグインの作り方

SonarQubeはソースコードを分析するツールですが、文字列を解析（Parse）するにあたり、2通りのSDKが存在します。

|種類|概要|サンプルコード|
|----------|------------|------|
|テキスト分析|テキスト分析における共通な部品を利用可能|[SonarSource/SonarQube](https://github.com/SonarSource/sonar-custom-plugin-example)|
|各言語別分析|各言語の文法に特化した取り扱いが可能（クラス、メソッド、引数 等）|(Javaの場合) [SonarSource/sonar-java](https://github.com/SonarSource/sonar-java/tree/master/docs/java-custom-rules-example)|

ルールによって使い分けることになると思いますが、どちらかというと各言語別分析用のプラグインの方が使い勝手が良いかと思います。

# ２．Javaのカスタマイズルール

ここではJavaソースのカスタマイズルールを開発してみたいと思います。

## (1) クラス図
大まかなクラス図は下図の通りです（サンプルコードを解析したもの）

![SonarQube_Javaプラグイン_カスタマイズ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735566/cd9ff55e-f8a7-1847-241d-ced40cef44a8.png)


| インターフェース名 | 責務 |
|--------------------------------------------|-----------------------------------|
| org.sonar.api.Plugin                       | SonarQubeで管理しているルールの集合体 |
| org.sonar.api.server.rule.RulesDefinition  | リポジトリを作成し、サーバ実行時に読み込むルールセットを定義 |
| org.sonar.api.batch.sensor.Sensor          | Sonar-scanner等のバッチ実行時に使用するルールセットを定義 |
| org.sonar.plugins.java.api.JavaFileScanner | 各種ルールの実装クラス |

参考まで、Javaの SonarQube のデフォルトルールですが、主に下記クラスにて実装されているようです。
| クラス名 | 責務 |
|---------|------|
| org.sonar.plugins.java.JavaPlugin | Java用のプラグインを作成 |
| org.sonar.plugins.java.JavaRulesDefinition | CheckListクラスからルールクラスのセットを取得し、順次登録 |
| org.sonar.java.check.CheckList | ルールクラスを定義（実クラスは、org.sonar.java.checks.*パッケージに格納） |
| org.sonar.plugins.java.JavaSensor | チェック対象プログラムソースに対して検証 |

流れとしては、`JavaFileScanner`インターフェースを実装してルールを作ります。

作ったルールを、（サーバでの実行の際は）`RulesDefinition`を実装したクラスに登録し、
更に`RulesDefinition`実装クラスを、`Plugin`インターフェースを実装したクラスに登録するという二段構えで、プラグインが完成します。



## (2) 開発方法

### ①ルールクラスの作成
ルールクラス作成ですが、今回は、SQL Injectionのチェックのため、
`PreparedStatment`インタフェースを使用しているかどうか？というチェックをサンプルとして実装してみます。Javaの場合、`JavaFileScanner`インタフェースを実装している`IssuableSubscriptionVisitor`クラスを継承すると便利です。

```java:CustomSqlInjectionCheck.java
/**
 * SQL Injectionに関するルール定義
 */
@Rule(key = "S0001")
public class CustomSqlInjectionCheck extends IssuableSubscriptionVisitor {

    /** 表示メッセージ */
    private static final String NOT_USE_PREPARED_STATEMENT_MESSAGE = "PreparedStatmentを利用されていません。値を直書きしていないか確認してください。";

    /**
     * 確認対象ノードを取得
     * @return 確認対象ノード（変数ノード）
     */
    @Override
    public List<Tree.Kind> nodesToVisit() {
        return Arrays.asList(Tree.Kind.VARIABLE);
    }

    /**
     * ノード確認時の検証
     * @param tree ノード
     */
    @Override
    public void visitNode(Tree tree) {
        this.checkUsePreparedStatement(tree);
    }

    /**
     * PreapredStatementを使用していることの確認
     * @param tree 変数ツリー
     */
    private void checkUsePreparedStatement(Tree tree) {

        // 変数のクラスを取得
        VariableTree variable = ((VariableTree)tree);
        TypeTree variableType = variable.type();
        // Statementインタフェースを継承しており、PreparedStatementを継承していない場合
        // エラーを出力する
        if(variableType.symbolType().isSubtypeOf("java.sql.Statement")
            && !variableType.symbolType().isSubtypeOf("java.sql.PreparedStatement")) {

            // 問題を起票
            this.reportIssue(variable.initializer(), NOT_USE_PREPARED_STATEMENT_MESSAGE);
        }
    }
}
```
### ②解説ページ（HTML）

次に、SonarQubeで指摘発生時に表示する解説ページを作成します。

`<h2>`タグの名称は固定のため変えられないのですが、その他については`<p>`タグや`<ul>` `<li>`タグ等、自由に利用可能です。

```html:S0001.html
<p>SQL Injectionの危険性(カスタムルール)です。</p>
<h2>Ask Yourself Whether</h2>
<p>SQL実行時にPrepared Statementを使用していないと、本ルールによって検知されることがあります。</p>
<h2>Recommended Secure Coding Practices</h2>
<ul>
  <li> PreparedStatementを使用し、バインド変数化（パラメータ化）してください。</li>
  <li> MyBatis等のOR Mapperフレームワークを使用する場合にもご注意ください。</li>
</ul>
<h2>Sensitive Code Example</h2>
<pre>
public String getUserName(Connection conn, String id) throws SQLException {

  Statement stmt = null;
  String query = "SELECT user_name " +
                 "FROM users WHERE id ='" + id + "'";
  try {
    stmt = con.createStatement();  // NG!!
    ResultSet rs = stmt1.executeQuery("query");  

    //...
}

</pre>
<h2>Compliant Solution</h2>
<pre>
public String getUserName(Connection conn, String id) throws SQLException {

  PreparedStatement pstmt = null;
  String query = SELECT user_name " +
                 "FROM users WHERE id = ?";
  try {
    pstmt = con.prepareStatement(query);  // OK!!
    pstmt.setString(1, user); 
    ResultSet rs = pstmt.executeQuery();

    //...
  }
}

</pre>
<h2>See</h2>
<ul>
  <li> <a href="https://owasp.org/Top10/A03_2021-Injection/">OWASP Top 10 2021 Category A3</a> - Injection </li>
</ul>
```

また、ルールに関する情報をJSONファイルで定義します。

```json:S0001.json
{
  "title": "SQL Injectionの危険性(カスタムルール)",
  "type": "SECURITY_HOTSPOT",
  "status": "ready",
  "remediation": {
    "func": "Constant\/Issue",
    "constantCost": "20min"
  },
  "tags": [
    "owasp-a1",
    "sql"
  ],
  "defaultSeverity": "Critical",
  "ruleSpecification": "RSPEC-2077",
  "sqKey": "J0002",
  "scope": "Main",
  "securityStandards": {
    "OWASP Top 10 2021": [
      "A3"
    ],
  },
  "quickfix": "unknown"
}
```

重要な点が一つあり、SonarQubeフレームワーク内で直書きのため、このHTMLファイルとJSONファイルについては配置場所が決まっており、下記フォルダに配置する必要があります。
> src\main\resources\org\sonar\l10n\java\rules\java

### ③ルールの登録

サーバサイドで実施するときには、`RulesDefinition`インターフェースを実装したクラスで登録します。
```java:CustomJavaRulesDefinition.java
/**
 * サーバのレポジトリにおける定義について管理
 */
public class CustomJavaRulesDefinition implements RulesDefinition {

    /** ルールに関するJSON、HTMLを管理しているフォルダ。変更不可のため、デフォルトのまま使用 */
    private static final String RESOURCE_BASE_PATH = "org/sonar/l10n/java/rules/java";

    /** レポジトリキー */
    public static final String REPOSITORY_KEY = "custom-java";
    /** レポジトリ名 */
    public static final String REPOSITORY_NAME = "独自プラグイン";
    /** ターゲットプログラミング言語 */
    public static final String LANGUAGE = "java";

    /** 実行ランタイム */
    private final SonarRuntime _runtime;

    /**
     * コンストラクタ
     * 
     * @param runtime SonarQubeのランタイム
     */
    public CustomJavaRulesDefinition(SonarRuntime runtime) {
        this._runtime = runtime;
    }

    /**
     * リポジトリ（ルールセット）に関する定義
     * 
     * @param context
     */
    @Override
    public void define(Context context) {

        // リポジトリの作成
        NewRepository repository = context.createRepository(REPOSITORY_KEY, "java").setName(REPOSITORY_NAME);

        // ルールのメタ情報をロード
        RuleMetadataLoader ruleMetadataLoader = new RuleMetadataLoader(RESOURCE_BASE_PATH, this._runtime);

        // 対象チェックルールを追加
        ruleMetadataLoader.addRulesByAnnotatedClass(repository, new ArrayList<>(CustomRulesList.getChecks()));

        // リポジトリに反映
        repository.done();
    }
  }
```

ルールセットについては、別途クラスを設けて定義した方が便利です。
メインクラスへのチェックルールのセット、テストクラスへのチェックルールのセットを別途引数で渡すメソッドもあったりするので、`CustomRulesList`のようなクラスを作ると使いまわしが効きます。

```java:CustomRulesList.java
/**
 * ルールセット
 */
public final class CustomRulesList {

    /** コンストラクタ（公開しない） */
    private CustomRulesList() {}

    /**
     * チェックルールを取得
     */
    public static List<Class<? extends JavaCheck>> getChecks() {
        List<Class<? extends JavaCheck>> checks = new ArrayList<>();
        checks.addAll(getJavaChecks());
        checks.addAll(getJavaTestChecks());
        return Collections.unmodifiableList(checks);
    }

    /**
     * メインチェックルールを取得
     */
    public static List<Class<? extends JavaCheck>> getJavaChecks() {
        return Collections.unmodifiableList(Arrays.asList(
            CustomSqlInjectionCheck.class));
    }

    /**
     * テストクラス用のチェックルールを取得
     */
    public static List<Class<? extends JavaCheck>> getJavaTestChecks() {
        // テストクラス用のチェックルールが出来たときには、ここに追加
        return Collections.emptyList();
    }
}
```

### ④プラグイン
最後に、ルールプラグインクラスを作成します。`Plugin`インタフェースを実装したクラスを準備し、`define(Context)`メソッドをオーバーライドします。
その中で、`addExtension()`や`addExtensions()`メソッドを使って、ルール定義クラスを登録します。

```java:CustomJavaRulesPlugin
/**
 * Custom Javaルールプラグイン
 */
public class CustomJavaRulesPlugin implements Plugin {

    @Override
    public void define(Context context) {
        context.addExtension(CustomJavaRulesDefinition.class);
    }
}
```

# ３．テスト方法（カスタマイズプラグインの登録）

カスタマイズプラグインを登録する手順は下記の通りです。

- pluginのjarファイルを`extensions/downloads`フォルダの配下に配置します。
- SonarQubeを起動します。（Windowsであれば、`bin\windows-x86-64\StartSonar.bat`を実行）
- Sonar Scannerを起動します。（今回はMavenで実行します）

## (1) pluginのjarの配置

pluginのjarを作るには、Maven等でjarをコンパイルする必要があります。
Mavenのxmlは`SonarSource/sonar-java/docs/java-custom-rules-example`の下にある`pom_SQ_8_9_LTS.xml`を使用します。（mvnコマンドをデフォルトで実行するときには、`pom.xml`へのリネームが必要）

`pom.xml`には
- SonarQubeのポータル上で表示されるプラグインの名称
- プラグインの説明
- プラグインが対応しているSonarQubeのバージョン
- Sonar Java（Java用のプラグインSDK。SonarQubeと別です）のバージョン
- Pluginのバージョン

等を指定します。

```xml:pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <!-- Change this groupId to your own organization groupId -->
  <groupId>org.sonar.Custom</groupId>

   <!-- Change this artifactId to your own organization artifactId -->
  <artifactId>custom-rules-plugin</artifactId>
   <version>0.0.1</version>
   <packaging>sonar-plugin</packaging>

   <!--  Change the name of the plugin here -->
  <name>SonarQube Java :: Documentation :: カスタマイズルール</name>

   <!--  Change the description of the plugin here -->
   <description>JavaのSonarQube用のカスタマイズプラグインです</description>
   <properties>
     <sonarqube.version>8.9.0.43852</sonarqube.version>
     <sonarjava.version>6.15.1.26025</sonarjava.version>
 
     <!-- Don't forget to update this version of JaCoCo -->
     <jacoco.version>0.8.7</jacoco.version>
 
     <!-- Use UTF-8 for all resources -->
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 
     <!-- Make sure to be compatible with java 8 -->
     <maven.compiler.release>8</maven.compiler.release>
   </properties>

    :
 （以下、依存モジュールの読込が続く）
    :
```
あとは普通にプラグインをコンパイルするだけです。

```
【プラグイン開発のルートフォルダ】> mvn install
```

実行すると、下記ファイルが出来上がります。
- `custom-rules-plugin-0.0.1.jar`
- `custom-rules-plugin-0.0.1-shaded.jar`
- `original-custom-rules-plguin-0.0.1.jar`

ご参考までですが、`shaded.jar`は同じモジュールで違ったバージョンに依存している場合、
そのコンフリクトを解決したものです。

## (2) SonarQubeの起動

先ほどのMavenによるビルドで出来上がった`custom-rules-plugin-0.0.1.jar`を、SonarQubeの`download`フォルダに格納します。

```
【SonarQubeのホームディレクトリ】/extensions/download
```

このフォルダに格納後、SonarQubeを起動すると、自動的にdownloadフォルダにあるプラグインが読み込まれます。

## (3) Sonar Scannerの起動

実際のソーススキャンについても、MavenやGradle等を使用して実行可能です。

### ①プロジェクトの作成
- SonarQubeのWeb画面にアクセスしてログインして、上部タブからProjectsをクリック
- 右上のCreate Projectをクリックし、プロジェクト名（下記画面キャプチャではTEST Project）、キーフレーズを入力

これによりプロジェクトが作成されます。
![SonarQube_12_Project選択画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735566/f8a687d3-c3b9-4da7-deb6-25cc83a5672d.png)

### ②プロファイルの作成
- 上部タブメニューよりQuality Profilesをクリック
- 右上のCreateをクリック
- 右側のCreate a blank quality profilesをクリック
- プログラム言語、プロファイル名（新規で命名）を入力してCreateをクリック

これで専用のプロファイル（チェックルール群）が作成されます

### ③ルールの設定
- 上部タブメニューよりRulesをクリック
- 左側メニューより Repositoryを開き、登録したカスタムルールプラグインをクリックすると、そのプラグインで対応しているルールの一覧が表示
- ルールを選択すると、カスタムルールについての説明を閲覧可能
- 左下に表示される「Activate」をクリックすると、どのプロファイルにどのレベル(Critical等)が設定できます
![SonarQube_11_カスタムルールプラグインの説明.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735566/7336f767-69bd-46dc-2036-9ba2824273c3.png)

### ④クライアント側からSonarQube検証を実行
ここまで設定した後、クライアント側からMavenコマンドを発行します。発行するコマンドは下記手順で表示できます。

1. プロジェクトを選択し、ターゲットソースがあるローカルをクリック
![SonarQube_01_検証対象ソースの指定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735566/7a5ea883-615a-5378-d524-cbaeb7679855.png)

2. トークン発行のためのキーフレーズを入力してGenerateボタンを押下。（この例では `test` と入力）
![SonarQube_02_トークン発行.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735566/cf9bebeb-9b7b-ef26-6fde-8499c402db5b.png)


3. Sonar Scannerを実行するのに使うツールを選択すると、コマンドが表示される
![SonarQube_03_コマンド表示.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735566/3fbf1f50-e4b1-7607-2994-d83191d40a08.png)
例えば、下記のようなコマンドが表示されます。
``` 
mvn clean verify sonar:sonar \
  -Dsonar.projectKey=test \
  -Dsonar.host.url=http://localhost:9000 \
  -Dsonar.login=test
```

こちらを実行すると、チェック結果がSonarQubeに反映されて、Web画面からチェック結果を見ることができます。
![SonarQube_13_チェック結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735566/6827a3b8-eb35-7576-1ade-6321758c0909.png)



# ４．Java文解析モジュール
SonarQubeでは各プログラミングに対応したParserがあり、SonarQubeエンジンで解析することで、
それぞれの要素を様々なオブジェクトとして取り扱うことができます。

Javaの場合、前述したように`IssuableSubscrptionVisitor`を継承したクラスを作成すると、
Javaプログラムを構造化（ツリー構造）で下記のようなクラスオブジェクトで管理することができます。
|クラス名|主な管理メタデータ|
|--------|----|
|VariableTree|変数情報（変数名、クラス名）|
|MethodTree|メソッド情報（引数情報）|
|ExpressionStatementTree|式に関する情報（例：`+`を使っているか？）|
|CatchTree|例外（キャッチ）に関する情報（変数名等）|

詳細は、GitHub[^GitHub_URL]を見ていただければと思いますが、かなり多くの情報を簡単に取得可能です。

これらを組み合わせて使用することで、プログラムソースのメトリクス情報を容易に取得することができます。

[^GitHub_URL]: https://github.com/SonarSource/sonar-java/tree/61d41791cf55f3b437c725a5be07deda1eadc1a7/java-frontend/src/main/java/org/sonar/plugins/java/api/tree

# おわりに
本記事では、SonarQubeのカスタマイズルールを作ってみました。
比較的簡単にカスタマイズルールが作成できることがお分かりいただけると幸いです。

大規模システムの開発において、プログラムソースを均質化するためには、検証ツールの利用が必要不可欠になりつつあります。
今までチェックリストで品質担保していたチェック内容を、コンピュータが自動で担保できることで、品質も勿論向上しますし、CI/CDの中に組み込めば、手戻りが小さくでき、結果、生産性の向上にも繋がると思います。


日頃、プログラミングしたくて仕方がないが、どちらかというとレビューの仕事が多いアーキテクトの皆さんにとっても、プログラミングする機会を得られる仕事でと思いますので、是非試してみてはいかがでしょうか？