Welcome back to EP 3 of my CI/CD Security Series!
In this episode, we’ll focus entirely on integrating SonarQube into your GitLab CI/CD pipeline. SonarQube is a powerful tool for automatically scanning your code for bugs, vulnerabilities, and code smells—right inside your pipeline.
If you missed EP 1 and 2, make sure to check it out for a full overview of the complete CI/CD security flow. Now, let’s get our hands dirty and bring SonarQube into the pipeline!
Getting Started with SonarQube
After you log in to SonarQube, you’ll land on this dashboard:
Start by clicking Create Project → Local Project.
Now, name your project as you like.
It’s a good idea to check your global settings here, too:
Integrating SonarQube with GitLab CI/CD
For this project, we’ll be using GitLab to build our CI/CD pipeline.
Click With GitLab CI to get started.
You’ll see a setup page like this:
You can follow these steps directly, but I’ll break them down for you for clarity.
Step 1: Add Environment Variables in GitLab
First, let’s add the necessary environment variables.
Head to your project in GitLab. On the sidebar, go to Settings (hover if you don’t see the label):
Then click CI/CD → Variables.
Now, click Add variable.
A sidebar will pop up:
- For Key, use the value from SonarQube (e.g.,
SONAR_TOKEN
). - For Value, go back to SonarQube and click Generate Token:
Choose your expiration date—although I usually set it to “No Expiration” for testing, I recommend setting an expiration for security reasons.
Copy your token and paste it into GitLab:
If you are working on branches other than main (for example, develop, feature, etc.), you also need to configure those branches as Protected branches.
Go to Settings → Repository:
Add a Protected Branch:
Type in your branch name:
Click Protect
Now, you’ll be able to use protected variables on these branches!
Step 2: Add SonarQube Config to Your Pipeline
Next, grab your project key from SonarQube and add it as a GitLab variable:
It should look like this:
Pick your programming language. I’m using Go for this demo:
If you don’t have a sonar-project.properties
file, create one:
sonar.projectKey=booranasak-bank-golang-sast
sonar.qualitygate.wait=true
Step 3: Update Your .gitlab-ci.yml
Now, update your .gitlab-ci.yml
with the following content.
(Make sure your rules include the branches you want SonarQube to run on!)
image:
name: sonarsource/sonar-scanner-cli:11
entrypoint: [""]
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: "0"
stages:
- sonarqube-check
- sonarqube-vulnerability-report
sonarqube-check:
stage: sonarqube-check
script:
- sonar-scanner -Dsonar.host.url="${SONAR_HOST_URL}"
allow_failure: true
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == 'master'
- if: $CI_COMMIT_BRANCH == 'main'
- if: $CI_COMMIT_BRANCH == 'develop'
sonarqube-vulnerability-report:
stage: sonarqube-vulnerability-report
script:
- 'curl -u "${SONAR_TOKEN}:" "${SONAR_HOST_URL}/api/issues/gitlab_sast_export?projectKey=booranasak-bank-golang-sast&branch=${CI_COMMIT_BRANCH}&pullRequest=${CI_MERGE_REQUEST_IID}" -o gl-sast-sonar-report.json'
allow_failure: true
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == 'master'
- if: $CI_COMMIT_BRANCH == 'main'
- if: $CI_COMMIT_BRANCH == 'develop'
artifacts:
expire_in: 1 day
reports:
sast: gl-sast-sonar-report.json
Note:
By default, this pipeline only runs SonarQube on these branches:
master
main
-
develop
Or on merge requests.
If you want to run SonarQube on another branch (like feature/sonarqube
for testing), simply update the rules:
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == 'master'
- if: $CI_COMMIT_BRANCH == 'main'
- if: $CI_COMMIT_BRANCH == 'develop'
- if: $CI_COMMIT_BRANCH == 'feature/sonarqube'
That’s it! Just adjust your rules for any branches you want to include.
And now, your .gitlab-ci.yml should look something like this (I merge SonarQube into one stage but the job still separate for clarity and easier management):
sonarqube-check:
stage: sast
image:
name: sonarsource/sonar-scanner-cli:11
entrypoint: [""]
dependencies:
- unit_test_and_coverage
script:
- sonar-scanner -Dsonar.host.url="${SONAR_HOST_URL}" -Dsonar.go.coverage.reportPaths=coverage.out -Dsonar.exclusions=**/*_test.go
allow_failure: true
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == 'master'
- if: $CI_COMMIT_BRANCH == 'main'
- if: $CI_COMMIT_BRANCH == 'develop'
sonarqube-vulnerability-report:
stage: sast
image:
name: sonarsource/sonar-scanner-cli:11
entrypoint: [""]
script:
- 'curl -u "${SONAR_TOKEN}:" "${SONAR_HOST_URL}/api/issues/gitlab_sast_export?projectKey=booranasak-golang-sast&branch=${CI_COMMIT_BRANCH}&pullRequest=${CI_MERGE_REQUEST_IID}" -o gl-sast-sonar-report.json'
allow_failure: true
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == 'master'
- if: $CI_COMMIT_BRANCH == 'main'
- if: $CI_COMMIT_BRANCH == 'develop'
artifacts:
expire_in: 1 day
reports:
sast: gl-sast-sonar-report.json
And when you merge everything together, It will look like this
image: docker:latest
services:
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2375/
DOCKER_TLS_CERTDIR: ""
GO_VERSION: "1.24.3"
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: "0"
stages:
- lint
- test
- sast
.go-job-template: &go-job-template
image: debian:bullseye
before_script:
- apt update && apt install -y curl git tar gzip
- curl -LO https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz
- rm -rf /usr/local/go && tar -C /usr/local -xzf go${GO_VERSION}.linux-amd64.tar.gz
- export PATH="/usr/local/go/bin:$PATH"
- go version
lint_golint:
stage: lint
<<: *go-job-template
script:
- export PATH="/usr/local/go/bin:$PATH"
- go install golang.org/x/lint/golint@latest
- export PATH="$PATH:$(go env GOPATH)/bin"
- echo "Linting files:"
- find . -name '*.go'
- golint ./... | tee lint-report.txt
- echo "--- Lint report preview ---"
- cat lint-report.txt || echo "lint-report.txt is empty"
allow_failure: true
artifacts:
name: "golint-report"
paths:
- lint-report.txt
expire_in: 1 week
unit_test_and_coverage:
stage: test
<<: *go-job-template
script:
- export PATH="/usr/local/go/bin:$PATH"
- go mod tidy
- go test -v -cover ./...
- go test -v -coverprofile=coverage.out ./...
artifacts:
paths:
- coverage.out
expire_in: 1 hour
sonarqube-check:
stage: sast
image:
name: sonarsource/sonar-scanner-cli:11
entrypoint: [""]
dependencies:
- unit_test_and_coverage
script:
- sonar-scanner -Dsonar.host.url="${SONAR_HOST_URL}" -Dsonar.go.coverage.reportPaths=coverage.out -Dsonar.exclusions=**/*_test.go
allow_failure: true
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == 'master'
- if: $CI_COMMIT_BRANCH == 'main'
- if: $CI_COMMIT_BRANCH == 'develop'
sonarqube-vulnerability-report:
stage: sast
image:
name: sonarsource/sonar-scanner-cli:11
entrypoint: [""]
script:
- 'curl -u "${SONAR_TOKEN}:" "${SONAR_HOST_URL}/api/issues/gitlab_sast_export?projectKey=booranasak-golang-sast&branch=${CI_COMMIT_BRANCH}&pullRequest=${CI_MERGE_REQUEST_IID}" -o gl-sast-sonar-report.json'
allow_failure: true
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == 'master'
- if: $CI_COMMIT_BRANCH == 'main'
- if: $CI_COMMIT_BRANCH == 'develop'
artifacts:
expire_in: 1 day
reports:
sast: gl-sast-sonar-report.json
Pipeline Example
Sonarqube Example
Failed Example
- First issue are code coverage below 80 percent
- Second issue are Duplicated string literals
You can also view more detail
even explain why it was an issue
Even more!!!, Explain how to fix it
Simple, right? With these steps, you can integrate SonarQube into your GitLab CI/CD pipeline and customize it to fit your workflow.
Top comments (0)