콘텐츠로 이동

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 통합에 필수적입니다.*