SonarQube 코드 품질 및 보안 플랫폼 치트 시트
개요
SonarQube는 코드 품질과 보안에 대한 지속적인 검사를 위한 포괄적인 플랫폼입니다. 25개 이상의 프로그래밍 언어에 걸쳐 정적 분석을 통해 버그, 코드 냄새, 보안 취약점을 자동으로 검토합니다. SonarQube는 DevSecOps 파이프라인에서 코드 품질 표준을 유지하고 개발 라이프사이클 초기에 보안 문제를 식별하는 데 필수적입니다.
⚠️ 참고: SonarQube는 최적의 결과를 위해 적절한 구성 및 규칙 사용자 정의가 필요합니다. CI/CD 파이프라인에 지속적인 모니터링을 위해 통합되어야 합니다.
설치
Docker 설치 (권장)
# Pull SonarQube image
docker pull sonarqube:community
# Run SonarQube with PostgreSQL
docker run -d --name sonarqube \
-p 9000:9000 \
-e SONAR_JDBC_URL=jdbc:postgresql://db:5432/sonar \
-e SONAR_JDBC_USERNAME=sonar \
-e SONAR_JDBC_PASSWORD=sonar \
sonarqube:community
# Using Docker Compose
cat > docker-compose.yml << 'EOF'
version: "3"
services:
sonarqube:
image: sonarqube:community
depends_on:
- db
environment:
SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonar
SONAR_JDBC_USERNAME: sonar
SONAR_JDBC_PASSWORD: sonar
ports:
- "9000:9000"
volumes:
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
db:
image: postgres:13
environment:
POSTGRES_USER: sonar
POSTGRES_PASSWORD: sonar
POSTGRES_DB: sonar
volumes:
- postgresql:/var/lib/postgresql
- postgresql_data:/var/lib/postgresql/data
volumes:
sonarqube_data:
sonarqube_extensions:
sonarqube_logs:
postgresql:
postgresql_data:
EOF
docker-compose up -d
수동 설치
# Download SonarQube
wget https://binaries.sonarsource.com/Distribution/sonarqube/sonarqube-9.9.0.65466.zip
unzip sonarqube-9.9.0.65466.zip
cd sonarqube-9.9.0.65466
# Configure database (PostgreSQL recommended)
# Edit conf/sonar.properties
cat >> conf/sonar.properties << 'EOF'
sonar.jdbc.username=sonar
sonar.jdbc.password=sonar
sonar.jdbc.url=jdbc:postgresql://localhost/sonar
EOF
# Start SonarQube
bin/linux-x86-64/sonar.sh start
# Check status
bin/linux-x86-64/sonar.sh status
SonarScanner 설치
# Download SonarScanner
wget https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.8.0.2856-linux.zip
unzip sonar-scanner-cli-4.8.0.2856-linux.zip
sudo mv sonar-scanner-4.8.0.2856-linux /opt/sonar-scanner
# Add to PATH
echo 'export PATH="/opt/sonar-scanner/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
# Verify installation
sonar-scanner --version
패키지 관리자 설치
# Ubuntu/Debian
wget -qO - https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.8.0.2856-linux.zip
# macOS with Homebrew
brew install sonar-scanner
# Using npm
npm install -g sonarqube-scanner
기본 구성
SonarQube 서버 구성
# conf/sonar.properties
# Database configuration
sonar.jdbc.username=sonar
sonar.jdbc.password=sonar
sonar.jdbc.url=jdbc:postgresql://localhost/sonar
# Web server configuration
sonar.web.host=0.0.0.0
sonar.web.port=9000
sonar.web.context=/sonar
# Elasticsearch configuration
sonar.search.javaOpts=-Xmx512m -Xms512m -XX:MaxDirectMemorySize=256m
# Security configuration
sonar.security.realm=LDAP
sonar.authenticator.downcase=true
# Quality gate configuration
sonar.qualitygate.wait=true
프로젝트 구성 (sonar-project.properties)
# sonar-project.properties
# Project identification
sonar.projectKey=my-project
sonar.projectName=My Project
sonar.projectVersion=1.0
# Source code configuration
sonar.sources=src
sonar.tests=tests
sonar.sourceEncoding=UTF-8
# Language-specific settings
sonar.java.source=11
sonar.java.target=11
sonar.java.binaries=target/classes
sonar.java.libraries=target/dependency/*.jar
# Exclusions
sonar.exclusions=**/*test*/**,**/*.min.js,**/vendor/**
sonar.test.exclusions=**/*test*/**
# Coverage reports
sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
sonar.javascript.lcov.reportPaths=coverage/lcov.info
기본 사용법
SonarScanner 실행
# Basic scan
sonar-scanner
# Scan with custom properties
sonar-scanner \
-Dsonar.projectKey=my-project \
-Dsonar.sources=. \
-Dsonar.host.url=http://localhost:9000 \
-Dsonar.login=your-token
# Scan specific directory
sonar-scanner -Dsonar.sources=src/main/java
# Scan with exclusions
sonar-scanner \
-Dsonar.exclusions="**/*test*/**,**/*.min.js" \
-Dsonar.test.exclusions="**/*test*/**"
# Debug mode
sonar-scanner -X
인증
# Generate authentication token
curl -u admin:admin -X POST "http://localhost:9000/api/user_tokens/generate?name=my-token"
# Use token in scan
sonar-scanner -Dsonar.login=your-generated-token
# Use username/password (not recommended)
sonar-scanner -Dsonar.login=admin -Dsonar.password=admin
품질 게이트
# Check quality gate status
curl -u token: "http://localhost:9000/api/qualitygates/project_status?projectKey=my-project"
# Set quality gate for project
curl -u token: -X POST "http://localhost:9000/api/qualitygates/select?projectKey=my-project&gateId=1"
# Create custom quality gate
curl -u token: -X POST "http://localhost:9000/api/qualitygates/create?name=Custom+Gate"
언어별 구성
Java 프로젝트
# Maven integration
sonar.java.source=11
sonar.java.target=11
sonar.java.binaries=target/classes
sonar.java.libraries=target/dependency/*.jar
sonar.java.test.binaries=target/test-classes
sonar.java.test.libraries=target/dependency/*.jar
sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.9.1.2184</version>
</plugin>
# Maven scan
mvn clean verify sonar:sonar \
-Dsonar.projectKey=my-project \
-Dsonar.host.url=http://localhost:9000 \
-Dsonar.login=your-token
JavaScript/TypeScript 프로젝트
# JavaScript configuration
sonar.sources=src
sonar.tests=test
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.typescript.lcov.reportPaths=coverage/lcov.info
sonar.exclusions=node_modules/**,dist/**,build/**
// package.json script
\\\\{
"scripts": \\\\{
"sonar": "sonar-scanner"
\\\\}
\\\\}
Python 프로젝트
# Python configuration
sonar.sources=src
sonar.tests=tests
sonar.python.coverage.reportPaths=coverage.xml
sonar.python.xunit.reportPath=test-reports/xunit.xml
C# 프로젝트
# C# configuration
sonar.cs.dotcover.reportsPaths=dotCover.html
sonar.cs.nunit.reportsPaths=TestResult.xml
sonar.cs.opencover.reportsPaths=opencover.xml
CI/CD 통합
GitHub Actions
# .github/workflows/sonarqube.yml
name: SonarQube Analysis
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
sonarqube:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Cache SonarQube packages
uses: actions/cache@v3
with:
path: ~/.sonar/cache
key: $\\\\{\\\\{ runner.os \\\\}\\\\}-sonar
restore-keys: $\\\\{\\\\{ runner.os \\\\}\\\\}-sonar
- name: Cache Maven packages
uses: actions/cache@v3
with:
path: ~/.m2
key: $\\\\{\\\\{ runner.os \\\\}\\\\}-m2-$\\\\{\\\\{ hashFiles('**/pom.xml') \\\\}\\\\}
restore-keys: $\\\\{\\\\{ runner.os \\\\}\\\\}-m2
- name: Build and analyze
env:
GITHUB_TOKEN: $\\\\{\\\\{ secrets.GITHUB_TOKEN \\\\}\\\\}
SONAR_TOKEN: $\\\\{\\\\{ secrets.SONAR_TOKEN \\\\}\\\\}
run: mvn clean verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar
GitLab CI
# .gitlab-ci.yml
stages:
- test
- sonarqube
sonarqube-check:
stage: sonarqube
image: maven:3.6.3-jdk-11
variables:
SONAR_USER_HOME: "$\\\\{CI_PROJECT_DIR\\\\}/.sonar"
GIT_DEPTH: "0"
cache:
key: "$\\\\{CI_JOB_NAME\\\\}"
paths:
- .sonar/cache
script:
- mvn verify sonar:sonar
allow_failure: true
only:
- merge_requests
- master
- develop
Jenkins 파이프라인
// Jenkinsfile
pipeline \\\\{
agent any
tools \\\\{
maven 'Maven-3.6.3'
jdk 'JDK-11'
\\\\}
environment \\\\{
SONAR_TOKEN = credentials('sonar-token')
\\\\}
stages \\\\{
stage('Checkout') \\\\{
steps \\\\{
checkout scm
\\\\}
\\\\}
stage('Build') \\\\{
steps \\\\{
sh 'mvn clean compile'
\\\\}
\\\\}
stage('Test') \\\\{
steps \\\\{
sh 'mvn test'
\\\\}
post \\\\{
always \\\\{
junit 'target/surefire-reports/*.xml'
\\\\}
\\\\}
\\\\}
stage('SonarQube Analysis') \\\\{
steps \\\\{
withSonarQubeEnv('SonarQube') \\\\{
sh 'mvn sonar:sonar'
\\\\}
\\\\}
\\\\}
stage('Quality Gate') \\\\{
steps \\\\{
timeout(time: 1, unit: 'HOURS') \\\\{
waitForQualityGate abortPipeline: true
\\\\}
\\\\}
\\\\}
\\\\}
\\\\}
Azure DevOps
# azure-pipelines.yml
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
MAVEN_CACHE_FOLDER: $(Pipeline.Workspace)/.m2/repository
MAVEN_OPTS: '-Dmaven.repo.local=$(MAVEN_CACHE_FOLDER)'
steps:
- task: Cache@2
inputs:
key: 'maven|"$(Agent.OS)"|**/pom.xml'
restoreKeys:|
maven|"$(Agent.OS)"
maven
path: $(MAVEN_CACHE_FOLDER)
displayName: Cache Maven local repo
- task: SonarQubePrepare@4
inputs:
SonarQube: 'SonarQube'
scannerMode: 'Other'
- task: Maven@3
inputs:
mavenPomFile: 'pom.xml'
goals: 'clean verify'
options: '-Dmaven.repo.local=$(MAVEN_CACHE_FOLDER)'
javaHomeOption: 'JDKVersion'
jdkVersionOption: '1.11'
- task: SonarQubeAnalyze@4
- task: SonarQubePublish@4
inputs:
pollingTimeoutSec: '300'
고급 구성
사용자 정의 규칙 및 품질 프로필
# Export quality profile
curl -u token: "http://localhost:9000/api/qualityprofiles/export?qualityProfile=MyProfile&language=java" > my-profile.xml
# Import quality profile
curl -u token: -X POST -F "backup=@my-profile.xml" "http://localhost:9000/api/qualityprofiles/restore"
# Create custom rule
curl -u token: -X POST \
"http://localhost:9000/api/rules/create" \
-d "custom_key=my-custom-rule" \
-d "name=My Custom Rule" \
-d "markdown_description=This is my custom rule" \
-d "severity=MAJOR" \
-d "status=READY" \
-d "template_key=java:S124"
웹훅 구성```bash
Create webhook
curl -u token: -X POST
“http://localhost:9000/api/webhooks/create”
-d “name=MyWebhook”
-d “url=https://my-server.com/webhook”
-d “project=my-project”
Test webhook
curl -u token: -X POST
“http://localhost:9000/api/webhooks/deliveries”
-d “webhook=webhook-id”
```bash
# Analyze main branch
sonar-scanner \
-Dsonar.projectKey=my-project \
-Dsonar.branch.name=main
# Analyze feature branch
sonar-scanner \
-Dsonar.projectKey=my-project \
-Dsonar.branch.name=feature/new-feature \
-Dsonar.branch.target=main
# Analyze pull request
sonar-scanner \
-Dsonar.projectKey=my-project \
-Dsonar.pullrequest.key=123 \
-Dsonar.pullrequest.branch=feature/new-feature \
-Dsonar.pullrequest.base=main
```## 보안 분석
### 보안 핫스팟
```bash
# Get security hotspots
curl -u token: "http://localhost:9000/api/hotspots/search?projectKey=my-project"
# Change hotspot status
curl -u token: -X POST \
"http://localhost:9000/api/hotspots/change_status" \
-d "hotspot=hotspot-key" \
-d "status=REVIEWED" \
-d "resolution=SAFE"
# Add comment to hotspot
curl -u token: -X POST \
"http://localhost:9000/api/hotspots/add_comment" \
-d "hotspot=hotspot-key" \
-d "comment=This has been reviewed and is safe"
```### 보안 규칙 구성
```properties
# Enable security rules
sonar.security.hotspots.enabled=true
sonar.security.review.enabled=true
# OWASP Top 10 categories
sonar.security.owasp-a1.enabled=true
sonar.security.owasp-a2.enabled=true
sonar.security.owasp-a3.enabled=true
sonar.security.owasp-a4.enabled=true
sonar.security.owasp-a5.enabled=true
sonar.security.owasp-a6.enabled=true
sonar.security.owasp-a7.enabled=true
sonar.security.owasp-a8.enabled=true
sonar.security.owasp-a9.enabled=true
sonar.security.owasp-a10.enabled=true
# SANS Top 25 categories
sonar.security.sans-top25.enabled=true
```## 자동화 및 스크립팅
### 자동화된 품질 게이트 확인
```python
#!/usr/bin/env python3
# sonar_quality_gate.py
import requests
import sys
import time
import argparse
class SonarQubeQualityGate:
def __init__(self, server_url, token, project_key):
self.server_url = server_url.rstrip('/')
self.token = token
self.project_key = project_key
self.session = requests.Session()
self.session.auth = (token, '')
def get_project_status(self):
"""Get project quality gate status"""
url = f"\\\\{self.server_url\\\\}/api/qualitygates/project_status"
params = \\\\{'projectKey': self.project_key\\\\}
try:
response = self.session.get(url, params=params)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
print(f"Error getting project status: \\\\{e\\\\}")
return None
def wait_for_analysis(self, timeout=300, interval=10):
"""Wait for analysis to complete"""
start_time = time.time()
while time.time() - start_time < timeout:
status = self.get_project_status()
if status and 'projectStatus' in status:
project_status = status['projectStatus']
if project_status.get('status') != 'IN_PROGRESS':
return status
print(f"Analysis in progress... waiting \\\\{interval\\\\} seconds")
time.sleep(interval)
else:
print("Could not get project status")
time.sleep(interval)
print(f"Timeout waiting for analysis to complete (\\\\{timeout\\\\}s)")
return None
def check_quality_gate(self):
"""Check if quality gate passes"""
status = self.get_project_status()
if not status:
return False, "Could not get project status"
project_status = status.get('projectStatus', \\\\{\\\\})
gate_status = project_status.get('status')
if gate_status == 'OK':
return True, "Quality gate passed"
elif gate_status == 'ERROR':
conditions = project_status.get('conditions', [])
failed_conditions = [c for c in conditions if c.get('status') == 'ERROR']
error_msg = "Quality gate failed:\n"
for condition in failed_conditions:
metric = condition.get('metricKey', 'Unknown')
actual = condition.get('actualValue', 'N/A')
threshold = condition.get('errorThreshold', 'N/A')
error_msg += f" - \\\\{metric\\\\}: \\\\{actual\\\\} (threshold: \\\\{threshold\\\\})\n"
return False, error_msg
else:
return False, f"Unknown quality gate status: \\\\{gate_status\\\\}"
def get_metrics(self):
"""Get project metrics"""
url = f"\\\\{self.server_url\\\\}/api/measures/component"
params = \\\\{
'component': self.project_key,
'metricKeys': 'bugs,vulnerabilities,code_smells,coverage,duplicated_lines_density'
\\\\}
try:
response = self.session.get(url, params=params)
response.raise_for_status()
data = response.json()
metrics = \\\\{\\\\}
for measure in data.get('component', \\\\{\\\\}).get('measures', []):
metrics[measure['metric']] = measure.get('value', '0')
return metrics
except requests.RequestException as e:
print(f"Error getting metrics: \\\\{e\\\\}")
return \\\\{\\\\}
def print_summary(self):
"""Print analysis summary"""
metrics = self.get_metrics()
print("\n=== SonarQube Analysis Summary ===")
print(f"Project: \\\\{self.project_key\\\\}")
print(f"Bugs: \\\\{metrics.get('bugs', 'N/A')\\\\}")
print(f"Vulnerabilities: \\\\{metrics.get('vulnerabilities', 'N/A')\\\\}")
print(f"Code Smells: \\\\{metrics.get('code_smells', 'N/A')\\\\}")
print(f"Coverage: \\\\{metrics.get('coverage', 'N/A')\\\\}%")
print(f"Duplicated Lines: \\\\{metrics.get('duplicated_lines_density', 'N/A')\\\\}%")
def main():
parser = argparse.ArgumentParser(description='SonarQube Quality Gate Checker')
parser.add_argument('--server', required=True, help='SonarQube server URL')
parser.add_argument('--token', required=True, help='Authentication token')
parser.add_argument('--project', required=True, help='Project key')
parser.add_argument('--wait', action='store_true', help='Wait for analysis to complete')
parser.add_argument('--timeout', type=int, default=300, help='Timeout in seconds')
args = parser.parse_args()
qg = SonarQubeQualityGate(args.server, args.token, args.project)
if args.wait:
print("Waiting for analysis to complete...")
status = qg.wait_for_analysis(timeout=args.timeout)
if not status:
print("Analysis did not complete within timeout")
sys.exit(1)
qg.print_summary()
passed, message = qg.check_quality_gate()
print(f"\n\\\\{message\\\\}")
sys.exit(0 if passed else 1)
if __name__ == '__main__':
main()
```### 대량 프로젝트 관리
```bash
#!/bin/bash
# bulk_sonar_scan.sh
# Configuration
SONAR_SERVER="http://localhost:9000"
SONAR_TOKEN="your-token"
PROJECTS_DIR="/path/to/projects"
REPORTS_DIR="/path/to/reports"
# Function to scan project
scan_project() \\\\{
local project_path="$1"
local project_name=$(basename "$project_path")
echo "Scanning $project_name..."
cd "$project_path"
# Create sonar-project.properties if it doesn't exist
if [ ! -f "sonar-project.properties" ]; then
cat > sonar-project.properties << EOF
sonar.projectKey=$project_name
sonar.projectName=$project_name
sonar.projectVersion=1.0
sonar.sources=.
sonar.exclusions=**/node_modules/**,**/target/**,**/build/**,**/.git/**
EOF
fi
# Run scan
sonar-scanner \
-Dsonar.host.url="$SONAR_SERVER" \
-Dsonar.login="$SONAR_TOKEN" \
-Dsonar.projectKey="$project_name"
# Check quality gate
python3 sonar_quality_gate.py \
--server "$SONAR_SERVER" \
--token "$SONAR_TOKEN" \
--project "$project_name" \
--wait
if [ $? -ne 0 ]; then
echo "$project_name" >> "$REPORTS_DIR/failed_quality_gates.txt"
fi
\\\\}
# Create reports directory
mkdir -p "$REPORTS_DIR"
# Scan all projects
find "$PROJECTS_DIR" -maxdepth 1 -type d|while read -r project_dir; do
if [ "$project_dir" != "$PROJECTS_DIR" ]; then
scan_project "$project_dir"
fi
done
echo "Bulk scanning completed. Check $REPORTS_DIR for results."
```## 모범 사례
### 품질 게이트 구성
```json
\\\\{
"name": "Strict Security Gate",
"conditions": [
\\\\{
"metric": "new_vulnerabilities",
"op": "GT",
"error": "0"
\\\\},
\\\\{
"metric": "new_bugs",
"op": "GT",
"error": "0"
\\\\},
\\\\{
"metric": "new_security_hotspots_reviewed",
"op": "LT",
"error": "100"
\\\\},
\\\\{
"metric": "new_coverage",
"op": "LT",
"error": "80"
\\\\},
\\\\{
"metric": "new_duplicated_lines_density",
"op": "GT",
"error": "3"
\\\\}
]
\\\\}
```### 프로젝트 조직
```properties
# Multi-module project configuration
sonar.projectKey=my-company:my-project
sonar.projectName=My Project
sonar.projectVersion=1.0
# Module configuration
sonar.modules=module1,module2,module3
module1.sonar.projectName=Module 1
module1.sonar.sources=module1/src
module1.sonar.tests=module1/test
module2.sonar.projectName=Module 2
module2.sonar.sources=module2/src
module2.sonar.tests=module2/test
module3.sonar.projectName=Module 3
module3.sonar.sources=module3/src
module3.sonar.tests=module3/test
```### 성능 최적화
```properties
# Performance tuning
sonar.scanner.metadataFilePath=.scannerwork/report-task.txt
sonar.working.directory=.scannerwork
sonar.scm.disabled=false
sonar.scm.provider=git
# Memory settings
sonar.scanner.javaOpts=-Xmx2048m -XX:MaxPermSize=256m
# Parallel processing
sonar.scanner.dumpToFile=.scannerwork/scanner-report.json
```## 문제 해결
### 일반적인 문제
```bash
# Issue: Out of memory errors
# Solution: Increase memory allocation
export SONAR_SCANNER_OPTS="-Xmx2048m"
# Issue: Analysis taking too long
# Solution: Exclude unnecessary files
sonar-scanner -Dsonar.exclusions="**/node_modules/**,**/target/**,**/build/**"
# Issue: Quality gate webhook not working
# Solution: Check webhook configuration and network connectivity
curl -X POST https://your-webhook-url.com/webhook -d '\\\\{"test": "data"\\\\}'
# Issue: Branch analysis not working
# Solution: Ensure proper branch configuration
sonar-scanner -Dsonar.branch.name=feature-branch -Dsonar.branch.target=main
```### 디버그 모드
```bash
# Enable debug logging
sonar-scanner -X
# Check scanner logs
tail -f .scannerwork/scanner-report.log
# Verify server connectivity
curl -u token: "http://localhost:9000/api/system/status"
```## 리소스
- [SonarQube 공식 문서](https://docs.sonarqube.org/)
- [SonarQube GitHub 저장소](https://github.com/SonarSource/sonarqube)
- [SonarQube 커뮤니티](https://community.sonarsource.com/)
- [SonarQube 규칙](https://rules.sonarsource.com/)
- [SonarQube 플러그인](https://docs.sonarqube.org/latest/instance-administration/plugin-version-matrix/)
---
*이 치트 시트는 코드 품질과 보안을 유지하기 위해 SonarQube를 사용하는 포괄적인 가이드를 제공합니다. 정기적인 모니터링과 적절한 구성은 효과적인 DevSecOps 통합에 필수적입니다.*