SonarQube 정적 보안 분석 파이프라인 추가
- Jenkinsfile.sonar: 분석 전용 파이프라인 (컴파일 JDK8 / 스캐너 JDK17 분리, Quality Gate 실패는 UNSTABLE 처리, 토큰은 환경변수로만 전달) - sonar-project.properties: 분석 대상/제외 규칙, Java 8 소스 설정 - ci/sonar-classpath.gradle: build.gradle 무수정으로 sonar.java.libraries 덤프 (타입 해석용 컴파일 클래스패스가 없으면 보안 룰 다수가 동작하지 않음) - .gitignore: .scannerwork/ 제외 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,9 @@ file_upload/
|
||||
# Ignore Gradle build output directory
|
||||
build
|
||||
|
||||
# SonarScanner 작업 디렉터리
|
||||
.scannerwork/
|
||||
|
||||
|
||||
|
||||
build_knbank.sh
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
// SonarQube 정적 보안 분석 전용 파이프라인.
|
||||
//
|
||||
// 전제 (Jenkins 쪽 설정 이름이 다르면 environment 블록의 SONAR_* 만 고치면 된다)
|
||||
// 1) Manage Jenkins > System > SonarQube servers 에 서버 등록 (이름: SONAR_ENV_NAME)
|
||||
// 2) Manage Jenkins > Tools > SonarQube Scanner 에 스캐너 등록 (이름: SONAR_SCANNER_TOOL)
|
||||
// 3) Quality Gate 대기를 쓰려면 SonarQube 서버에
|
||||
// Administration > Configuration > Webhooks 에 <JENKINS_URL>/sonarqube-webhook/ 등록
|
||||
// 4) Job 의 Git 설정에서 shallow clone 을 끄고 tags/history 를 받아와야 blame 기반 "새 코드" 판정이 정확하다.
|
||||
//
|
||||
// 빌드는 JDK 8, 스캐너 실행은 JDK 17 로 분리한다.
|
||||
// (SonarScanner CLI 5.x / SonarQube 10.x 이상은 JRE 17 필수. JDK 8 로 실행하면 UnsupportedClassVersionError)
|
||||
pipeline {
|
||||
agent { label 'djb-vm' }
|
||||
|
||||
triggers {
|
||||
pollSCM('H/30 * * * *')
|
||||
}
|
||||
|
||||
options {
|
||||
timestamps()
|
||||
disableConcurrentBuilds()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20'))
|
||||
}
|
||||
|
||||
parameters {
|
||||
booleanParam(
|
||||
name: 'RUN_TESTS',
|
||||
defaultValue: false,
|
||||
description: '단위 테스트를 함께 실행해 JUnit 결과를 Sonar 로 전송한다(분석 시간 증가).'
|
||||
)
|
||||
}
|
||||
|
||||
environment {
|
||||
// --- Jenkins 설정 이름 (환경에 맞게 수정) ---
|
||||
SONAR_ENV_NAME = 'SonarQube'
|
||||
SONAR_SCANNER_TOOL = 'SonarScanner'
|
||||
|
||||
// --- 툴체인 ---
|
||||
JAVA_HOME = '/apps/opts/jdk8' // gradle 컴파일용 (프로젝트는 Java 8)
|
||||
JAVA_HOME_SCANNER = '/apps/opts/jdk17' // sonar-scanner 실행용
|
||||
GRADLE_HOME = '/apps/opts/gradle-8.7'
|
||||
GRADLE_USER_HOME = '/apps/opts/gradle-home'
|
||||
PATH = "/apps/opts/jdk8/bin:/apps/opts/gradle-8.7/bin:/apps/opts/bin:${env.PATH}"
|
||||
GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
|
||||
// eapim-portal 은 elink-portal-common / elink-online-core-jpa 를 subproject 로 참조한다.
|
||||
// 이 둘이 없으면 컴파일 자체가 불가능하고, 컴파일 산출물이 없으면 Sonar Java 분석이 무력화된다.
|
||||
stage('Checkout dependencies') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
cd "$WORKSPACE/.."
|
||||
|
||||
if [ ! -d elink-portal-common/.git ]; then
|
||||
rm -rf elink-portal-common
|
||||
git clone --depth=1 --branch master \
|
||||
ssh://git@172.30.1.50:2222/djb-eapim/elink-portal-common.git \
|
||||
elink-portal-common
|
||||
else
|
||||
git -C elink-portal-common fetch --depth=1 origin master
|
||||
git -C elink-portal-common reset --hard origin/master
|
||||
git -C elink-portal-common clean -fdx
|
||||
fi
|
||||
|
||||
mkdir -p eapim-online
|
||||
if [ ! -d eapim-online/elink-online-core-jpa/.git ]; then
|
||||
rm -rf eapim-online/elink-online-core-jpa
|
||||
git clone --depth=1 --branch master \
|
||||
ssh://git@172.30.1.50:2222/djb-eapim/elink-online-core-jpa.git \
|
||||
eapim-online/elink-online-core-jpa
|
||||
else
|
||||
git -C eapim-online/elink-online-core-jpa fetch --depth=1 origin master
|
||||
git -C eapim-online/elink-online-core-jpa reset --hard origin/master
|
||||
git -C eapim-online/elink-online-core-jpa clean -fdx
|
||||
fi
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Verify toolchain') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
echo "--- build JDK ---"
|
||||
java -version
|
||||
gradle --version
|
||||
|
||||
echo "--- scanner JDK ---"
|
||||
if [ ! -x "$JAVA_HOME_SCANNER/bin/java" ]; then
|
||||
echo "JDK 17 이 없다: $JAVA_HOME_SCANNER"
|
||||
echo "SonarScanner 는 JRE 17 이상을 요구한다. 노드에 설치하거나 JAVA_HOME_SCANNER 를 수정할 것."
|
||||
exit 1
|
||||
fi
|
||||
"$JAVA_HOME_SCANNER/bin/java" -version
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
// clean 을 반드시 붙인다. build/generated 잔여 산출물이 남으면
|
||||
// MapStruct Impl / QueryDSL Q클래스가 중복 생성되어 컴파일이 깨진다.
|
||||
stage('Compile') {
|
||||
steps {
|
||||
sh 'gradle clean classes testClasses --no-daemon'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Test') {
|
||||
when { expression { return params.RUN_TESTS } }
|
||||
steps {
|
||||
sh 'gradle test --no-daemon'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit allowEmptyResults: true, testResults: 'build/test-results/test/*.xml'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sonar.java.libraries 용 클래스패스 덤프.
|
||||
// 없으면 타입 해석이 안 돼 보안 룰 다수가 침묵하므로 실패 시 UNSTABLE 로만 넘기고 분석은 계속한다.
|
||||
stage('Export analysis classpath') {
|
||||
steps {
|
||||
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
|
||||
sh 'gradle -I ci/sonar-classpath.gradle exportSonarClasspath --no-daemon'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('SonarQube analysis') {
|
||||
steps {
|
||||
script {
|
||||
def scannerHome = tool name: env.SONAR_SCANNER_TOOL,
|
||||
type: 'hudson.plugins.sonar.SonarRunnerInstallation'
|
||||
|
||||
withSonarQubeEnv(env.SONAR_ENV_NAME) {
|
||||
withEnv(["SONAR_SCANNER_HOME=${scannerHome}"]) {
|
||||
sh '''
|
||||
set -eu
|
||||
|
||||
# 스캐너는 JDK 17 로 실행한다 (빌드용 JDK 8 과 분리)
|
||||
export JAVA_HOME="$JAVA_HOME_SCANNER"
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
|
||||
# 토큰은 커맨드라인(-Dsonar.token)에 노출시키지 않고 환경변수로만 넘긴다.
|
||||
# 플러그인 버전에 따라 SONAR_AUTH_TOKEN 만 주입되는 경우가 있다.
|
||||
if [ -z "${SONAR_TOKEN:-}" ] && [ -n "${SONAR_AUTH_TOKEN:-}" ]; then
|
||||
export SONAR_TOKEN="$SONAR_AUTH_TOKEN"
|
||||
fi
|
||||
|
||||
LIBS=""
|
||||
if [ -f build/sonar/java-libraries.txt ]; then
|
||||
LIBS=$(cat build/sonar/java-libraries.txt)
|
||||
else
|
||||
echo "WARN: build/sonar/java-libraries.txt 없음 - 타입 해석 정확도 저하"
|
||||
fi
|
||||
|
||||
TEST_LIBS=""
|
||||
if [ -f build/sonar/java-test-libraries.txt ]; then
|
||||
TEST_LIBS=$(cat build/sonar/java-test-libraries.txt)
|
||||
fi
|
||||
|
||||
JUNIT_ARG=""
|
||||
if [ -d build/test-results/test ]; then
|
||||
JUNIT_ARG="-Dsonar.junit.reportPaths=build/test-results/test"
|
||||
fi
|
||||
|
||||
"$SONAR_SCANNER_HOME/bin/sonar-scanner" \
|
||||
-Dsonar.projectVersion="${BUILD_NUMBER}" \
|
||||
-Dsonar.java.libraries="$LIBS" \
|
||||
-Dsonar.java.test.libraries="$TEST_LIBS" \
|
||||
$JUNIT_ARG
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SonarQube 웹훅으로 게이트 결과를 받아 판정한다.
|
||||
// 정책: 게이트 실패는 UNSTABLE 로만 표시하고 빌드는 실패시키지 않는다.
|
||||
stage('Quality Gate') {
|
||||
steps {
|
||||
script {
|
||||
def qg = null
|
||||
try {
|
||||
timeout(time: 15, unit: 'MINUTES') {
|
||||
qg = waitForQualityGate abortPipeline: false
|
||||
}
|
||||
} catch (err) {
|
||||
unstable("Quality Gate 결과 대기 실패/타임아웃: ${err}")
|
||||
return
|
||||
}
|
||||
|
||||
if (qg == null) {
|
||||
unstable('Quality Gate 결과를 받지 못했다. SonarQube 웹훅 설정을 확인할 것.')
|
||||
} else if (qg.status != 'OK') {
|
||||
unstable("Quality Gate ${qg.status}")
|
||||
} else {
|
||||
echo 'Quality Gate OK'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
always {
|
||||
archiveArtifacts allowEmptyArchive: true, artifacts: '.scannerwork/report-task.txt'
|
||||
script {
|
||||
def report = '.scannerwork/report-task.txt'
|
||||
if (fileExists(report)) {
|
||||
def url = readFile(report).readLines().find { it.startsWith('dashboardUrl=') }
|
||||
if (url) {
|
||||
echo "SonarQube: ${url.substring('dashboardUrl='.length())}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// SonarScanner 의 sonar.java.libraries / sonar.java.test.libraries 로 넘길
|
||||
// 컴파일 클래스패스를 파일로 덤프한다.
|
||||
//
|
||||
// build.gradle 을 건드리지 않기 위해 init script(-I) 로만 주입한다.
|
||||
// gradle -I ci/sonar-classpath.gradle exportSonarClasspath --no-daemon
|
||||
//
|
||||
// 출력(콤마 구분, 절대경로):
|
||||
// build/sonar/java-libraries.txt main 컴파일 클래스패스
|
||||
// build/sonar/java-test-libraries.txt main + test 컴파일 클래스패스
|
||||
//
|
||||
// 이 파일이 없으면 Sonar Java 분석기가 타입을 해석하지 못해
|
||||
// 보안 룰(SQL Injection, XSS 등) 상당수가 침묵한다. 선택 사항이 아니다.
|
||||
|
||||
rootProject { project ->
|
||||
project.plugins.withId('java') {
|
||||
project.tasks.register('exportSonarClasspath') {
|
||||
group = 'verification'
|
||||
description = 'SonarScanner 용 컴파일 클래스패스를 build/sonar 에 덤프한다'
|
||||
|
||||
doLast {
|
||||
def outDir = new File(project.layout.buildDirectory.get().asFile, 'sonar')
|
||||
outDir.mkdirs()
|
||||
|
||||
def toLine = { files ->
|
||||
files.findAll { it.exists() }
|
||||
.collect { it.absolutePath }
|
||||
.unique()
|
||||
.join(',')
|
||||
}
|
||||
|
||||
def mainCp = project.sourceSets.main.compileClasspath.files
|
||||
def testCp = project.sourceSets.test.compileClasspath.files
|
||||
|
||||
new File(outDir, 'java-libraries.txt').text = toLine(mainCp)
|
||||
new File(outDir, 'java-test-libraries.txt').text = toLine(mainCp + testCp)
|
||||
|
||||
logger.lifecycle("sonar classpath dumped: ${outDir}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
# SonarQube 분석 설정 (SonarScanner CLI 용)
|
||||
#
|
||||
# Jenkinsfile.sonar 가 이 파일을 그대로 읽고, 아래 항목만 실행 시점에 -D 로 덮어쓴다.
|
||||
# sonar.projectVersion : 빌드 번호
|
||||
# sonar.java.libraries : ci/sonar-classpath.gradle 이 덤프한 컴파일 클래스패스
|
||||
# sonar.java.test.libraries : 위 + 테스트 클래스패스
|
||||
# sonar.junit.reportPaths : 테스트를 실행한 경우에만
|
||||
#
|
||||
# 로컬에서 수동 분석 시(스캐너는 JDK 17 필요):
|
||||
# gradle clean classes testClasses
|
||||
# gradle -I ci/sonar-classpath.gradle exportSonarClasspath
|
||||
# sonar-scanner -Dsonar.host.url=... -Dsonar.token=... \
|
||||
# -Dsonar.java.libraries="$(cat build/sonar/java-libraries.txt)"
|
||||
|
||||
sonar.projectKey=djb-eapim-portal
|
||||
sonar.projectName=DJB eAPIM Portal
|
||||
sonar.sourceEncoding=UTF-8
|
||||
|
||||
# --- 분석 대상 -------------------------------------------------------------
|
||||
# src/main/resources 를 포함시켜 Thymeleaf 템플릿(XSS), yml/properties(하드코딩 시크릿)까지 검사한다.
|
||||
sonar.sources=src/main/java,src/main/resources
|
||||
sonar.tests=src/test/java
|
||||
|
||||
# --- Java ------------------------------------------------------------------
|
||||
# 소스는 Java 8. 스캐너 실행 JRE 는 별개로 17 이상이어야 한다(Jenkinsfile.sonar 참고).
|
||||
sonar.java.source=8
|
||||
sonar.java.binaries=build/classes/java/main
|
||||
sonar.java.test.binaries=build/classes/java/test
|
||||
|
||||
# --- 제외 대상 -------------------------------------------------------------
|
||||
# 서드파티 번들/컴파일 산출물/바이너리. 분석해도 우리가 고칠 수 없는 코드는 뺀다.
|
||||
sonar.exclusions=\
|
||||
**/build/**,\
|
||||
**/node_modules/**,\
|
||||
**/lib/**,\
|
||||
**/libs/**,\
|
||||
**/local_jars/**,\
|
||||
src/main/resources/static/plugins/**,\
|
||||
src/main/resources/static/js/egovframework/**,\
|
||||
src/main/resources/static/js/**/*.min.js,\
|
||||
src/main/resources/static/js/lodash.js,\
|
||||
src/main/resources/static/js/popper.js,\
|
||||
src/main/resources/static/js/daterangepicker.js,\
|
||||
src/main/resources/static/js/json-lint.js,\
|
||||
src/main/resources/static/html/**,\
|
||||
src/main/resources/static/css/**,\
|
||||
src/main/resources/static/sass/**,\
|
||||
src/main/resources/static/font/**,\
|
||||
src/main/resources/static/webfonts/**,\
|
||||
src/main/resources/static/img/**
|
||||
|
||||
# 자동 생성 코드(MapStruct Impl, QueryDSL Q클래스)는 build/ 아래라 이미 제외된다.
|
||||
|
||||
# --- SCM -------------------------------------------------------------------
|
||||
# blame 기반 "새 코드" 판정을 위해 Jenkins Job 에서 shallow clone 을 쓰지 않는다.
|
||||
sonar.scm.provider=git
|
||||
|
||||
# --- 브랜치 ----------------------------------------------------------------
|
||||
# sonar.branch.name 은 SonarQube Developer Edition 이상에서만 동작한다.
|
||||
# Community Edition 에서 지정하면 분석이 실패하므로 기본적으로 설정하지 않는다.
|
||||
# sonar.branch.name=feats/ci-test
|
||||
Reference in New Issue
Block a user