Scan and Fix a Vulnerable Python App

Use Semgrep SAST to find, triage, and fix security flaws in a Python app.

Introduction

30 Second Summary

Every application you use stores personal data, processes payments, or handles sensitive information. A single overlooked vulnerability in the code can expose all of it.

In this project, you will build a deliberately vulnerable Python application, scan it with Semgrep (a static analysis tool used by professional security teams), triage the findings by severity, fix each vulnerability, and verify your fixes with a clean re-scan.

What You'll Build

You will create a small Flask-based user management API with intentional security flaws, then use Semgrep to find and fix every one of them.

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

  • A working SAST scanning workflow with Semgrep running locally on your Windows machine.
  • A triaged vulnerability report exported as JSON from scanning a deliberately insecure Python application.
  • A hardened version of the application with all critical findings resolved and verified clean by a re-scan.
  • Secret Mission: Write your own custom Semgrep rule to detect a project-specific anti-pattern.

Are there any prerequisites?

You should have basic familiarity with Python syntax (reading code, not writing from scratch) and be comfortable using the command line. No prior security experience is needed.

Set Up Your Environment

Before you can scan code for security flaws, you need the right tools installed. Semgrep is an open source static analysis tool that searches your source code for insecure patterns without ever running it.

In this step, you will install Semgrep and its dependency (pipx), configure your Windows environment for compatibility, and create a project folder to work in.

In this step, get ready to:
  • Verify your Python installation and configure UTF-8 encoding.
  • Install pipx and Semgrep CE.
  • Create your project folder and open it in VS Code.
Verify Python version and configure UTF-8 encoding

Semgrep requires Python 3.10 or later. You also need to configure Windows to use UTF-8 text encoding by default, because Semgrep relies on it to read source files correctly.

  • Press the Windows key to open your search bar.
  • Type PowerShell and press Enter to open it.
  • Confirm your Python version is 3.10 or higher by running this command:
python --version

You should see output like Python 3.10.x or higher.

Seeing a version lower than 3.10?

Download and install the latest Python from python.org. During installation, check the box to add python.exe to PATH. Restart PowerShell after installing.

help me install Python 3.10 or later on Windows

  • Configure UTF-8 encoding for Python by running this command:
[System.Environment]::SetEnvironmentVariable('PYTHONUTF8', '1', 'User')

What does this command do?

This sets a permanent environment variable called PYTHONUTF8 at the User scope. It tells Python to use UTF-8 encoding by default on your machine.

Without this, Semgrep may crash or produce garbled output when reading source files that contain non-ASCII characters.

  • Close PowerShell completely (click the X button or type exit).
  • Reopen PowerShell using the same method (press the Windows key, type PowerShell, press Enter).

Restarting picks up the new environment variable so it takes effect for the next commands.

Install pipx and Semgrep

Pipx manages CLI tools in isolated environments so they don't conflict with your other Python packages. You will use pipx to install Semgrep cleanly.

  • Install pipx by running these commands:
py -m pip install --user pipx
.\pipx.exe ensurepath

What do these commands do?

The first command installs pipx into your user directory using pip. The second command (.\pipx.exe ensurepath) adds pipx's install directory to your system PATH so you can run pipx from anywhere.

You may see a WARNING that pipx.exe is not on PATH. That is expected. The ensurepath command fixes it.

  • Close and reopen PowerShell again (same method as before) to pick up the PATH changes.
  • Install Semgrep by running this command:
pipx install semgrep
  • Confirm Semgrep installed successfully by running:
semgrep --version

✔️ I see a version number

Semgrep is ready to go. You should see something like 1.168.0. The exact number may differ. Any version number means the install succeeded.

ⓧ Command not found or error

If PowerShell does not recognize semgrep, try these fixes:

  • Make sure you restarted PowerShell after running .\pipx.exe ensurepath. PATH changes only take effect in a new session.
  • Verify pipx itself works by running pipx --version. If this also fails, close PowerShell, reopen it, and try again.
  • If pipx works but semgrep does not, run pipx install semgrep again in the new session.

help me troubleshoot Semgrep not being recognized in PowerShell after installing with pipx

Create the project folder and open it in VS Code

You need a dedicated folder for your vulnerable application and scan results. Creating it on your Desktop makes it easy to find.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir vuln-app-scan
cd vuln-app-scan
  • Open the folder in VS Code by running:
code .

What does code . do?

The code command launches VS Code from the terminal. The dot (.) tells it to open the current directory as a workspace.

VS Code opens with the vuln-app-scan folder in the Explorer sidebar. This is where all your project files will live.

VS Code not opening?

If code is not recognized, open VS Code manually. Press the Windows key, type VS Code, and press Enter. Then click File > Open Folder and select the vuln-app-scan folder on your Desktop.

help me open my project folder in VS Code on Windows

Your environment is locked and loaded. Next up, you will build a deliberately vulnerable Python application to give Semgrep something dangerous to find.

Build the Vulnerable Application

Your environment is ready with Semgrep installed and your project folder open. Now you need something to scan.

In this step, you will build a small Flask web application that acts as a user management API. The app will contain five deliberate security vulnerabilities drawn from the OWASP Top 10. These are the same kinds of flaws that Static Application Security Testing (SAST) tools catch every day in production codebases.

In this step, get ready to:
  • Create a requirements.txt and install Flask in a virtual environment.
  • Build app.py with five deliberate security vulnerabilities.
  • Verify the app starts without errors.
Create requirements.txt and install Flask

Every Python project lists its dependencies in a requirements.txt file so they can be installed reproducibly. You only need one dependency for this project: Flask, a lightweight web framework.

  • In VS Code, create a new file called requirements.txt in your vuln-app-scan folder.
  • Add the following content:
flask==3.1.1

What does this file do?

Pinning Flask to version 3.1.1 ensures everyone running this project gets the exact same version. This prevents subtle differences in behaviour between versions from causing unexpected scan results.

  • Save requirements.txt.

Next, you will create a Python virtual environment to isolate your project's packages from the rest of your system.

  • Create and activate a virtual environment, then install Flask by running these commands in your PowerShell terminal:
python -m venv venv
.\venv\Scripts\Activate.ps1
pip install -r requirements.txt

What do these commands do?

  • python -m venv venv creates an isolated Python environment in a folder called venv.
  • .\venv\Scripts\Activate.ps1 activates the environment so that pip install puts packages inside it instead of system-wide.
  • pip install -r requirements.txt reads the file you just created and installs Flask 3.1.1.
  • Confirm the install succeeded by checking that your terminal prompt now starts with (venv) and that pip install printed Successfully installed near the end of its output.

Seeing a permissions error on Activate.ps1?

PowerShell's execution policy may block scripts. Run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned and then try the activate command again.

If you see a different error, help me troubleshoot my virtual environment activation in PowerShell.

Create app.py with five deliberate vulnerabilities

This is the intentionally insecure application you will scan in the next step. Each vulnerability represents a real-world flaw that Semgrep's rules are designed to catch.

  • Create a new file called app.py in your vuln-app-scan folder.
  • Add the imports, app configuration, and database helper functions:
import os
import sqlite3
from flask import Flask, request, jsonify

app = Flask(__name__)
app.secret_key = "super_secret_key_12345"

DATABASE = "users.db"


def get_db():
    conn = sqlite3.connect(DATABASE)
    conn.row_factory = sqlite3.Row
    return conn


def init_db():
    conn = get_db()
    conn.execute(
        "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT, password TEXT, email TEXT)"
    )
    conn.execute(
        "INSERT OR IGNORE INTO users (id, username, password, email) VALUES (1, 'admin', 'admin123', 'admin@example.com')"
    )
    conn.commit()
    conn.close()

What does this code do?

  • The imports bring in os, sqlite3, and Flask's core utilities.
  • app.secret_key = "super_secret_key_12345" is vulnerability #1: a hardcoded secret. Anyone reading the source code (or a public repository) now knows the key used to sign session cookies.
  • get_db() connects to a local SQLite database. init_db() creates a users table and seeds it with one admin record.
  • Below the init_db() function, add the search route:
@app.route("/users/search")
def search_users():
    username = request.args.get("username", "")
    conn = get_db()
    query = "SELECT * FROM users WHERE username = '" + username + "'"
    results = conn.execute(query).fetchall()
    conn.close()
    users = [{"id": row["id"], "username": row["username"], "password": row["password"], "email": row["email"]} for row in results]
    return jsonify(users)

What vulnerabilities are in this route?

This route contains two of the five vulnerabilities.

  • Vulnerability #2 (SQL injection): The query is built by concatenating user input directly into the SQL string. An attacker can type ' OR '1'='1 as the username to dump the entire table.
  • Vulnerability #3 (sensitive data exposure): The response includes the password field. API responses should never return credentials to the client.
  • Below search_users(), add the export route, health check, and the main entry point:
@app.route("/users/export")
def export_users():
    filename = request.args.get("filename", "users.csv")
    os.system("sqlite3 users.db '.mode csv' '.output " + filename + "' 'SELECT * FROM users;'")
    return jsonify({"message": "Exported to " + filename})


@app.route("/health")
def health():
    return jsonify({"status": "healthy"})


if __name__ == "__main__":
    init_db()
    app.run(debug=True, host="0.0.0.0")

What vulnerabilities are in this section?

  • Vulnerability #4 (command injection): os.system() passes user-supplied input directly to the operating system shell. An attacker can inject arbitrary commands by manipulating the filename parameter.
  • Vulnerability #5 (debug mode in production): debug=True exposes a full interactive debugger to anyone who triggers an error. Combined with host="0.0.0.0", the app is reachable from any network interface.
  • Save app.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved app.py before moving on.

ⓧ I'd like to double check the full code

Here is the complete app.py file:

import os
import sqlite3
from flask import Flask, request, jsonify

app = Flask(__name__)
app.secret_key = "super_secret_key_12345"

DATABASE = "users.db"


def get_db():
    conn = sqlite3.connect(DATABASE)
    conn.row_factory = sqlite3.Row
    return conn


def init_db():
    conn = get_db()
    conn.execute(
        "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT, password TEXT, email TEXT)"
    )
    conn.execute(
        "INSERT OR IGNORE INTO users (id, username, password, email) VALUES (1, 'admin', 'admin123', 'admin@example.com')"
    )
    conn.commit()
    conn.close()


@app.route("/users/search")
def search_users():
    username = request.args.get("username", "")
    conn = get_db()
    query = "SELECT * FROM users WHERE username = '" + username + "'"
    results = conn.execute(query).fetchall()
    conn.close()
    users = [{"id": row["id"], "username": row["username"], "password": row["password"], "email": row["email"]} for row in results]
    return jsonify(users)


@app.route("/users/export")
def export_users():
    filename = request.args.get("filename", "users.csv")
    os.system("sqlite3 users.db '.mode csv' '.output " + filename + "' 'SELECT * FROM users;'")
    return jsonify({"message": "Exported to " + filename})


@app.route("/health")
def health():
    return jsonify({"status": "healthy"})


if __name__ == "__main__":
    init_db()
    app.run(debug=True, host="0.0.0.0")
Verify the app runs

Before scanning, confirm the application starts without import errors. A broken app would produce misleading scan results.

  • Start the Flask development server by running:
python app.py
  • You should see output that includes Running on http://127.0.0.1:5000 and a warning about the development server.
  • Press Ctrl+C in your terminal to stop the server.

Why stop the server?

You only needed to confirm Flask starts without errors. Semgrep scans your source code files directly. It does not need the server running.

Seeing a ModuleNotFoundError?

This means Flask is not installed in your active environment. Check that your terminal prompt starts with (venv). If it does not, re-run .\venv\Scripts\Activate.ps1 and then pip install -r requirements.txt.

Still stuck? Help me fix a ModuleNotFoundError when running my Flask app.

Your vulnerable app is ready to scan. Next up, you will point Semgrep at this code and see exactly which flaws it catches.

Scan with Semgrep and Triage Findings

Your vulnerable application is built and running. Now it is time to point a security scanner at it and see what falls out.

In this step, you will run Semgrep against app.py using its Python security ruleset. Semgrep downloads community-maintained rules from its registry and checks your code against hundreds of known vulnerability patterns in seconds.

In this step, get ready to:
  • Run a SAST scan against your vulnerable application.
  • Export findings to a JSON report file.
  • Triage findings by severity level.
Run Semgrep with the Python security ruleset

Semgrep's p/python ruleset contains hundreds of rules covering SQL injection, command injection, hardcoded secrets, insecure configurations, and more. When you run a scan, Semgrep downloads these rules from its registry and matches them against your source code.

  • Run your first security scan against app.py by running this command:
semgrep scan --config p/python app.py

Semgrep downloads the ruleset on first run, so give it a moment. When the scan finishes, you will see a Scan Summary followed by a list of Code Findings.

What does this output mean?

The Scan Summary at the bottom tells you how many rules ran and how many findings were detected.

Each finding in the list above it shows four things:

  • The rule name (a namespaced identifier like python.lang.security.audit.dangerous-system-call-audit).
  • A brief explanation of the vulnerability and why it is dangerous.
  • The exact line of code that triggered the finding.
  • A remediation suggestion telling you how to fix it.
  • Scroll through the terminal output and count how many findings Semgrep detected. You should see multiple findings flagging different vulnerabilities in app.py.

Not seeing any findings?

  • Make sure you are in the vuln-app-scan directory where app.py lives. Run ls to confirm the file is there.
  • Check your internet connection. Semgrep needs to download rules from its registry on the first run.
  • If you see a timeout error, try running the command again. The registry download can be slow on the first attempt.

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

Export findings to JSON for your report

The terminal output is great for quick review, but a JSON export gives you a structured, machine-readable report. This is what security teams use to track findings over time and integrate with ticketing systems.

  • Export the scan results to a JSON file by running this command:
semgrep scan --config p/python --json --json-output=findings.json app.py

What do these flags do?

  • --json tells Semgrep to format the output as JSON instead of human-readable text.
  • --json-output=findings.json writes the JSON results to a file called findings.json in your current directory.
  • Confirm findings.json was created by running:
ls
  • You should see findings.json listed alongside app.py and requirements.txt.
  • Open findings.json in VS Code by clicking on it in the Explorer sidebar.

What is inside findings.json?

Each finding in the JSON file is an object with fields that give you everything you need for a vulnerability report:

  • check_id identifies which rule flagged the issue.
  • severity rates the finding (ERROR, WARNING, or INFO).
  • path shows which file contains the vulnerability.
  • start and end give you the exact line numbers.
  • message explains the vulnerability and how to fix it.

Don't see findings.json?

  • Make sure you typed --json-output=findings.json with no spaces around the equals sign.
  • If the scan itself failed, fix whatever error appeared in the terminal and re-run the command.

Why isn't my findings.json file appearing after the Semgrep scan?

Triage the findings by severity

Finding vulnerabilities is only half the job. A security engineer's real skill is triage: deciding which findings are urgent, which can wait, and which are noise. Semgrep assigns each finding a severity level that maps to risk.

  • Review your findings and categorize them into three severity buckets:

How do severity levels map to action?

  • ERROR (Critical) means you must fix this. These are exploitable vulnerabilities like SQL injection and command injection that an attacker can use right now.
  • WARNING (Medium) means you should fix this soon. These are risky patterns like hardcoded secrets and debug mode enabled in production.
  • INFO (Low) means this is good to fix when possible. These are best-practice violations that increase risk over time.

In a real production triage, ERROR findings get fixed in the current sprint. WARNING findings get tickets. INFO findings go into the backlog.

  • Look at the severity field for each finding in findings.json. Match each finding to one of the three categories above.
  • For the ERROR findings, note which lines of code are affected. These are the ones you will fix in the next step.

What is SAST?

Static Application Security Testing (SAST) analyzes source code without running it. Unlike a penetration test that attacks a live app, SAST reads the code and pattern-matches against known vulnerability signatures. It catches problems before the code ever reaches production.

You now have a complete vulnerability report with findings triaged by severity. Next up, you will fix every critical and high finding and prove the fixes work with a clean re-scan.

Fix the Vulnerabilities and Verify

You have a triaged list of findings and understand why each one is dangerous. Now it's time to remediate every vulnerability in your application.

You'll fix the most critical findings first. Command injection and SQL injection are directly exploitable by attackers, so they take priority. Then you'll clean up configuration issues and verify your fixes with a clean re-scan.

In this step, get ready to:
  • Fix command injection and SQL injection vulnerabilities.
  • Fix hardcoded secrets, debug mode, and data exposure.
  • Re-scan and confirm zero findings.
Fix command injection and SQL injection

These two vulnerabilities are rated ERROR (Critical) because an attacker can exploit them directly from a web request. You'll replace unsafe patterns with their secure equivalents.

  • In VS Code, open app.py and find the export_users() function.
  • Find this line inside the function:
    os.system("sqlite3 users.db '.mode csv' '.output " + filename + "' 'SELECT * FROM users;'")
  • Replace the entire os.system(...) line with the following code:
    # Validate filename before using it in a command
    if not filename.isalnum() and not filename.replace(".", "").replace("_", "").isalnum():
        return jsonify({"error": "Invalid filename"}), 400
    subprocess.run(
        ["sqlite3", "users.db", ".mode csv", ".output " + filename, "SELECT * FROM users;"],
        capture_output=True,
        text=True,
    )

What does this fix do?

The vulnerable version used os.system() which passes the entire string to a shell. An attacker could inject shell commands through the filename parameter.

The fix uses subprocess.run() with a list of arguments, which bypasses the shell entirely. It also validates the filename to reject suspicious characters before the command runs.

  • Now add import subprocess at the top of the file. Find this line:
import os
import sqlite3
  • Add import subprocess between those two lines, so the imports look like this:
import os
import subprocess
import sqlite3

You won't see a change yet. The SQL injection fix is next.

  • Scroll up to the search_users() function and find these lines:
    conn = get_db()
    query = "SELECT * FROM users WHERE username = '" + username + "'"
    results = conn.execute(query).fetchall()
    conn.close()
    users = [{"id": row["id"], "username": row["username"], "password": row["password"], "email": row["email"]} for row in results]
  • Replace all five of those lines with the following code:
    conn = get_db()
    # Use parameterized query to prevent SQL injection
    results = conn.execute(
        "SELECT id, username, email FROM users WHERE username = ?", (username,)
    ).fetchall()
    conn.close()
    # Exclude password from the response to prevent data exposure
    users = [{"id": row["id"], "username": row["username"], "email": row["email"]} for row in results]

What does this fix do?

The vulnerable version concatenated user input directly into the SQL string. An attacker could submit a username like ' OR '1'='1 to dump the entire database.

The ? placeholder tells SQLite to treat the value as data, never as SQL syntax. This also fixes two other findings: the SELECT no longer fetches the password column, and the response dictionary no longer returns it to the caller.

  • Save app.py.

You won't see a runtime change yet. The remaining configuration fixes come next.

Fix hardcoded secrets, debug mode, and data exposure

The password exposure was already addressed in the previous fix. Two configuration-level issues remain: the hardcoded secret key and the unsafe debug/host settings.

  • In app.py, find this line near the top:
app.secret_key = "super_secret_key_12345"
  • Replace it with this line:
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "change-me-in-production")

Why use an environment variable?

Hardcoded secrets end up in version control, CI logs, and anywhere the source code is shared. Reading from an environment variable keeps the secret out of the codebase. The fallback string makes local development work without extra setup.

  • Now scroll to the very last line of the file and find:
    app.run(debug=True, host="0.0.0.0")
  • Replace it with:
    app.run(debug=False, host="127.0.0.1")

What do these changes prevent?

Setting debug=True exposes an interactive debugger that lets anyone execute arbitrary Python on your server. Binding to 0.0.0.0 makes the app reachable from any network interface.

The fix disables debug mode and binds only to 127.0.0.1 (localhost), so the app is only reachable from your own machine.

  • Save app.py.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file, then move on to the re-scan below.

ⓧ I'd like to double check the full code

Here is the complete app.py after all fixes:

import os
import subprocess
import sqlite3
from flask import Flask, request, jsonify

app = Flask(__name__)
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "change-me-in-production")

DATABASE = "users.db"


def get_db():
    conn = sqlite3.connect(DATABASE)
    conn.row_factory = sqlite3.Row
    return conn


def init_db():
    conn = get_db()
    conn.execute(
        "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT, password TEXT, email TEXT)"
    )
    conn.execute(
        "INSERT OR IGNORE INTO users (id, username, password, email) VALUES (1, 'admin', 'admin123', 'admin@example.com')"
    )
    conn.commit()
    conn.close()


@app.route("/users/search")
def search_users():
    username = request.args.get("username", "")
    conn = get_db()
    results = conn.execute(
        "SELECT id, username, email FROM users WHERE username = ?", (username,)
    ).fetchall()
    conn.close()
    users = [{"id": row["id"], "username": row["username"], "email": row["email"]} for row in results]
    return jsonify(users)


@app.route("/users/export")
def export_users():
    filename = request.args.get("filename", "users.csv")
    if not filename.isalnum() and not filename.replace(".", "").replace("_", "").isalnum():
        return jsonify({"error": "Invalid filename"}), 400
    subprocess.run(
        ["sqlite3", "users.db", ".mode csv", ".output " + filename, "SELECT * FROM users;"],
        capture_output=True,
        text=True,
    )
    return jsonify({"message": "Exported to " + filename})


@app.route("/health")
def health():
    return jsonify({"status": "healthy"})


if __name__ == "__main__":
    init_db()
    app.run(debug=False, host="127.0.0.1")
Re-scan and confirm a clean report

The real proof of remediation is a clean scan. If Semgrep finds zero issues on the same ruleset, your fixes are verified.

  • Run the same Semgrep scan you used in Step 3:
semgrep scan --config p/python app.py
  • Check the Scan Summary in your terminal output. You should see a line showing 0 findings.

What should I see?

Semgrep prints a Scan Summary box at the end. The Findings line should show 0. The final summary line confirms the scan ran all rules on 1 file with no matches.

Still seeing findings?

Check that you saved app.py after making all edits. Unsaved changes are not picked up by the scan.

Compare your file against the full code in the double-check tab above. A common issue is leaving the old query variable in place alongside the new parameterized call.

help me fix remaining Semgrep findings in my app.py

  • Confirm the app still starts without errors by running:
python app.py
  • You should see the Flask development server start on http://127.0.0.1:5000.
  • Press Ctrl+C to stop the server.

App won't start?

If you see ModuleNotFoundError: No module named 'subprocess' or a similar import error, make sure your virtual environment is still active. Run .\venv\Scripts\Activate.ps1 first.

If you see a SyntaxError, check that your import subprocess line is on its own line between import os and import sqlite3.

help me fix a startup error in my hardened app.py

Your application is now hardened against the critical vulnerabilities Semgrep found. Next up, you'll extend your security workflow by writing a custom rule that catches project-specific anti-patterns.

Secret mission

Write a Custom Semgrep Rule

Registry rules cover common vulnerabilities, but every codebase has its own anti-patterns. In this secret mission, you will write a custom Semgrep rule from scratch to detect a project-specific security risk: printing sensitive variables to stdout.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing cloud costs. Decide whether to keep your resources for future scanning, pause them to free up space, or delete them entirely.

Resources you used:

  • vuln-app-scan project folder (contains app.py, requirements.txt, findings.json, rules/, venv/, and users.db).
  • Semgrep CLI (installed via pipx).
  • pipx (installed via pip).
  • PYTHONUTF8 environment variable (set for Semgrep on Windows).

Keep everything running

No action needed. Choose this if you plan to scan more Python projects with Semgrep or want to revisit the vulnerable app for practice.

  • Semgrep stays installed and ready to scan any codebase.
  • Your vuln-app-scan folder stays on your Desktop as a reference.
  • The PYTHONUTF8 environment variable remains set. This is harmless and benefits other Python tools.

Pause - I'll come back to this later

Deactivate the virtual environment to free up the active shell session. Your files and tools stay installed for next time.

  • If your virtual environment is still active, deactivate it by running:
deactivate
  • Your project folder, Semgrep, and pipx all remain installed and ready when you return.

Delete - I don't want to use this again

Remove all project resources and tools installed during this project. Run these commands in PowerShell.

  • If your virtual environment is active, deactivate it first:
deactivate
  • Uninstall Semgrep from pipx by running:
pipx uninstall semgrep
  • Uninstall pipx itself by running:
py -m pip uninstall pipx -y
  • Remove the PYTHONUTF8 environment variable by running:
[System.Environment]::SetEnvironmentVariable('PYTHONUTF8', $null, 'User')
  • Delete the project folder by running:
cd ~\Desktop
Remove-Item -Recurse -Force vuln-app-scan
  • Confirm the folder is gone by running:
ls ~\Desktop

You should no longer see vuln-app-scan in the output.

Nice Work!

Nice Work!

You just completed a full application security workflow from start to finish. You built a vulnerable app, scanned it with the same tool professional AppSec teams use daily, triaged the findings, fixed every flaw, and proved your fixes with a clean re-scan.

You've learned how to:

  • Scan Python code with Semgrep using community rulesets and export findings to a JSON report for triage.
  • Identify and fix real-world vulnerabilities including SQL injection, command injection, and hardcoded secrets using secure coding patterns.
  • Establish a repeatable scan-triage-fix-verify workflow that maps directly to the Product Security Engineer daily loop.
  • Secret Mission: Authored a custom Semgrep rule to enforce a project-specific security policy as code.

Ready to quiz yourself?