跳到主要内容

SonarQube

概要

SonarQubeは、コードの品質とセキュリティを継続的に検査するためのオープンソースプラットフォームです。静的コード解析を通じて、バグ、脆弱性、コードスメルを検出し、技術的負債を可視化します。

主な機能

1. コード品質分析

  • バグ検出: 実行時エラーにつながる可能性のあるコードの問題を特定
  • コードスメル: 保守性を低下させるコードパターンを検出
  • 重複コード: コードベース内の重複を識別
  • 複雑度測定: サイクロマティック複雑度などのメトリクスを計算

2. セキュリティ分析

  • 脆弱性検出: OWASP Top 10などのセキュリティ脆弱性を特定
  • セキュリティホットスポット: レビューが必要なセキュリティ上の懸念箇所を指摘
  • CWE/SANS Top 25対応: 一般的な脆弱性パターンに基づく分析

3. 技術的負債の可視化

  • 負債の定量化: 問題を修正するために必要な時間を推定
  • 品質ゲート: プロジェクトが品質基準を満たしているかを判定
  • トレンド分析: 時間経過に伴うコード品質の変化を追跡

サポート言語

SonarQubeは多数のプログラミング言語をサポートしています:

  • Java、C#、JavaScript、TypeScript
  • Python、PHP、Ruby、Go
  • C、C++、Objective-C、Swift
  • Kotlin、Scala、HTML、CSS
  • SQL、XML、YAML など

検出される問題の例(.NET)

SonarQubeが検出する典型的なコード品質とセキュリティの問題を、C#のコード例で紹介します。

1. Null参照の問題

問題のあるコード:

public class UserService
{
public string GetUserName(User user)
{
// S2259: user がnullの可能性があるのにチェックしていない
return user.Name.ToUpper();
}
}

修正後:

public class UserService
{
public string GetUserName(User user)
{
if (user == null)
throw new ArgumentNullException(nameof(user));

return user.Name?.ToUpper() ?? string.Empty;
}
}

2. 例外処理の問題

問題のあるコード:

public class DataProcessor
{
public void ProcessData()
{
try
{
// 処理
}
catch (Exception ex)
{
// S2737: 空のcatchブロック - 例外が無視されている
}
}

public void AnotherMethod()
{
try
{
// 処理
}
catch (Exception ex)
{
// S1181: 一般的なExceptionをキャッチすべきでない
Console.WriteLine(ex.Message);
}
}
}

修正後:

public class DataProcessor
{
private readonly ILogger<DataProcessor> _logger;

public void ProcessData()
{
try
{
// 処理
}
catch (InvalidOperationException ex)
{
// 具体的な例外をキャッチし、適切に処理
_logger.LogError(ex, "データ処理中にエラーが発生しました");
throw; // 必要に応じて再スロー
}
}
}

3. リソースのリーク

問題のあるコード:

public class FileHandler
{
public string ReadFile(string path)
{
// S2930: using ステートメントを使用していない
var reader = new StreamReader(path);
return reader.ReadToEnd();
// reader が破棄されない
}
}

修正後:

public class FileHandler
{
public string ReadFile(string path)
{
using (var reader = new StreamReader(path))
{
return reader.ReadToEnd();
}
// または using 宣言を使用
// using var reader = new StreamReader(path);
// return reader.ReadToEnd();
}
}

4. セキュリティ脆弱性 - SQLインジェクション

問題のあるコード:

public class UserRepository
{
private readonly SqlConnection _connection;

public User GetUser(string username)
{
// S3649: SQLインジェクションの脆弱性
var query = $"SELECT * FROM Users WHERE Username = '{username}'";
var command = new SqlCommand(query, _connection);
// 攻撃者が username に "admin' OR '1'='1" を入力可能
return ExecuteQuery(command);
}
}

修正後:

public class UserRepository
{
private readonly SqlConnection _connection;

public User GetUser(string username)
{
var query = "SELECT * FROM Users WHERE Username = @username";
var command = new SqlCommand(query, _connection);
command.Parameters.AddWithValue("@username", username);
return ExecuteQuery(command);
}
}

5. セキュリティ脆弱性 - ハードコードされた認証情報

問題のあるコード:

public class DatabaseConnection
{
// S2068: ハードコードされたパスワード
private const string ConnectionString =
"Server=myserver;Database=mydb;User=admin;Password=P@ssw0rd123;";

public SqlConnection GetConnection()
{
return new SqlConnection(ConnectionString);
}
}

修正後:

public class DatabaseConnection
{
private readonly IConfiguration _configuration;

public DatabaseConnection(IConfiguration configuration)
{
_configuration = configuration;
}

public SqlConnection GetConnection()
{
var connectionString = _configuration.GetConnectionString("DefaultConnection");
return new SqlConnection(connectionString);
}
}

6. コードの複雑度が高い

問題のあるコード:

public class OrderValidator
{
// S1541: 認知的複雑度が高すぎる(ネストが深い)
public bool ValidateOrder(Order order)
{
if (order != null)
{
if (order.Items != null)
{
if (order.Items.Count > 0)
{
foreach (var item in order.Items)
{
if (item != null)
{
if (item.Quantity > 0)
{
if (item.Price > 0)
{
// 処理
}
else
{
return false;
}
}
else
{
return false;
}
}
}
}
}
}
return true;
}
}

修正後:

public class OrderValidator
{
public bool ValidateOrder(Order order)
{
if (order == null || order.Items == null || order.Items.Count == 0)
return false;

return order.Items.All(item => IsValidItem(item));
}

private bool IsValidItem(OrderItem item)
{
return item != null && item.Quantity > 0 && item.Price > 0;
}
}

7. デッドコード

問題のあるコード:

public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}

// S1144: 使用されていないprivateメソッド
private int Subtract(int a, int b)
{
return a - b;
}

public int Multiply(int a, int b)
{
int unused = 10; // S1481: 使用されていないローカル変数
return a * b;
}
}

修正後:

public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}

public int Multiply(int a, int b)
{
return a * b;
}

// 不要なコードを削除
}

8. 非同期メソッドの誤用

問題のあるコード:

public class ApiClient
{
private readonly HttpClient _httpClient;

public string GetData(string url)
{
// S4462: async メソッドを同期的に待機している(デッドロックの危険性)
var response = _httpClient.GetAsync(url).Result;
return response.Content.ReadAsStringAsync().Result;
}
}

修正後:

public class ApiClient
{
private readonly HttpClient _httpClient;

public async Task<string> GetDataAsync(string url)
{
var response = await _httpClient.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
}

9. 重複コード

問題のあるコード:

public class ReportGenerator
{
public string GeneratePdfReport(Report report)
{
var header = $"Report: {report.Title}\n";
header += $"Date: {DateTime.Now:yyyy-MM-dd}\n";
header += $"Author: {report.Author}\n";
header += "---\n";
// PDF生成処理
return GeneratePdf(header, report.Content);
}

public string GenerateHtmlReport(Report report)
{
var header = $"Report: {report.Title}\n";
header += $"Date: {DateTime.Now:yyyy-MM-dd}\n";
header += $"Author: {report.Author}\n";
header += "---\n";
// HTML生成処理
return GenerateHtml(header, report.Content);
}
}

修正後:

public class ReportGenerator
{
private string BuildHeader(Report report)
{
return $"Report: {report.Title}\n" +
$"Date: {DateTime.Now:yyyy-MM-dd}\n" +
$"Author: {report.Author}\n" +
"---\n";
}

public string GeneratePdfReport(Report report)
{
var header = BuildHeader(report);
return GeneratePdf(header, report.Content);
}

public string GenerateHtmlReport(Report report)
{
var header = BuildHeader(report);
return GenerateHtml(header, report.Content);
}
}

10. 暗号化の弱い実装

問題のあるコード:

public class PasswordHasher
{
// S4790: 弱いハッシュアルゴリズム(MD5)を使用
public string HashPassword(string password)
{
using (var md5 = MD5.Create())
{
var bytes = Encoding.UTF8.GetBytes(password);
var hash = md5.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
}
}

修正後:

public class PasswordHasher
{
public string HashPassword(string password)
{
// bcrypt、PBKDF2、またはArgon2を使用
return BCrypt.Net.BCrypt.HashPassword(password);
}

public bool VerifyPassword(string password, string hash)
{
return BCrypt.Net.BCrypt.Verify(password, hash);
}
}

これらの例は、SonarQubeが検出する問題の一部です。各問題には固有のルールID(例: S2259、S3649)が付与され、詳細な説明と修正方法がSonarQubeのダッシュボードで確認できます。

アーキテクチャ

┌─────────────────┐
│ 開発者のIDE │
│ (SonarLint) │
└────────┬────────┘


┌─────────────────┐
│ CI/CDパイプ │
│ (SonarScanner) │
└────────┬────────┘


┌─────────────────┐
│ SonarQubeサーバ │
│ ・分析エンジン │
│ ・データベース │
│ ・Webダッシュ │
└─────────────────┘

主要コンポーネント

  1. SonarQube Server

    • Webサーバー(ダッシュボード)
    • 検索サーバー(Elasticsearch)
    • コンピュートエンジン(バックグラウンド処理)
  2. SonarScanner

    • コードスキャンを実行するクライアントツール
    • CI/CDパイプラインに統合
  3. SonarLint

    • IDE統合プラグイン
    • リアルタイムでコード品質をチェック
  4. Database

    • 分析結果とプロジェクト設定を保存
    • PostgreSQL、Microsoft SQL Server、Oracleをサポート

セットアップ

Docker を使用したクイックスタート

# SonarQubeコンテナを起動
docker run -d --name sonarqube \
-p 9000:9000 \
sonarqube:lts-community

# ブラウザで http://localhost:9000 にアクセス
# 初期認証情報: admin / admin

プロジェクトスキャンの実行

.NET プロジェクトの場合

# SonarScanner for .NET をインストール
dotnet tool install --global dotnet-sonarscanner

# スキャン開始
dotnet sonarscanner begin \
/k:"project-key" \
/d:sonar.host.url="http://localhost:9000" \
/d:sonar.login="your-token"

# ビルド実行
dotnet build

# スキャン完了
dotnet sonarscanner end /d:sonar.login="your-token"

JavaScript/TypeScript プロジェクトの場合

# sonar-scannerをインストール
npm install -g sonar-scanner

# プロジェクトルートにsonar-project.propertiesを作成
# sonar.projectKey=my-project
# sonar.sources=src
# sonar.host.url=http://localhost:9000
# sonar.login=your-token

# スキャン実行
sonar-scanner

Java/Maven プロジェクトの場合

# pom.xmlにSonarQubeプラグインを追加済みの場合
mvn clean verify sonar:sonar \
-Dsonar.projectKey=my-project \
-Dsonar.host.url=http://localhost:9000 \
-Dsonar.login=your-token

CI/CD統合

GitHub Actions

name: SonarQube Analysis

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
sonarqube:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # 履歴全体を取得

- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

Azure DevOps

# azure-pipelines.yml
trigger:
- main

pool:
vmImage: 'ubuntu-latest'

steps:
- task: SonarQubePrepare@5
inputs:
SonarQube: 'SonarQubeServiceConnection'
scannerMode: 'MSBuild'
projectKey: 'my-project'

- task: DotNetCoreCLI@2
inputs:
command: 'build'

- task: SonarQubeAnalyze@5

- task: SonarQubePublish@5
inputs:
pollingTimeoutSec: '300'

品質ゲート

品質ゲートは、プロジェクトがリリース可能かどうかを判定する基準です。

デフォルトの品質ゲート条件

  • 新しいコードのカバレッジ >= 80%
  • 重複コード < 3%
  • セキュリティホットスポットのレビュー率 = 100%
  • 新しいコードの信頼性評価 = A
  • 新しいコードのセキュリティ評価 = A
  • 新しいコードの保守性評価 = A

カスタム品質ゲートの作成

品質ゲート > 作成 > 条件を追加
- メトリクスを選択(例: バグ、脆弱性、コードスメル)
- 演算子を選択(例: より大きい、より小さい)
- しきい値を設定

ベストプラクティス

1. 継続的な統合

  • すべてのプルリクエストでSonarQube分析を実行
  • 品質ゲートが失敗した場合はマージをブロック
  • 定期的にmainブランチをスキャン

2. 新しいコードに焦点を当てる

  • "Clean as You Code"アプローチを採用
  • 既存の問題よりも新しいコードの品質を優先
  • レガシーコードは段階的に改善

3. セキュリティを最優先

  • セキュリティホットスポットを定期的にレビュー
  • 脆弱性は即座に修正
  • セキュリティレポートを活用

4. チーム全体での活用

  • SonarLintをIDEに統合し、リアルタイムフィードバックを活用
  • コードレビュー時にSonarQubeの結果を参照
  • 品質指標をチームで共有

5. カスタムルールの設定

  • プロジェクト固有のコーディング規約を反映
  • 不要なルールは無効化
  • 重要なルールの重要度を調整

SonarQube vs SonarCloud

特徴SonarQubeSonarCloud
ホスティングセルフホストクラウド(SaaS)
料金Community版は無料パブリックリポジトリは無料
セットアップサーバーインストール必要すぐに利用可能
カスタマイズ完全な制御制限あり
メンテナンス自己管理SonarSource管理
適用対象プライベートコード向けOSS・クラウドネイティブ向け

トラブルシューティング

よくある問題

Q: スキャンが失敗する

# ログを確認
docker logs sonarqube

# メモリ不足の場合
docker run -d --name sonarqube \
-e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true \
-p 9000:9000 \
sonarqube:lts-community

Q: カバレッジが表示されない

  • カバレッジツールの実行を確認(例: dotnet test --collect:"XPlat Code Coverage")
  • sonar.cs.opencover.reportsPaths パラメータでレポートパスを指定

Q: 分析時間が長い

  • 増分分析を有効化
  • 不要なファイルを除外(sonar.exclusions設定)
  • コンピュートエンジンのスレッド数を増やす

参考リンク

まとめ

SonarQubeは、コード品質とセキュリティを継続的に改善するための強力なツールです。CI/CDパイプラインに統合することで、新しいコードの品質を維持し、技術的負債を可視化できます。"Clean as You Code"の原則に従い、チーム全体でコード品質向上に取り組むことが重要です。