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.
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
- Create an account: Visit SonarQube’s official website and sign up for a free or paid cloud account.
- Create a new project: Once logged in, create a new project by providing your repository details.
- 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.
- 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.
- CI providers:
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
- Open IntelliJ IDEA.
- Navigate to File > Settings > Plugins and search for the "SonarQube" plugin.
- Install and restart the IDE. It must appear in the “Installed” section.
- 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.
- 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.
Enter the Project key of your SonarQube project and click “Configure the connection”.
You will see the Connection section, click the plus to add a new connection.
Enter the name of the connection and click “Next”.
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.
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”.
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.
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.
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:
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
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'
}
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"]
}
}
Run the following command to start the container: docker-compose up -d, or you can run it from your IntelliJ IDEA
You can see the container is up and 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.
Enter the same name and project key as in your gradle-local.properties file. Then select to use the previous version.
Choose Locally as the Analysis Method
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
.
If you want to generate manually, you can do it as shown in the screenshot below.
Run the Sonar scan from the command line or from IntelliJ IDEA
Use this command ./gradlew sonar --info or run from your IntelliJ IDEA
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.
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'
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/**"
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/**'])
}))
}
}
Update the task test by adding this line at the end
finalizedBy jacocoTestReport
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
}
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');
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);
}
}
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");
}
}
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");
}
}
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
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
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
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
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.
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.
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.
Now all jobs have passed.
Return to the Pull Request page, and now you can merge the changes to the main branch.
Merge the changes, and verify that all jobs pass after merging to the main branch.
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.
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
- SonarQube Cloud - https://sonarcloud.io/
- SonarQube documentation - https://docs.sonarsource.com/
Originally published on my personal blog: https://matevosian.tech/blog/post/SAST-part1-theory




























Top comments (0)