DEV Community

Cover image for Catch vulnerabilities before they ship: local SonarQube setup (Part 2)
Vardan Matevosian
Vardan Matevosian

Posted on • Originally published at matevosian.tech

Catch vulnerabilities before they ship: local SonarQube setup (Part 2)

 

Note: Click on the image to see a larger (original) version.

 

Introduction

Static Application Security Testing (SAST) is a crucial practice within the Software Security Development Life Cycle (SSDLC) that enables developers to identify security vulnerabilities early in the code development phase. While understanding the concepts of SAST is important, implementing it effectively in real-world projects is what ensures robust and secure software delivery.

In this second part of our SAST series, we focus on integrating the SonarQube Cloud, a popular static analysis tool, into the IntelliJ IDEA and running it using Docker.

We will explore:

  • SonarQube Cloud for SAST
  • Local scanning with SonarQube Cloud via IntelliJ IDEA
  • Local scanning using Docker Compose with SonarQube image

Tools:

  • SonarQube Cloud
  • Spring Boot
  • IntelliJ IDEA
  • Docker-Compose

Prerequisites:

  • Create a Spring Boot project using IntelliJ IDEA or the Spring Boot Initializr at https://start.spring.io/.

  • Click Next and generate the project without dependencies.

New Spring Boot project in IntelliJ EDIA

 
 

SonarQube Cloud for SAST

SonarQube Cloud offers a hassle-free way to start static code analysis without managing your own infrastructure. It provides cloud-hosted scanning capabilities that analyze code quality, security vulnerabilities, bugs, and code smells.

 
 

Setting Up SonarQube Cloud

  1. Create an account: Visit SonarQube’s official website and sign up for a free or paid cloud account.
  2. Create a new project: Once logged in, create a new project by providing your repository details.
  3. Generate an authentication token: For integrating scanning tools, generate a security token from your account settings. We must store it in a secret place, like in the GitHub Actions environment or repository secret.
  4. Configure project settings: We need to set Quality Gates, SLAs, and rules according to our security policy. For a free account, we cannot create the Quality Gates; we must use the default one.

SonarQube Cloud then becomes the central place to view detailed security reports and code quality metrics.

 
 

Overview of SonarQube’s cloud capabilities for static code analysis

 
 

SonarCloud is a fully managed SaaS platform for continuous static code analysis, designed to detect code quality issues, security vulnerabilities, and maintainability risks across modern software projects. It provides code coverage plugin integration that is used by 30+ different programming languages, including Java, Python, C#, JavaScript, TypeScript, and Go.

Capabilities:

  • Cloud-hosted and fully managed: SonarCloud removes the need for installation, hosting, or maintenance. This allows teams to focus solely on development and CI/CD pipelines. Use the cloud version if your company does not work under compliance regulations.

    SonarSource manages:

    • uptime
    • scaling
    • rule updates
    • language analyzers
    • security patches
  • Deep Static Code Analysis: SonarCloud performs comprehensive static analysis across many dimensions:

    • Bug Detection. Identifies code paths that can lead to crashes, incorrect logic, or unintended behavior.
    • Security vulnerability detection. Finds CWE-based, OWASP-aligned issues such as:
      • SQL Injection
      • Path traversal
      • Input validation issues
      • Hardcoded secrets
      • Command injection
      • Code Smells. Flags maintainability issues, duplicated code, bad design, and anti-patterns.
    • Cognitive Complexity. Measures how difficult code is to understand and maintain.
    • Multi-language support. Supports 30+ languages, including Java, Python, JavaScript, TypeScript, Go, C#, Kotlin, PHP, Terraform, YAML, and more.
  • Seamless CI/CD integration: SonarCloud integrates natively with:

    • CI providers:
      • GitHub Actions
      • Azure Pipelines
      • Bitbucket Pipelines
      • GitLab CI
    • Build tools: SonarQube provides integrations with build tools. For example, in our case, the Gradle Sonar plugin is used to run Sonar Scanner in the CI/CD pipeline.
  • Pull request and branch analysis: SonarCloud performs inline analysis during PR reviews, preventing issues from reaching the main branch. The free plan does not support this.

  • Quality Gates: A Quality Gate defines the rules that code must satisfy before being merged. If a PR fails the Quality Gate, the CI pipeline can block the merge. Adding custom Quality Gates is not supported by the free plan.

    Default checks include:

    • No new critical or blocker issues.
    • Code coverage must meet a minimum threshold.
    • No new bugs or security vulnerabilities.
    • No new code duplications.
  • Test coverage and code metrics: SonarCloud provides overall coverage and new code coverage, for example, for a specific PR or branch.

  • Organization and project dashboards: SonarCloud provides rich dashboards for:

    • Centralized issue tracking.
    • Historical trend charts for issues, code coverage, and duplications.
    • Hotspot security review.
    • Code coverage progress.
    • History of activities for each branch or PR.
  • Notifications and integrations: Supports notifications via:

    • GitHub checks.
    • GitLab merge requests.
    • Slack.
    • Email.
    • Webhooks.
  • Open APIs and extensions: SonarQube provides APIs to build custom dashboards and export metrics.

 
 

Local scanning with SonarQube Cloud via IntelliJ IDEA

Running local scans during development helps catch issues early before code is committed.

 

Installing and configuring the SonarQube plugin in IntelliJ IDEA

  1. Open IntelliJ IDEA.
  2. Navigate to File > Settings > Plugins and search for the "SonarQube" plugin.
  3. Install and restart the IDE. It must appear in the “Installed” section.

SonarQube plugin isntallation in IntelliJ

 
 

  1. Generate the authentication Sonar token. Go to the SonarCloud website, then My Account > Security > Generate Token, or click on this link https://sonarcloud.io/account/security/, and you will be redirected to the token generation page. Enter the name of the token, typically where you want to use it, and click the “Generate Token” button.

SonarQube cloud security tab

 
 

  1. Configure the plugin by adding your SonarQube Cloud server URL and authentication token. Click on the SonarQube plugin icon, then select the gear icon, as shown in the screenshot.

IntelliJ SonarQube plugin icon

 
 

Enter the Project key of your SonarQube project and click “Configure the connection”.

IntelliJ SonarQube plugin configuration 1

 
 

You will see the Connection section, click the plus to add a new connection.

IntelliJ SonarQube plugin configuration 2

 
 

Enter the name of the connection and click “Next”.

IntelliJ SonarQube plugin configuration 3

 
 

Paste the generated token, or you can even create the token from this IDE window. Click “Next”, and the IDE will attempt to connect to Sonar Cloud. You will see that authentication is successful if the connection is established.

IntelliJ SonarQube plugin configuration 4

 
 

Running local scans

 
 

After configuration, you can run SonarQube scans directly from the IDE. The plugin highlights issues inline and provides quick access to detailed analysis reports. Click “Analyze All Project Files”.

IntelliJ SonarQube plugin analyze project 1

 
 

You can see the results. In this case, it found the token in the gradle-local.properties file, which is ignored by the .gitignore file, and it is only for local purposes. However, you will still see it unless you exclude it from scanning. This plugin also allows you to do it.

IntelliJ SonarQube plugin analyze project 2

 
 

Below you can see the result of this specific finding. Additionally, the plugin provides information on why this issue occurs and how to resolve it.

IntelliJ SonarQube plugin analyze project 3

 
 

Benefits of local scanning:

  • Immediate feedback on security and quality issues.
  • Reduces the likelihood of pushing vulnerable code.
  • Saves time by fixing problems early in the development workflow.

 

Local scanning using Docker Compose with SonarQube image

If you prefer running SonarQube locally, Docker provides an easy setup that eliminates the need for complex installation.

 

Setting up SonarQube locally via Docker Compose

Create a “docker-compose.yml” file with the following content:

 
 

    version: "3.9"


    services:
     sonarqube:
       image: sonarqube:latest
       ports:
         - "9000:9000"
       environment:
         SONAR_ES_BOOTSTRAP_CHECKS_DISABLE: "true"
       volumes:
         - sonarqube_data:/opt/sonarqube/data
         - sonarqube_extensions:/opt/sonarqube/extensions


    volumes:
     sonarqube_data:
     sonarqube_extensions:

Enter fullscreen mode Exit fullscreen mode

 
 

Here is the gradle-local.properties file.

Place it in the root of the project and add it to the .gitignore.

 
 

    systemProp.sonar.qualitygate.wait=true


    sonar.projectKey=sonarqube_actions_demo_key
    sonar.organization=local-organization
    sonar.projectName=sonarqube_actions_demo
    sonar.token=sqp_b7dc6e025b58eb7455b11c6c468cda2b79eb6450b
    sonar.host.url=http://localhost:9000
    sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml
Enter fullscreen mode Exit fullscreen mode

 
 

Gradle configuration. The SonarQube plugin is required.

 
 

    plugins {
       id 'java'
       id "org.sonarqube" version "7.0.0.6105"
       id 'jacoco'
       id 'org.springframework.boot' version '3.0.0'
       id 'io.spring.dependency-management' version '1.1.7'
    }
Enter fullscreen mode Exit fullscreen mode

 
 

SonarQube properties

 
 

    def localProps = new Properties()
    def localFile = file("gradle-local.properties")
    if (localFile.exists()) {
    localProps.load(localFile.newDataInputStream())
    }


    sonar {
    properties {
       property "sonar.projectKey", System.getenv("SONAR_PROJECT_KEY") ?: localProps["sonar.projectKey"]
       property "sonar.organization", System.getenv("SONAR_ORGANIZATION_KEY") ?: localProps["sonar.organization"]
       property "sonar.projectName", System.getenv("SONAR_PROJECT_NAME") ?: localProps["sonar.projectName"]
       property "sonar.token", System.getenv("SONAR_TOKEN") ?: localProps["sonar.token"]
       property "sonar.host.url", System.getenv("SONAR_HOST_URL") ?: localProps["sonar.host.url"]
    }
    }

Enter fullscreen mode Exit fullscreen mode

 
 

Run the following command to start the container: docker-compose up -d, or you can run it from your IntelliJ IDEA

IntelliJ Docker-compose icon run

 
 

You can see the container is up and running

IntelliJ Docker container running

 
 

Access SonarQube dashboard at http://localhost:9000. Login is admin, but you will need to change the password from admin to a new one.

Click Create project from the top right corner.

Local SonarQube create project menu

 
 

Enter the same name and project key as in your gradle-local.properties file. Then select to use the previous version.

Local SonarQube create project step 1

 
 

Choose Locally as the Analysis Method

Local SonarQube create project step 2

 
 

Type the name of the token and click Generate, then Continue, and set it in the gradle-local.properties file to the “sonar.token” property like

properties sonar.token=sqp_b7dc6e025b58eb7455b11c6c468cda2b79eb6450b

.

Local SonarQube create project token generation step 3

 
 

If you want to generate manually, you can do it as shown in the screenshot below.

Local SonarQube manual token generation

 
 

Run the Sonar scan from the command line or from IntelliJ IDEA

Use this command ./gradlew sonar --info or run from your IntelliJ IDEA

Gradle sonar local scanning command result

 
 

You can view the result by going to the Projects tab. The metrics and coverage are for the main branch only and show overall coverage and metrics. For more details about the new code analysis, click on the project.

Local SonarQube result on UI

 
 

Comparing Local Docker Setup with Cloud Scanning

  • Cloud: No maintenance, scalable, accessible anywhere.
  • Local Docker: Full control, ideal if you have no internet connection for an extended period but need to review scan results, useful for sensitive projects.

 
 

Enhancing Code Coverage with Jacoco

Add these changes to the “gradle.build” file:

Jacoco plugin

    id 'jacoco'
Enter fullscreen mode Exit fullscreen mode

 
 

Jacoco properties for SonarQube.

The config, mapper, model, and exception packages should be excluded from code coverage processing in SonarQube.

 
 

   property "sonar.coverage.jacocoxml.import", "true"
   property "sonar.java.coveragePlugin", "jacoco"
   property "sonar.coverage.jacoco.xmlReportPaths",
           System.getenv("SONAR_COVERAGE_JACOCO_XML_REPORT_PATH")
                   ?: localProps["sonar.coverage.jacoco.xmlReportPaths"]
   property "sonar.coverage.exclusions", "**/config/**,**/mapper/**,**/model/**,**/exception/**"
Enter fullscreen mode Exit fullscreen mode

 
 

Jacoco configuration

 
 

    jacoco {
       toolVersion = "0.8.12"
    }


    jacocoTestReport {
       dependsOn test
       reports {
           xml.required = true
           html.required = true
           csv.required = false
       }


       afterEvaluate {
           classDirectories.setFrom(files(classDirectories.files.collect {
               fileTree(dir: it, exclude: ['**/config/**', '**/model/**', '**/exception/**', '**/mapper/**'])
           }))
       }
    }


    jacocoTestCoverageVerification {
       dependsOn jacocoTestReport
       violationRules {
           rule {
               enabled = true
               element = 'BUNDLE'


               limit {
                   counter = 'LINE'
                   value = 'COVEREDRATIO'
                   minimum = 0.90
               }


               limit {
                   counter = 'BRANCH'
                   value = 'COVEREDRATIO'
                   minimum = 0.65
               }


               limit {
                   counter = 'METHOD'
                   value = 'COVEREDRATIO'
                   minimum = 0.90
               }


               limit {
                   counter = 'CLASS'
                   value = 'COVEREDRATIO'
                   minimum = 0.90
               }
           }
       }


       afterEvaluate {
           classDirectories.setFrom(files(classDirectories.files.collect {
               fileTree(dir: it, exclude: ['**/config/**', '**/model/**', '**/exception/**', '**/mapper/**'])
           }))
       }
    }

Enter fullscreen mode Exit fullscreen mode

 
 

Update the task test by adding this line at the end

finalizedBy jacocoTestReport
Enter fullscreen mode Exit fullscreen mode

 
 

The complete “gradle.build” file

    plugins {
       id 'java'
       id 'jacoco'
       id "org.sonarqube" version "7.0.0.6105"
       id 'org.springframework.boot' version '3.0.0'
       id 'io.spring.dependency-management' version '1.1.7'
    }


    group = 'com.practice'
    version = '0.0.1-SNAPSHOT'
    description = 'sonarqube_actions_demo'


    java {
       toolchain {
           languageVersion = JavaLanguageVersion.of(17)
       }
    }


    configurations {
       compileOnly {
           extendsFrom annotationProcessor
       }
    }


    repositories {
       mavenCentral()
    }


    dependencies {
       // Spring boot runtime
       implementation 'org.springframework.boot:spring-boot-starter-web'
       implementation 'org.springframework.boot:spring-boot-starter-jdbc'


       // Persistence H2 for demo (optional)
       runtimeOnly 'com.h2database:h2'


       // Observability
       implementation 'ch.qos.logback:logback-classic:1.5.13'
       implementation 'ch.qos.logback:logback-core:1.5.19'




       // Test
       testImplementation 'org.springframework.boot:spring-boot-starter-test'
       testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
       testImplementation 'com.h2database:h2'


       // Annotation processing
       compileOnly 'org.projectlombok:lombok:1.18.26'
       annotationProcessor 'org.projectlombok:lombok:1.18.26'
       implementation 'org.mapstruct:mapstruct:1.5.5.Final'
       annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
    }




    def localProps = new Properties()
    def localFile = file("gradle-local.properties")
    if (localFile.exists()) {
       localProps.load(localFile.newDataInputStream())
    }


    sonar {
       properties {
           property "sonar.coverage.jacocoxml.import", "true"
           property "sonar.java.coveragePlugin", "jacoco"
           property "sonar.coverage.jacoco.xmlReportPaths",
                   System.getenv("SONAR_COVERAGE_JACOCO_XML_REPORT_PATH")
                           ?: localProps["sonar.coverage.jacoco.xmlReportPaths"]
           property "sonar.projectKey", System.getenv("SONAR_PROJECT_KEY") ?: localProps["sonar.projectKey"]
           property "sonar.organization", System.getenv("SONAR_ORGANIZATION_KEY") ?: localProps["sonar.organization"]
           property "sonar.projectName", System.getenv("SONAR_PROJECT_NAME") ?: localProps["sonar.projectName"]
           property "sonar.token", System.getenv("SONAR_TOKEN") ?: localProps["sonar.token"]
           property "sonar.host.url", System.getenv("SONAR_HOST_URL") ?: localProps["sonar.host.url"]
           property "sonar.coverage.exclusions", "**/config/**,**/mapper/**,**/model/**,**/exception/**"
       }
    }




    jacoco {
       toolVersion = "0.8.12"
    }


    jacocoTestReport {
       dependsOn test
       reports {
           xml.required = true
           html.required = true
           csv.required = false
       }


       afterEvaluate {
           classDirectories.setFrom(files(classDirectories.files.collect {
               fileTree(dir: it, exclude: ['**/config/**', '**/model/**', '**/exception/**', '**/mapper/**'])
           }))
       }
    }


    jacocoTestCoverageVerification {
       dependsOn jacocoTestReport
       violationRules {
           rule {
               enabled = true
               element = 'BUNDLE'


               limit {
                   counter = 'LINE'
                   value = 'COVEREDRATIO'
                   minimum = 0.90
               }


               limit {
                   counter = 'BRANCH'
                   value = 'COVEREDRATIO'
                   minimum = 0.65
               }


               limit {
                   counter = 'METHOD'
                   value = 'COVEREDRATIO'
                   minimum = 0.90
               }


               limit {
                   counter = 'CLASS'
                   value = 'COVEREDRATIO'
                   minimum = 0.90
               }
           }
       }


       afterEvaluate {
           classDirectories.setFrom(files(classDirectories.files.collect {
               fileTree(dir: it, exclude: ['**/config/**', '**/model/**', '**/exception/**', '**/mapper/**'])
           }))
       }
    }


    tasks.named('test') {
       useJUnitPlatform()
       finalizedBy jacocoTestReport
    }

Enter fullscreen mode Exit fullscreen mode

 
 

Test package structure

Project test package structure

 
 

We need to add the “data.sql” file. This script inserts testing data into the database. You can access them while running tests.

 
 

INSERT INTO users (id, username, email)
VALUES (10, 'alice', 'alice@example.com'),
      (11, 'bob', 'bob@example.com');
Enter fullscreen mode Exit fullscreen mode

 
 

UseControllerTest

 
 

    @WebMvcTest(UserController.class)
    class UserControllerTest {


       @Autowired
       private MockMvc mockMvc;


       @MockBean
       private UserServiceImpl userService;


       @Test
       void getUser_shouldReturnUserDto_whenUserExists() throws Exception {
           // Arrange
           Integer userId = 1;
           UserDto userDto = new UserDto(userId, "jane_doe", "jane@example.com");
           when(userService.getUserById(userId)).thenReturn(userDto);


           // Act and Assert
           mockMvc.perform(get("/users/{id}", userId)
                           .contentType(MediaType.APPLICATION_JSON))
                   .andExpect(status().isOk())
                   .andExpect(jsonPath("$.id").value(userDto.getId()))
                   .andExpect(jsonPath("$.username").value(userDto.getUsername()))
                   .andExpect(jsonPath("$.email").value(userDto.getEmail()));


           verify(userService).getUserById(userId);
       }


       @Test
       void getUser_shouldReturn500_whenUserServiceThrowsException() throws Exception {
           // Arrange
           Integer userId = 999;
           when(userService.getUserById(userId))
                   .thenThrow(new NoSuchElementException("User not found with id: " + userId));


           // Act and Assert
           mockMvc.perform(get("/users/{id}", userId)
                           .contentType(MediaType.APPLICATION_JSON))
                   .andExpect(status().is4xxClientError());


           verify(userService).getUserById(userId);
       }
    }

Enter fullscreen mode Exit fullscreen mode

 
 

UserDaoImplTest

 
 

    @JdbcTest
    @TestPropertySource(properties = {
           "spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1",
           "spring.datasource.driver-class-name=org.h2.Driver"
    })
    class UserDaoImplTest {


       @Autowired
       private DataSource dataSource;


       private UserDaoImpl userDao;


       @BeforeEach
       void setUp() {
           this.userDao = new UserDaoImpl(dataSource);
       }


       @Test
       void getUserById_shouldReturnUser_whenUserExists() {
           // Arrange
           int userId = 10;
           // Act
           var userOpt = userDao.getUserById(userId);


           // Assert
           assertThat(userOpt).isPresent();
           User user = userOpt.get();
           assertThat(user.getId()).isEqualTo(userId);
           assertThat(user.getUsername()).isEqualTo("alice");
           assertThat(user.getEmail()).isEqualTo("alice@example.com");
       }


       @Test
       void getUserById_shouldReturnEmpty_whenUserDoesNotExist() {
           // Act
           var userOpt = userDao.getUserById(999);


           // Assert
           assertThat(userOpt).isEmpty();
       }


       @Test
       void getUserById_shouldReturnEmpty_whenIdIsNull() {
           // Assert and Act
           assertThatException()
                   .isThrownBy(() -> userDao.getUserById(null))
                   .isInstanceOf(DaoException.class)
                   .withMessage("SQL error");
       }
    }

Enter fullscreen mode Exit fullscreen mode

 
 

UserServiceImplTest

 
 

    @ExtendWith(MockitoExtension.class)
    class UserServiceImplTest {


       @Mock
       private UserDao userDao;


       @Mock
       private UserMapper userMapper;


       @InjectMocks
       private UserServiceImpl userService;


       @Test
       void test_whenGetUserById_shouldReturnUserDto_whenUserExists() {
           // Arrange
           Integer userId = 1;
           User user = new User(userId, "john_doe", "john@example.com");
           UserDto userDto = new UserDto(userId, "john_doe", "john@example.com");


           when(userDao.getUserById(userId)).thenReturn(Optional.of(user));
           when(userMapper.toDto(user)).thenReturn(userDto);


           // Act
           UserDto result = userService.getUserById(userId);


           // Assert
           assertThat(result).isEqualTo(userDto);
           verify(userDao).getUserById(userId);
           verify(userMapper).toDto(user);
       }


       @Test
       void test_whenGetUserById_shouldThrowNoSuchElementException_whenUserNotFound() {
           // Arrange
           Integer userId = 999;
           when(userDao.getUserById(userId)).thenReturn(Optional.empty());


           // Act and Assert
           assertThatThrownBy(() -> userService.getUserById(userId))
                   .isInstanceOf(NoSuchElementException.class)
                   .hasMessage("User not found with id: " + userId);


           verify(userDao).getUserById(userId);
           verify(userMapper, never()).toDto(any());
       }


       @Test
       void getUserById_shouldHandleNullId() {
           // Act and Assert
           assertThatThrownBy(() -> userService.getUserById(null))
                   .isInstanceOf(NullPointerException.class)
                   .hasMessage("id must not be null");
       }
    }

Enter fullscreen mode Exit fullscreen mode

 
 

Update “build.yml” file.

Add test_and_coverage next to the build job.

 
 

test_and_coverage:
 name: Test and Coverage
 runs-on: ubuntu-latest
 container:
   image: eclipse-temurin:17-jdk
 steps:
   - name: Checkout source code to docker ubuntu container
     uses: actions/checkout@v4
     with:
       token: ${{ secrets.GITHUB_TOKEN }}
       fetch-depth: 0
   - name: Run tests with coverage
     run: ./gradlew jacocoTestCoverageVerification
   - name: Upload test results
     uses: actions/upload-artifact@v4
     with:
       name: test-coverage-report
       path: 'build'
       overwrite: true
       retention-days: 5
Enter fullscreen mode Exit fullscreen mode

 
 

Add the test_and_coverage job after the build job under the needs section for the sast job.

This configuration ensures that the sast job will wait until the build and test_and_coverage jobs have completed their execution.

 
 

sast:
 needs:
   - build
   - test_and_coverage

Enter fullscreen mode Exit fullscreen mode

 
 

Add these two sast steps after the “Cache SonarQube packages” step. First one download the Jacoco report saved by the previous test_and_coverage job execution. The second one is to check if the report exists.

    - name: Download JaCoCo report
      uses: actions/download-artifact@v4
      with:
       name: test-coverage-report
       path: .

    - name: Verify report exists
      run: |
       ls -la ./reports/jacoco/test

Enter fullscreen mode Exit fullscreen mode

 
 

Complete “build.yml” file.

name: SonarQube


on:
 push:
   branches:
     - 'main'
 pull_request:
   branches:
     - main


jobs:
 branch-name-policy:
   name: branch-name-policy
   runs-on: ubuntu-latest
   steps:
     - name: Check PR source branch name
       shell: bash
       run: |
         if [ "${{ github.event_name }}" = "pull_request" ]; then
           BRANCH="${{ github.head_ref }}"
         else
           BRANCH="${{ github.ref_name }}"
         fi
         echo "PR head ref: $BRANCH"
         if [[ "$BRANCH" =~ ^(release/|hotfix/|feature/|bugfix/|test|main).* ]]; then
           echo "Allowed branch pattern: $BRANCH"
           exit 0
         else
           echo "::error ::Branch name '$BRANCH' is not allowed to merge into main. Allowed patterns: release/*, hotfix/*, feature/*, bugfix/*, test*"
           exit 1
         fi
 build:
   name: Build
   runs-on: ubuntu-latest
   container:
     image: eclipse-temurin:17-jdk
   steps:
     - name: Checkout source code to docker ubuntu container
       uses: actions/checkout@v4
       with:
         token: ${{ secrets.GITHUB_TOKEN }}
         fetch-depth: 0
     - name: Build project
       run: ./gradlew build -x test
 test_and_coverage:
   name: Test and Coverage
   runs-on: ubuntu-latest
   container:
     image: eclipse-temurin:17-jdk
   steps:
     - name: Checkout source code to docker ubuntu container
       uses: actions/checkout@v4
       with:
         token: ${{ secrets.GITHUB_TOKEN }}
         fetch-depth: 0
     - name: Run tests with coverage
       run: ./gradlew jacocoTestCoverageVerification
     - name: Upload test results
       uses: actions/upload-artifact@v4
       with:
         name: test-coverage-report
         path: 'build'
         overwrite: true
         retention-days: 5
 sast:
   needs:
     - build
     - test_and_coverage
   name: SonarQube Scan
   runs-on: ubuntu-latest
   steps:
     - name: Checkout source code to docker ubuntu container
       uses: actions/checkout@v4
       with:
         fetch-depth: 0


     - name: Cache SonarQube packages
       uses: actions/cache@v4
       with:
         path: ~/.sonar/cache
         key: ${{ runner.os }}-sonar
         restore-keys: ${{ runner.os }}-sonar


     - name: Download JaCoCo report
       uses: actions/download-artifact@v4
       with:
         name: test-coverage-report
         path: .


     - name: Verify report exists
       run: |
         ls -la ./reports/jacoco/test


     - name: SonarQube Scan
       env:
         SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
         SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
         SONAR_ORGANIZATION_KEY: ${{ secrets.SONAR_ORGANIZATION_KEY }}
         SONAR_PROJECT_KEY: ${{ secrets.SONAR_PROJECT_KEY }}
         SONAR_PROJECT_NAME: ${{ secrets.SONAR_PROJECT_NAME }}
       run: ./gradlew sonar --info
Enter fullscreen mode Exit fullscreen mode

 
 

Push the changes to the remote branch and check the state of the Pull Request’s jobs execution results. The pipeline failed because one last piece is missing.

Push the changes to the remote branch and check the state of the Pull Request’s jobs execution results. The pipeline failed because one last piece is missing.

Failed pipeline on Pull Request

 
 

We need to add the Jacoco test coverage report path in the SonarQube configuration for this project. This can be done by navigating to the SonarQube project, Administration, located in the bottom-left corner, and then General Settings. Paste reports/jacoco/test/jacocoTestReport.xml and click save.

Jacoco path in SonarQube UI

 
 

Go to GitHub, click on the SoarQube job to navigate to the GitHub Actions page. Click on the Sonar job and re-run the job.

Rerun icon on GitHub Actions UI

 
 

Now all jobs have passed.

GitHub Actions pipeline status page UI

 
 

Return to the Pull Request page, and now you can merge the changes to the main branch.

GitHub Pull Request pipeline status page UI

 
 

Merge the changes, and verify that all jobs pass after merging to the main branch.

GitHub main branch pipeline status page UI

 
 

Verify that test coverage appears on the SonarQube project. It shows the overall test coverage percentage. If you need charts on code coverage, click on the project and select the Coverage header at the top of the “Main Branch Evolution” section.

SonarQube test coverage result value displayed on scanning result

 
 

Conclusion

This article demonstrates how to maximize the benefits of SonarCloud by scanning locally with IntelliJ IDEA or Docker. This helps prevent insecure code from being pushed even to a feature branch and allows developers to work more productively.

 
 

Closing

This article covered SonarQube Cloud configuration and IntelliJ IDEA integration, as well as running SonarQube locally with Docker. In “Automating code security in CI/CD: SonarCloud SAST guide (Part 3)”, we will explore practical integration examples, configuration patterns, and CI/CD pipelines.

 
 

Links

Originally published on my personal blog: https://matevosian.tech/blog/post/SAST-part1-theory

Top comments (0)