SonarQube
Overview
SonarQube is an open-source platform for continuously inspecting code quality and security. Through static code analysis, it detects bugs, vulnerabilities, and code smells, visualizing technical debt.
Key Features
1. Code Quality Analysis
- Bug Detection: Identify code issues that could lead to runtime errors
- Code Smells: Detect code patterns that reduce maintainability
- Duplicate Code: Identify duplication within the codebase
- Complexity Measurement: Calculate metrics such as cyclomatic complexity
2. Security Analysis
- Vulnerability Detection: Identify security vulnerabilities such as OWASP Top 10
- Security Hotspots: Point out security concerns that need review
- CWE/SANS Top 25 Support: Analysis based on common vulnerability patterns
3. Visualization of Technical Debt
- Quantification of Debt: Estimate time required to fix issues
- Quality Gate: Determine if the project meets quality standards
- Trend Analysis: Track changes in code quality over time
Supported Languages
SonarQube supports numerous programming languages:
- Java, C#, JavaScript, TypeScript
- Python, PHP, Ruby, Go
- C, C++, Objective-C, Swift
- Kotlin, Scala, HTML, CSS
- SQL, XML, YAML, etc.
Examples of Detected Issues (.NET)
Here are typical code quality and security issues detected by SonarQube, with C# code examples.
1. Null Reference Issues
Problematic Code:
public class UserService
{
public string GetUserName(User user)
{
// S2259: user might be null but is not checked
return user.Name.ToUpper();
}
}
Fixed:
public class UserService
{
public string GetUserName(User user)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
return user.Name?.ToUpper() ?? string.Empty;
}
}
2. Exception Handling Issues
Problematic Code:
public class DataProcessor
{
public void ProcessData()
{
try
{
// Processing
}
catch (Exception ex)
{
// S2737: Empty catch block - exception is ignored
}
}
public void AnotherMethod()
{
try
{
// Processing
}
catch (Exception ex)
{
// S1181: Should not catch generic Exception
Console.WriteLine(ex.Message);
}
}
}
Fixed:
public class DataProcessor
{
private readonly ILogger<DataProcessor> _logger;
public void ProcessData()
{
try
{
// Processing
}
catch (InvalidOperationException ex)
{
// Catch specific exception and handle appropriately
_logger.LogError(ex, "Error occurred during data processing");
throw; // Rethrow if necessary
}
}
}
3. Resource Leaks
Problematic Code:
public class FileHandler
{
public string ReadFile(string path)
{
// S2930: Not using using statement
var reader = new StreamReader(path);
return reader.ReadToEnd();
// reader is not disposed
}
}
Fixed:
public class FileHandler
{
public string ReadFile(string path)
{
using (var reader = new StreamReader(path))
{
return reader.ReadToEnd();
}
// Or use using declaration
// using var reader = new StreamReader(path);
// return reader.ReadToEnd();
}
}
4. Security Vulnerability - SQL Injection
Problematic Code:
public class UserRepository
{
private readonly SqlConnection _connection;
public User GetUser(string username)
{
// S3649: SQL Injection vulnerability
var query = $"SELECT * FROM Users WHERE Username = '{username}'";
var command = new SqlCommand(query, _connection);
// Attacker can input "admin' OR '1'='1" in username
return ExecuteQuery(command);
}
}
Fixed:
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. Security Vulnerability - Hardcoded Credentials
Problematic Code:
public class DatabaseConnection
{
// S2068: Hardcoded password
private const string ConnectionString =
"Server=myserver;Database=mydb;User=admin;Password=P@ssw0rd123;";
public SqlConnection GetConnection()
{
return new SqlConnection(ConnectionString);
}
}
Fixed:
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. High Code Complexity
Problematic Code:
public class OrderValidator
{
// S1541: Cognitive complexity is too high (nesting is deep)
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)
{
// Processing
}
else
{
return false;
}
}
else
{
return false;
}
}
}
}
}
}
return true;
}
}
Fixed:
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. Dead Code
Problematic Code:
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
// S1144: Unused private method
private int Subtract(int a, int b)
{
return a - b;
}
public int Multiply(int a, int b)
{
int unused = 10; // S1481: Unused local variable
return a * b;
}
}
Fixed:
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public int Multiply(int a, int b)
{
return a * b;
}
// Remove unnecessary code
}
8. Misuse of Async Methods
Problematic Code:
public class ApiClient
{
private readonly HttpClient _httpClient;
public string GetData(string url)
{
// S4462: Waiting synchronously for async method (Risk of deadlock)
var response = _httpClient.GetAsync(url).Result;
return response.Content.ReadAsStringAsync().Result;
}
}
Fixed:
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. Duplicate Code
Problematic Code:
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 generation processing
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 generation processing
return GenerateHtml(header, report.Content);
}
}
Fixed:
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. Weak Encryption Implementation
Problematic Code:
public class PasswordHasher
{
// S4790: Using weak hashing algorithm (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);
}
}
}
Fixed:
public class PasswordHasher
{
public string HashPassword(string password)
{
// Use bcrypt, PBKDF2, or Argon2
return BCrypt.Net.BCrypt.HashPassword(password);
}
public bool VerifyPassword(string password, string hash)
{
return BCrypt.Net.BCrypt.Verify(password, hash);
}
}
These examples are some of the issues detected by SonarQube. Each issue is assigned a unique rule ID (e.g., S2259, S3649), and detailed descriptions and remediation methods can be checked on the SonarQube dashboard.
Architecture
┌─────────────────┐
│ Developer IDE │
│ (SonarLint) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ CI/CD Pipe │
│ (SonarScanner) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ SonarQube Server│
│ ・Analysis Eng │
│ ・Database │
│ ・Web Dash │
└─────────────────┘
Key Components
-
SonarQube Server
- Web Server (Dashboard)
- Search Server (Elasticsearch)
- Compute Engine (Background processing)
-
SonarScanner
- Client tool to execute code scan
- Integrated into CI/CD pipeline
-
SonarLint
- IDE integration plugin
- Check code quality in real-time
-
Database
- Store analysis results and project settings
- Supports PostgreSQL, Microsoft SQL Server, Oracle
Setup
Quick Start using Docker
# Start SonarQube container
docker run -d --name sonarqube \
-p 9000:9000 \
sonarqube:lts-community
# Access http://localhost:9000 in browser
# Initial credentials: admin / admin
Running Project Scan
For .NET Projects
# Install SonarScanner for .NET
dotnet tool install --global dotnet-sonarscanner
# Start scan
dotnet sonarscanner begin \
/k:"project-key" \
/d:sonar.host.url="http://localhost:9000" \
/d:sonar.login="your-token"
# Execute build
dotnet build
# End scan
dotnet sonarscanner end /d:sonar.login="your-token"
For JavaScript/TypeScript Projects
# Install sonar-scanner
npm install -g sonar-scanner
# Create sonar-project.properties in project root
# sonar.projectKey=my-project
# sonar.sources=src
# sonar.host.url=http://localhost:9000
# sonar.login=your-token
# Execute scan
sonar-scanner
For Java/Maven Projects
# If SonarQube plugin is added to pom.xml
mvn clean verify sonar:sonar \
-Dsonar.projectKey=my-project \
-Dsonar.host.url=http://localhost:9000 \
-Dsonar.login=your-token
CI/CD Integration
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 # Fetch entire history
- 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'
Quality Gate
Quality Gate is a criterion to determine if a project is releasable.
Default Quality Gate Conditions
- Coverage on New Code >= 80%
- Duplicated Lines on New Code < 3%
- Security Hotspots Reviewed on New Code = 100%
- Reliability Rating on New Code = A
- Security Rating on New Code = A
- Maintainability Rating on New Code = A
Creating Custom Quality Gate
Quality Gates > Create > Add Condition
- Select Metric (e.g., Bugs, Vulnerabilities, Code Smells)
- Select Operator (e.g., is greater than, is less than)
- Set Threshold
Best Practices
1. Continuous Integration
- Run SonarQube analysis on every pull request
- Block merge if Quality Gate fails
- Regularly scan main branch
2. Focus on New Code
- Adopt "Clean as You Code" approach
- Prioritize quality of new code over existing issues
- Improve legacy code incrementally
3. Security First
- Regularly review Security Hotspots
- Fix vulnerabilities immediately
- Leverage security reports
4. Team-wide Usage
- Integrate SonarLint into IDE and leverage real-time feedback
- Refer to SonarQube results during code reviews
- Share quality metrics with the team
5. Custom Rule Configuration
- Reflect project-specific coding conventions
- Disable unnecessary rules
- Adjust severity of important rules
SonarQube vs SonarCloud
| Feature | SonarQube | SonarCloud |
|---|---|---|
| Hosting | Self-hosted | Cloud (SaaS) |
| Pricing | Community Edition is free | Public repositories are free |
| Setup | Server installation required | Available immediately |
| Customization | Full control | Limited |
| Maintenance | Self-managed | Managed by SonarSource |
| Target | For private code | For OSS/Cloud Native |
Troubleshooting
Common Issues
Q: Scan fails
# Check logs
docker logs sonarqube
# If out of memory
docker run -d --name sonarqube \
-e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true \
-p 9000:9000 \
sonarqube:lts-community
Q: Coverage is not displayed
- Check execution of coverage tool (e.g., dotnet test --collect:"XPlat Code Coverage")
- Specify report path with sonar.cs.opencover.reportsPaths parameter
Q: Analysis time is long
- Enable incremental analysis
- Exclude unnecessary files (sonar.exclusions setting)
- Increase number of Compute Engine threads
Reference Links
Summary
SonarQube is a powerful tool for continuously improving code quality and security. By integrating it into the CI/CD pipeline, you can maintain the quality of new code and visualize technical debt. It is important to follow the "Clean as You Code" principle and work on improving code quality as a whole team.