SAST with Semgrep and SonarQube

Write custom taint-tracking rules and scan a vulnerable app with two SAST tools.

Introduction

30 Second Summary

Every application has code that could be exploited. Static Application Security Testing tools automatically scan your source code for vulnerabilities before attackers find them.

In this project, you will install and configure two industry-standard SAST tools, scan a deliberately vulnerable Python application, and write a custom rule that detects SQL injection using taint analysis.

What You'll Build

A complete SAST analysis workflow using Semgrep and SonarQube Community Build against a vulnerable Flask application.

By the end of this project, you'll have:

  • A custom taint-tracking rule in Semgrep that detects SQL injection via unsanitized user input.
  • A running SonarQube instance in Docker with scan results visible in the web dashboard.
  • The vocabulary to explain taint analysis, false positives, and SAST tool comparison in a DevSecOps interview.
  • Secret Mission: Write a second custom rule that detects insecure deserialization via pickle.loads and test it.

Are there any prerequisites?

None. This guide includes setup for all required tools including Semgrep, Docker, and Python. You just need a Windows machine with Docker Desktop, Python 3.10+, and VS Code installed.

Install Semgrep and Set Up the Vulnerable App

Before you can scan code for security vulnerabilities, you need the right tools installed and a target to scan. SAST tools like Semgrep work by analyzing source code without running it, looking for patterns that indicate security flaws.

In this step, you will install Semgrep, verify Docker is ready for later use, and create a deliberately vulnerable Python application that contains real-world security flaws for scanning.

In this step, get ready to:
  • Configure PowerShell for Python UTF-8 encoding and install Semgrep.
  • Verify Docker Desktop is running and ready.
  • Create a vulnerable Flask application to use as a scan target.
Configure Python for UTF-8 and install Semgrep via pipx

Semgrep requires Python to use UTF-8 encoding on Windows. Without this setting, you may encounter encoding errors during scans.

  • Press the Windows key to open your search bar.
  • Type PowerShell and press Enter to open it.
  • Set the UTF-8 environment variable for Python by running this command:
[System.Environment]::SetEnvironmentVariable('PYTHONUTF8', '1', 'User')

What does this command do?

This sets a permanent User-level environment variable called PYTHONUTF8 to 1.

It tells Python to always use UTF-8 encoding for reading and writing text, which prevents encoding crashes when Semgrep processes files.

  • Close PowerShell completely.
  • Reopen PowerShell using the same search method (Windows key, type PowerShell, press Enter).

Why close and reopen?

Environment variables set at the User scope only take effect in new terminal sessions. Your current PowerShell window still has the old environment loaded.

  • Install pipx by running this command:
python -m pip install --upgrade pipx
  • Ensure pipx is on your PATH by running:
pipx ensurepath

What is pipx?

pipx installs Python command-line tools into isolated virtual environments. This keeps Semgrep and its dependencies separate from your other Python projects.

The ensurepath command adds the directory where pipx stores installed tools to your system PATH, so you can run them from anywhere.

  • Close and reopen PowerShell again so the updated PATH takes effect.
  • Install Semgrep by running:
pipx install semgrep

This may take a minute while pipx downloads and installs Semgrep and its dependencies.

  • Verify the installation by running:
semgrep --version

✔️ I see a version number

You should see 1.165.0 or a similar version number. Semgrep is installed and ready to use.

ⓧ Command not found

If PowerShell cannot find semgrep, the pipx binary directory is not on your PATH.

  • Run pipx ensurepath again.
  • Close and reopen PowerShell.
  • Try semgrep --version again.

If the issue persists, run pipx list to confirm Semgrep appears in your installed packages. If it does, copy the path shown next to the semgrep entry and run it directly using that full path.

Verify Docker is running

You will use Docker later to run SonarQube. Confirming it works now saves troubleshooting time in a later step.

  • Check your Docker installation by running:
docker --version
  • Confirm Docker Desktop is actively running by pulling and running a test container:
docker run --rm hello-world

✔️ I see "Hello from Docker!"

Docker is running and can pull images from Docker Hub. You are all set for the SonarQube step later.

ⓧ I see an error about the Docker daemon

This means Docker Desktop is installed but not currently running.

  • Press the Windows key, type Docker Desktop, and press Enter to launch it.
  • Wait for the Docker Desktop window to show a green "Running" indicator (this can take 30-60 seconds).
  • Return to PowerShell and run docker run --rm hello-world again.
Create the project directory and vulnerable app

You need a deliberately vulnerable application to scan. This Flask app contains intentional security flaws that represent common real-world vulnerabilities.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir sast-lab
cd sast-lab
  • Confirm the folder was created by running:
ls

You should see the empty sast-lab directory listed. Since you already moved into it, the output will be empty (no files yet).

  • Open the sast-lab folder in VS Code by running:
code .
  • In VS Code, create a new file called app.py by pressing Ctrl+N then Ctrl+S and saving it as app.py in the sast-lab folder.
  • Copy the following vulnerable Flask application code into app.py:
from flask import Flask, request
import sqlite3
import os
import pickle
import base64

app = Flask(__name__)

def get_db():
    conn = sqlite3.connect("app.db")
    return conn

# VULNERABLE: SQL Injection via string concatenation
@app.route("/user")
def get_user():
    user_id = request.args.get("id")
    conn = get_db()
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users WHERE id = " + user_id)
    result = cursor.fetchone()
    conn.close()
    return str(result)

# VULNERABLE: Cross-Site Scripting (XSS)
@app.route("/search")
def search():
    query = request.args.get("q")
    return "<h1>Results for: " + query + "</h1>"

# VULNERABLE: Command Injection
@app.route("/ping")
def ping():
    host = request.args.get("host")
    output = os.popen("ping -c 1 " + host).read()
    return output

# SAFE: Parameterized query (should NOT trigger taint rule)
@app.route("/user-safe")
def get_user_safe():
    user_id = request.args.get("id")
    conn = get_db()
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
    result = cursor.fetchone()
    conn.close()
    return str(result)

# VULNERABLE: Insecure deserialization (for Secret Mission)
@app.route("/load-session")
def load_session():
    session_data = request.cookies.get("session_data")
    decoded = base64.b64decode(session_data)
    data = pickle.loads(decoded)
    return str(data)

if __name__ == "__main__":
    app.run(debug=True)

What vulnerabilities are in this app?

This file contains five route handlers, each demonstrating a different security pattern:

  • SQL injection (/user): user input is concatenated directly into a SQL query string.
  • XSS (/search): user input is embedded directly into HTML without escaping.
  • Command injection (/ping): user input is passed directly to a shell command.
  • Safe parameterized query (/user-safe): uses a placeholder ? to separate data from the query. This should NOT trigger taint rules.
  • Insecure deserialization (/load-session): user-controlled cookie data is decoded and passed to pickle.loads(), which can execute arbitrary code.
  • Save app.py with Ctrl+S.
  • Confirm the file exists by switching back to PowerShell and running:
ls

You should see app.py listed in the output.

Don't see app.py in the output?

  • Make sure you saved the file inside the sast-lab folder, not somewhere else on your system.
  • Check that your PowerShell is still in the sast-lab directory by running pwd. If not, run cd ~/Desktop/sast-lab to get back.
  • help me find where my app.py was saved

✔️ Awesome, I've got everything!

Great. Double check you have saved app.py and that it appears when you run ls in your sast-lab folder.

ⓧ I'd like to double check the full code

Here is the complete app.py for comparison:

from flask import Flask, request
import sqlite3
import os
import pickle
import base64

app = Flask(__name__)

def get_db():
    conn = sqlite3.connect("app.db")
    return conn

# VULNERABLE: SQL Injection via string concatenation
@app.route("/user")
def get_user():
    user_id = request.args.get("id")
    conn = get_db()
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users WHERE id = " + user_id)
    result = cursor.fetchone()
    conn.close()
    return str(result)

# VULNERABLE: Cross-Site Scripting (XSS)
@app.route("/search")
def search():
    query = request.args.get("q")
    return "<h1>Results for: " + query + "</h1>"

# VULNERABLE: Command Injection
@app.route("/ping")
def ping():
    host = request.args.get("host")
    output = os.popen("ping -c 1 " + host).read()
    return output

# SAFE: Parameterized query (should NOT trigger taint rule)
@app.route("/user-safe")
def get_user_safe():
    user_id = request.args.get("id")
    conn = get_db()
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
    result = cursor.fetchone()
    conn.close()
    return str(result)

# VULNERABLE: Insecure deserialization (for Secret Mission)
@app.route("/load-session")
def load_session():
    session_data = request.cookies.get("session_data")
    decoded = base64.b64decode(session_data)
    data = pickle.loads(decoded)
    return str(data)

if __name__ == "__main__":
    app.run(debug=True)

Your environment is locked in. Semgrep is installed, Docker is running, and your vulnerable app is ready to be scanned. Next up, you will run your first Semgrep scan and see real security findings.

Scan with Semgrep Built-in Rules

Your environment is ready and app.py is loaded with intentional vulnerabilities.

Now it is time to point Semgrep at the code and see what it finds. In this step, you will run scans using Semgrep's built-in rules, export the results to JSON for structured review, and compare findings across different rulesets.

In this step, get ready to:
  • Run your first SAST scan with auto-detected rules.
  • Export scan results to a JSON file for structured review.
  • Run a Python-specific ruleset and compare findings.
Run a scan with auto-detected rules

Semgrep's --config auto flag automatically selects rules from the Semgrep Registry that match your project's language and frameworks.

  • In your PowerShell terminal (still inside the sast-lab folder), run your first SAST scan by running this command:
semgrep scan --config auto

The scan takes a moment to download rules and analyze your code. You should see output listing several findings, each with a rule ID, file path, line number, severity level, and a code snippet.

What does this output mean?

Each finding in the terminal output represents a pattern match against a known vulnerability. The key parts to notice are:

  • The rule ID tells you which rule matched (e.g., a SQL injection rule or an XSS rule).
  • The file and line number point to the exact location in your code.
  • The severity (ERROR, WARNING, INFO) indicates how critical the finding is.
  • The code snippet shows the matched pattern so you can verify the finding.
  • Look through the output and identify at least two different vulnerability types (SQL injection, XSS, command injection).

Not seeing any findings?

Make sure you are running the command from inside the sast-lab folder where app.py lives. Semgrep scans the current directory by default.

If you see a network error downloading rules, check your internet connection and try again.

Why is Semgrep not finding vulnerabilities in my app.py?

Export results to JSON for review

Terminal output is useful for a quick look, but JSON output gives you structured data you can search, filter, and feed into other tools.

  • Export the scan results to a JSON file by running this command:
semgrep scan --config auto --json --json-output=results.json

What does this command do?

  • --json tells Semgrep to format its output as JSON instead of human-readable text.
  • --json-output=results.json writes the JSON results to a file called results.json in your current directory.
  • Confirm the file was created by running:
ls results.json

You should see results.json listed in the output.

  • Open results.json in VS Code by running:
code results.json

Inside the file, look for the results array. Each object in this array represents one finding with fields like check_id, path, start/end line numbers, extra.message, and extra.severity.

Why export to JSON?

In a real DevSecOps pipeline, JSON output feeds into CI/CD systems, ticketing tools, or dashboards. It gives you a machine-readable record of every finding that you can track over time.

The check_id field is especially important. It uniquely identifies the rule that triggered, so you can reference it when triaging or suppressing findings.

results.json is empty or missing?

If ls results.json shows no file, make sure you typed the filename correctly with no spaces around the = sign in --json-output=results.json.

If the file exists but is nearly empty, check that app.py is in the same directory where you ran the command.

Help me figure out why my results.json file is empty or missing.

Run with a specific security ruleset

The auto config pulls rules for all detected languages and categories. You can also target specific rulesets from the Semgrep Registry for more focused scanning.

  • Run a scan using only the Python-specific ruleset by running this command:
semgrep scan --config "p/python"

What is the p/python ruleset?

Semgrep organizes rules into rulesets prefixed with p/. The p/python ruleset contains Python-specific security and correctness rules maintained by the Semgrep community.

Different rulesets may overlap or catch different issues. Comparing results between auto and p/python shows you how rule selection affects what gets flagged.

  • Compare the output to your earlier auto scan. Note which findings appear in both scans and whether any new findings appear.

Interview-ready insight

In a DevSecOps interview, you can explain that --config auto casts a wide net across all languages and frameworks detected in your codebase. Targeted rulesets like p/python let you focus on language-specific patterns when you know what you are scanning.

Seeing different findings than expected?

The p/python ruleset may produce more or fewer findings than auto depending on how the registry curates each. This is expected behavior and demonstrates why rule selection matters in SAST.

Help me understand the difference between my auto scan and p/python scan results.

You have now seen how Semgrep identifies vulnerabilities using its built-in rules. Next up, you will write your own custom taint-tracking rule that gives you precise control over what Semgrep detects.

Write a Custom Taint-Tracking Rule

You have seen what Semgrep's built-in rules can detect across your vulnerable app. Built-in rules are generic, though. They cannot encode your organization's specific security policies or catch custom patterns unique to your codebase.

In this step, you will write a custom taint-tracking rule from scratch. This rule traces data from an untrusted source (user input) through the program to a dangerous sink (a raw SQL query), and flags the vulnerability only when no sanitizer is present along the way.

In this step, get ready to:
  • Create a taint-mode rule YAML file with sources, sinks, and sanitizers.
  • Test the custom rule against the vulnerable app and confirm a true positive.
  • Verify the rule avoids false positives on safe, parameterized code.
Create the rule YAML file

Taint analysis tracks data from untrusted sources through the program to dangerous sinks. If data reaches a sink without passing through a sanitizer, the tool flags it.

What is the source-sink-sanitizer model?

Think of your application like a plumbing system. A source is where untrusted data enters (like request.args.get(...)). A sink is a dangerous function where that data must not arrive raw (like cursor.execute()).

A sanitizer is any operation that makes tainted data safe before it reaches the sink (like casting to int() or using parameterized queries). If data flows from source to sink without hitting a sanitizer, the rule fires.

  • Create the rules/ directory inside your sast-lab folder by running this command:
mkdir rules
  • Confirm the directory exists by running:
ls

You should see rules listed alongside app.py and results.json.

  • In VS Code, create a new file called sql-injection-taint.yaml inside the rules folder.
  • Paste the following taint-mode rule into the file:
rules:
  - id: custom-sql-injection-taint
    message: >
      User input from request.args.get() flows into cursor.execute()
      without parameterization. Use parameterized queries instead.
    severity: ERROR
    languages:
      - python
    mode: taint
    pattern-sources:
      - pattern: request.args.get(...)
    pattern-sinks:
      - patterns:
          - pattern: cursor.execute($QUERY, ...)
          - pattern-not: cursor.execute("...", (...))
      - pattern: cursor.execute($QUERY)
    pattern-sanitizers:
      - pattern: int(...)
      - pattern: str(int(...))
    metadata:
      category: security
      technology:
        - flask
        - sqlite3
      confidence: HIGH
      cwe:
        - "CWE-89: SQL Injection"

What does this rule do?

  • mode: taint activates dataflow tracking instead of simple pattern matching.
  • pattern-sources defines where untrusted data enters. Here, any call to request.args.get(...) is a source of tainted data.
  • pattern-sinks defines where tainted data must not arrive. The first sink uses pattern-not to exclude parameterized queries (those passing a tuple) from being flagged.
  • pattern-sanitizers defines operations that make data safe. Wrapping input in int() eliminates the injection risk, so the rule ignores that flow.
  • Save rules/sql-injection-taint.yaml.

YAML not saving correctly?

YAML is whitespace-sensitive. Make sure you are using spaces (not tabs) for indentation, and that each nested level is indented by exactly two spaces.

Check that the file is saved inside sast-lab/rules/, not directly in sast-lab/. help me fix YAML indentation errors in my Semgrep rule

Test the custom rule against the vulnerable app

Your rule is written. Now you will point Semgrep at it and see if it catches the SQL injection in app.py.

  • Run Semgrep with your custom rule by running this command:
semgrep scan --config rules/sql-injection-taint.yaml

You should see a finding that flags app.py at the get_user function, where request.args.get("id") flows directly into cursor.execute() via string concatenation.

How to read the output

Semgrep displays each finding with the rule ID (custom-sql-injection-taint), the file and line number, the severity (ERROR), and the message you wrote in the rule. It also shows the exact code snippet that triggered the match.

This is a true positive: the code is genuinely vulnerable because user input reaches the SQL query without any sanitization.

How would you explain taint analysis in an interview?

"Taint analysis tracks data from untrusted sources through the program to dangerous sinks. If data reaches a sink without passing through a sanitizer, the tool flags it. It automates the manual process of tracing user input through code to see if it ever hits a function where unsanitized input is dangerous."

No findings reported?

Make sure you are running the command from the sast-lab directory, not from inside the rules/ folder.

Confirm the file path is correct: rules/sql-injection-taint.yaml relative to where you run the command. help me debug why my custom Semgrep taint rule shows zero findings

Verify the rule handles false positives correctly

A useful rule must not only catch real vulnerabilities. It must also stay silent on safe code. Your app.py already includes a get_user_safe function that uses parameterized queries.

  • In VS Code, open app.py and scroll to the get_user_safe function near line 36.
  • Notice it uses cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,)) with a tuple argument.

Why does the safe function avoid the taint flag?

Your rule's first sink entry uses pattern-not: cursor.execute("...", (...)) to exclude any call where cursor.execute receives a string literal plus a tuple. This is how parameterized queries look in Python's sqlite3 module.

Because the safe function passes (user_id,) as a separate tuple argument, it matches the exclusion pattern and is never flagged as a sink.

  • Confirm the rule correctly ignores the safe function by re-running your scan:
semgrep scan --config rules/sql-injection-taint.yaml

You should see findings only for the get_user function (the vulnerable one). The get_user_safe function should not appear in the results.

How would you explain false positives in an interview?

"A false positive in SAST occurs when the tool flags code that is actually safe. Common causes include the tool not recognizing custom sanitization functions, or not understanding framework-specific safety mechanisms.

You triage by checking: does user input actually reach the sink? Is there validation the tool missed? SAST tools have no runtime context, so they cannot determine if a code path is reachable or if middleware applies validation before the function executes."

The safe function is getting flagged?

Double-check that your pattern-sinks section includes the pattern-not exclusion nested under patterns: (note the plural). The pattern-not must be at the same indentation level as pattern: inside that list.

help me fix my Semgrep rule that is flagging parameterized queries as vulnerable

✔️ Awesome, I've got everything!

Great. Double check you have saved rules/sql-injection-taint.yaml and your scan produces at least one finding on the vulnerable function.

ⓧ I'd like to double check the full code

Here is the complete YAML rule file for comparison:

rules:
  - id: custom-sql-injection-taint
    message: >
      User input from request.args.get() flows into cursor.execute()
      without parameterization. Use parameterized queries instead.
    severity: ERROR
    languages:
      - python
    mode: taint
    pattern-sources:
      - pattern: request.args.get(...)
    pattern-sinks:
      - patterns:
          - pattern: cursor.execute($QUERY, ...)
          - pattern-not: cursor.execute("...", (...))
      - pattern: cursor.execute($QUERY)
    pattern-sanitizers:
      - pattern: int(...)
      - pattern: str(int(...))
    metadata:
      category: security
      technology:
        - flask
        - sqlite3
      confidence: HIGH
      cwe:
        - "CWE-89: SQL Injection"

You now have a custom taint rule that catches real SQL injection and stays quiet on safe code. Next up, you will spin up SonarQube in Docker and scan the same app to compare how a different SAST tool reports its findings.

Run SonarQube in Docker and Scan

You now have Semgrep producing targeted findings with both built-in and custom rules. Time to bring in a second perspective.

In this step, you will spin up SonarQube Community Build inside a Docker container, scan the same vulnerable app, and compare what each tool catches. This gives you a real-world understanding of how different SAST tools complement each other.

In this step, get ready to:
  • Start SonarQube Community Build in Docker and configure a project.
  • Create a properties file and run the SonarScanner CLI via Docker.
  • Review and compare SonarQube findings with Semgrep results.
Start SonarQube Community Build

SonarQube is a code quality and security platform that provides a web dashboard for reviewing findings. The Community Build edition is free and runs easily in Docker with an embedded database for testing.

  • Start the SonarQube container by running this command:
docker run -d --name sonarqube -p 9000:9000 sonarqube:community

What does this command do?

  • docker run -d starts a new container in detached mode (runs in the background).
  • --name sonarqube gives the container the name sonarqube so you can reference it easily later.
  • -p 9000:9000 maps port 9000 on your machine to port 9000 inside the container.
  • sonarqube:community pulls and runs the Community Build image from Docker Hub.

Why does SonarQube take time to start?

SonarQube runs an embedded Elasticsearch instance for indexing. The first startup takes 1-2 minutes while it initializes the database and search engine. Wait until you can load the login page before proceeding.

  • Wait 1-2 minutes, then open your browser and navigate to http://localhost:9000.
  • Log in with the default credentials: enter admin for both the username and password.
  • SonarQube will prompt you to change the default password. Choose a new password and confirm it.

Can't load the login page?

  • SonarQube may still be starting. Run docker logs sonarqube and look for a message indicating the server is ready.
  • Make sure Docker Desktop is running. If the container exited, restart it with docker start sonarqube.
  • Confirm nothing else is using port 9000 on your machine.

Help me troubleshoot why SonarQube won't start in Docker on my Windows machine.

Now create a local project in SonarQube to receive your scan results.

  • In the top navigation bar, select the Projects tab.
  • Click Create Project in the top right corner, then select Local Project.
  • Enter sast-lab as the Project key.
  • Enter SAST Lab as the Display name.
  • Click Set up.

Next, generate an authentication token so the scanner can submit results to your project.

  • Navigate to User (your avatar in the top right corner), then select My Account.
  • Select the Security tab.
  • In the token generation form, enter a name like sast-lab-token and click Generate.
  • Copy the token immediately. You will not be able to see it again after dismissing this notification.

Keep your token safe

Paste your token somewhere temporary (like a text file or sticky note on your desktop) for the next command. Do not commit it to version control or share it. You will use it once in the next substep.

Create the sonar-project.properties file and scan

The SonarScanner needs a properties file to know which project to scan and how to analyze it. This file lives in your project root alongside the code.

  • Create a new file called sonar-project.properties in your sast-lab folder with the following content:
sonar.projectKey=sast-lab
sonar.projectName=SAST Lab
sonar.sources=.
sonar.sourceEncoding=UTF-8
sonar.language=py
sonar.python.version=3.10

What do these properties mean?

  • sonar.projectKey must match the project key you set in the SonarQube UI.
  • sonar.sources=. tells the scanner to analyze all files in the current directory.
  • sonar.language=py restricts analysis to Python files.
  • sonar.python.version=3.10 tells SonarQube which Python version to use for compatibility checks.
  • Save sonar-project.properties.
  • Confirm the file exists by running:
ls

You should see sonar-project.properties alongside app.py and the rules folder in the output.

Now run the SonarScanner CLI via Docker. This container handles Java dependencies internally so you do not need Java installed locally.

  • Run the SonarScanner against your project by running this command:
docker run --rm -e SONAR_HOST_URL="http://host.docker.internal:9000" -e SONAR_TOKEN="your-token-here" -v "${PWD}:/usr/src" sonarsource/sonar-scanner-cli

What does this command do?

  • docker run --rm runs the scanner container and removes it when done.
  • SONAR_HOST_URL uses host.docker.internal to reach your SonarQube container from inside another container. This is how containers on Windows reach the host machine.
  • SONAR_TOKEN authenticates the scan. Replace your-token-here with the token you copied earlier.
  • -v "${PWD}:/usr/src" mounts your current directory into the scanner container at /usr/src (the default analysis directory).
  • sonarsource/sonar-scanner-cli is the official SonarScanner CLI Docker image.
  • Wait for the scan to complete. Look for EXECUTION SUCCESS in the terminal output.

Seeing EXECUTION FAILURE?

  • Verify you replaced your-token-here with the actual token you copied from SonarQube.
  • Make sure the SonarQube container is still running. Check with docker ps and confirm sonarqube is listed.
  • Confirm sonar-project.properties exists in the current directory and that sonar.projectKey matches the project key you set in the UI.

Help me debug why my SonarScanner Docker command is failing.

✔️ Awesome, I've got everything!

Great. Double check you have saved sonar-project.properties and that your terminal shows EXECUTION SUCCESS.

ⓧ I'd like to double check the full code

Here is the complete sonar-project.properties file:

sonar.projectKey=sast-lab
sonar.projectName=SAST Lab
sonar.sources=.
sonar.sourceEncoding=UTF-8
sonar.language=py
sonar.python.version=3.10
Review and compare findings

With the scan complete, SonarQube now displays your project findings in a web dashboard. This is where you can explore how a different tool categorizes the same vulnerabilities.

  • Open http://localhost:9000 in your browser.
  • Click on the sast-lab project to open its dashboard.
  • Navigate to the Issues tab to see the full list of findings.

SonarQube categorizes findings by type (Bug, Vulnerability, Code Smell), severity, and estimated effort to fix.

How do Semgrep and SonarQube compare?

Semgrep excels at custom, organization-specific rules and taint tracking for injection-style vulnerabilities. Its YAML rule format makes it fast to write rules that match your exact codebase patterns.

SonarQube provides broader code quality analysis including complexity metrics, code smells, and maintainability scores. It also provides a persistent dashboard that tracks findings across scans over time.

In a mature DevSecOps program, teams use both. Semgrep handles precision security rules in CI. SonarQube provides ongoing quality visibility across the entire codebase.

  • Compare the SonarQube findings with what Semgrep reported. Note which vulnerability types overlap and which are unique to each tool.
  • Pay attention to how SonarQube surfaces code smells and complexity issues that Semgrep does not flag.

Interview talking point

When asked about SAST in interviews, mention that you have experience with both Semgrep and SonarQube. Explain that Semgrep's strength is custom taint rules for injection flaws, while SonarQube's strength is broad code quality coverage with a persistent dashboard. Mature teams use both in their CI/CD pipelines.

Secret mission

Detect Insecure Deserialization with a Custom Rule

You've written a taint rule with a template to guide you. Can you write one from scratch for a completely different vulnerability class? This time you'll use Semgrep's pattern-propagators feature to track taint through intermediate transformations like base64 decoding.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs locally using Docker and free tools, so there are no ongoing financial costs. The SonarQube container does consume RAM and disk space while running.

Resources you used:

  • SonarQube Docker container (sonarqube) running on port 9000.
  • Project folder sast-lab with app.py, results.json, rules/, and sonar-project.properties.
  • Semgrep CLI installed via pipx.
  • PYTHONUTF8 environment variable set at the User level.

Keep everything running

No action needed. Choose this if you plan to keep experimenting with Semgrep rules or running SonarQube scans against other projects.

  • Your SonarQube container stays active at http://localhost:9000.
  • If you reboot your machine, restart the container by running docker start sonarqube in PowerShell.
  • Semgrep remains available globally via pipx. You can scan any project folder at any time.

Pause - I'll come back to this later

Stop the running container to free up RAM and CPU. Your SonarQube data persists inside the container, so your project and scan history will be there when you return.

  • Stop the SonarQube container by running:
  • When you're ready to resume, run docker start sonarqube and visit http://localhost:9000.
docker stop sonarqube

Delete - I don't want to use this again

Remove all project resources and start fresh. This removes the SonarQube container, your project files, and Semgrep from your system.

  • Remove the SonarQube container by running:
docker rm -f sonarqube
  • Clean up any anonymous Docker volumes left behind by running:
docker volume prune -f
  • Navigate up one level and remove the project folder by running:
cd ..
Remove-Item -Recurse -Force sast-lab
  • Uninstall Semgrep by running:
pipx uninstall semgrep
  • Remove the PYTHONUTF8 environment variable by running:
[System.Environment]::SetEnvironmentVariable('PYTHONUTF8', $null, 'User')

Close and reopen PowerShell for the environment variable removal to take effect.

Nice Work!

Nice Work!

You just built a complete local SAST analysis environment with two industry-standard tools. You can now scan code for security vulnerabilities, write custom detection rules, and explain the concepts behind them in an interview.

You've learned how to:

  • Install Semgrep and run SAST scans against a deliberately vulnerable Flask application, exporting findings to JSON for review.
  • Write a custom taint-tracking rule from scratch that detects SQL injection using sources, sinks, and sanitizers.
  • Deploy SonarQube Community Build in Docker, scan the same app using the SonarScanner CLI Docker image, and compare findings between both tools.
  • Secret Mission: Write an insecure deserialization detection rule with custom propagators that tracks tainted data through base64.b64decode() into pickle.loads().

Ready to quiz yourself?