Cybersecurity for Developers
Hands-onIntermediate 40 min· Modern threats and careers · 11 of 13

Security testing tools: ZAP, Semgrep and Trivy

Install and use three free security tools: Semgrep to scan source code, Trivy to scan dependencies, images and config, and OWASP ZAP to test a running app. Practise only on your own code and a deliberately vulnerable lab.

Reading code for security bugs is slow and people miss things. Security testing tools do the boring, repeatable part for you. In this tutorial you will use three free, open-source tools that cover three different questions:

- Semgrep (SAST, static analysis): is there a risky pattern in my source code? - Trivy: do my dependencies, container images and config files contain known vulnerabilities or mistakes? - OWASP ZAP (DAST, dynamic analysis): when my app is running, does it behave in an unsafe way?

Prerequisites: Python and pip, Docker Desktop (or Docker Engine) for ZAP, Trivy and the practice app. Use these tools only on code and apps that you own or have written permission to test. Scanning someone else's website without permission can be illegal. In this lesson the only web target is a deliberately vulnerable app running on your own machine.

What you will do

  1. 1Semgrep: scan source code and fix what it finds.
  2. 2Trivy: scan dependencies, an image and config files.
  3. 3ZAP: run a baseline scan against a local practice app.
  4. 4Put the scans into CI.

What each tool is good at

Three tools, three views

ToolTypeLooks atFindsMisses
SemgrepSASTSource codeInjection patterns, hard-coded secrets, unsafe functionsProblems that only appear at runtime, business-logic flaws
TrivySCA, image and config scannerDependencies, container images, IaC filesKnown CVEs, exposed secrets, misconfigurationsUnknown vulnerabilities, your own logic bugs
OWASP ZAPDASTA running web appMissing security headers, some injection and XSS issues, cookie flagsCode that the scanner cannot reach, authorisation flaws that need understanding of your users

No tool finds everything, and every tool produces false positives (a warning that is not a real problem) and false negatives (a real problem it misses). A scanner is a helper, not a judge. You still read each result and decide.

Part 1: Semgrep on source code

Create a folder scan-demo with a file app.py. It is intentionally unsafe so the tool has something to find. The key is fake.

scan-demo/app.py
import sqlite3
import subprocess

API_KEY = "sk_live_51HxExampleNotARealKey1234567890"  # hard-coded secret


def find_order(db: sqlite3.Connection, customer: str):
    # SQL built with string formatting: injectable
    query = "SELECT * FROM orders WHERE customer_name = '%s'" % customer
    return db.execute(query).fetchall()


def ping(host: str):
    # shell=True with user input: command injection
    return subprocess.run("ping -c 1 " + host, shell=True, capture_output=True)

Three problems: a hard-coded secret, SQL built with string formatting, and a shell command built from user input.

Terminal
pip install semgrep

cd scan-demo
semgrep scan --config auto .

--config auto picks rules from the Semgrep Registry that fit the languages it finds. It needs an internet connection to download the rules. On Windows, Semgrep's own docs recommend running it through WSL or Docker if the pip install gives you trouble.

Example output (rule names and wording will vary)
Findings:

  app.py
     python.lang.security.audit.subprocess-shell-true...
          Found 'subprocess' function 'run' with 'shell=True'...
     python.lang.security.audit.formatted-sql-query...
          Detected possible formatted SQL query. Use parameterized queries instead.

Read each finding: the file and line, the rule that matched and the explanation. Then fix the code. Here is the safe version:

scan-demo/app.py (fixed)
import os
import sqlite3
import subprocess

API_KEY = os.environ["API_KEY"]  # from the environment, never in the code


def find_order(db: sqlite3.Connection, customer: str):
    # Parameterised query: the driver keeps data separate from SQL
    return db.execute(
        "SELECT * FROM orders WHERE customer_name = ?", (customer,)
    ).fetchall()


def ping(host: str):
    # Validate first, and pass a list so no shell is involved
    if not host.replace(".", "").replace("-", "").isalnum():
        raise ValueError("bad host")
    return subprocess.run(["ping", "-c", "1", host], capture_output=True)

Secret from the environment, parameterised SQL, no shell. See *Managing secrets and API keys* and *Injection attacks: SQL, command and XSS* for the why.

Terminal
semgrep scan --config auto .

Run it again. The three findings should disappear. If a finding remains, read it: either fix it or understand why it is a false positive.

Writing a small custom rule

Semgrep rules look like the code they find. Save this as rules.yml to flag Flask apps started with debug mode on (debug mode lets anyone who can reach the app run code through the debugger, so it must never be on in production).

scan-demo/rules.yml
rules:
  - id: no-debug-true
    languages: [python]
    severity: WARNING
    message: Flask debug mode must not be enabled in production.
    pattern: $APP.run(..., debug=True, ...)

$APP is a metavariable that matches any name. ... matches any arguments.

scan-demo/demo.py
# demo.py
from flask import Flask

app = Flask(__name__)
app.run(host="0.0.0.0", debug=True)

A file the custom rule should flag.

Terminal
semgrep scan --config rules.yml demo.py

Runs only your rule. Custom rules are how teams enforce their own standards, for example "always use our safe_query() helper".

Part 2: Trivy on dependencies, images and config

Trivy is a scanner from Aqua Security. Install it from the official instructions for your system (Windows, macOS, Linux packages, or run it in Docker). Check with trivy --version. The first run downloads a vulnerability database, so it needs internet.

Terminal
# 1. Scan a project folder: dependency files, secrets, misconfigurations
trivy fs .

# Only show serious findings
trivy fs --severity HIGH,CRITICAL .

# 2. Scan a container image (public image used as a practice example)
trivy image python:3.9-slim

# 3. Scan Dockerfiles, Kubernetes YAML and Terraform for misconfigurations
trivy config .

fs reads lockfiles such as package-lock.json and requirements.txt. image scans the packages inside an image. config checks infrastructure files.

Trivy marks each finding with a severity (LOW to CRITICAL), the installed version and a fixed version if one exists. The fix is usually "upgrade this package to that version". If no fix exists, check whether the vulnerable feature is even used by you.

Terminal
# Make the command fail (exit code 1) when serious issues exist: useful in CI
trivy fs --severity HIGH,CRITICAL --exit-code 1 .

A non-zero exit code makes a CI job fail, which blocks a risky merge.

Bonus: SBOM

Trivy can also generate an SBOM (the ingredients list mentioned in *Dependencies and supply-chain security*): run trivy fs --format cyclonedx --output sbom.json . and look at the file.

Part 3: OWASP ZAP on a running app

ZAP (Zed Attack Proxy) is a free OWASP tool that sits between a browser and a web app, or crawls the app itself, and tests it. For safe practice, start OWASP Juice Shop, an intentionally vulnerable shop built for learning. It runs only on your computer.

Terminal 1
docker run --rm -p 3000:3000 bkimminich/juice-shop

Starts Juice Shop at http://localhost:3000. Leave this terminal open. Stop it with Ctrl+C.

ZAP's baseline scan crawls the site for a short time and only runs passive checks, so it does not attack the app. It is safe for CI and for learning. Run it in a second terminal. The folder you mount receives the report.

Terminal 2
mkdir zap-out

# macOS / Windows with Docker Desktop (host.docker.internal reaches your computer)
docker run --rm -v "$(pwd)/zap-out:/zap/wrk:rw" -t ghcr.io/zaproxy/zaproxy:stable \
  zap-baseline.py -t http://host.docker.internal:3000 -r report.html

# Linux: use host networking instead
docker run --rm --network host -v "$(pwd)/zap-out:/zap/wrk:rw" -t ghcr.io/zaproxy/zaproxy:stable \
  zap-baseline.py -t http://localhost:3000 -r report.html

Writes zap-out/report.html. If the report cannot be written because of folder permissions on Linux, check the ZAP Docker documentation for the user and permission settings. On Windows PowerShell use ${PWD} instead of $(pwd).

Example summary (numbers will differ)
PASS: ...
WARN-NEW: Content Security Policy (CSP) Header Not Set [10038] x 5
WARN-NEW: Missing Anti-clickjacking Header [10020] x 3
...
FAIL-NEW: 0   FAIL-INPROG: 0   WARN-NEW: 9   WARN-INPROG: 0   INFO: 0   IGNORE: 0   PASS: 50

Open report.html in a browser. Each alert has a risk level, the URL, an explanation and a suggested fix. Typical baseline findings are missing security headers (such as Content-Security-Policy or X-Frame-Options), cookies without HttpOnly or Secure flags, and information leaks in headers. These are real, common and usually cheap to fix.

The desktop ZAP application (a free download) also has an active scan that sends attack-like requests, and a proxy mode to explore the app by hand. Active scans can change data or overload an app, so use them only on your own test copies.

Only scan what you are allowed to scan

Never point ZAP, or any scanner, at a site you do not own or have written permission to test. Scanning someone else's server can break the law and can crash it. Practise only on your own apps and on deliberate practice targets such as Juice Shop.

Putting it into your workflow

A simple pipeline

WhenWhat to runWhy
While codingSemgrep in your editor or semgrep scanFast feedback on risky patterns
Before each commit or pull requestSemgrep and trivy fs in CIStops new problems from being merged
When building imagestrivy imageFinds old OS packages and libraries
On a test deploymentZAP baseline scanChecks headers, cookies and visible behaviour
.github/workflows/security.yml
name: security
on: [pull_request]
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Semgrep
        run: |
          pip install semgrep
          semgrep scan --config auto --error .
      - name: Trivy
        uses: aquasecurity/trivy-action@0.28.0
        with:
          scan-type: fs
          severity: HIGH,CRITICAL
          exit-code: 1

Runs Semgrep and Trivy on every pull request and fails the check on findings. Check each action's page for the latest recommended version before using it.

Prompt · Explain a scanner finding
I ran [Semgrep / Trivy / ZAP] on my [language] project and got this finding:

<finding>
[paste the finding]
</finding>

Explain in plain English: what the risk is, how an attacker could abuse it in my own app, whether it could be a false positive, and show a minimal code or config fix. If you are unsure, say what I should check.

What to expect: Verify the fix by re-running the tool and testing your app. Do not paste real secrets.

Try it: Scan and fix your own project

Run semgrep scan --config auto . and trivy fs --severity HIGH,CRITICAL . on one of your projects. Pick three findings. For each, write: real problem or false positive, the fix, and how you re-checked. Commit the fixes.

Try it: Fix the header warnings

Using the ZAP report from Juice Shop as a guide, pick the two header warnings. Then add those headers to a small FastAPI or Express app of your own (a middleware that sets X-Content-Type-Options: nosniff and X-Frame-Options: DENY is enough), and scan your own app with the ZAP baseline to confirm the warnings are gone.

Key takeaways

  1. SAST (Semgrep) reads code, SCA and image scanners (Trivy) check dependencies and config, DAST (ZAP) tests a running app. They find different things.
  2. semgrep scan --config auto ., trivy fs . and ZAP's zap-baseline.py are good first commands.
  3. Treat findings as leads: read, judge for false positives, fix, then scan again.
  4. Put scanners in CI with a failing exit code so problems are caught before merging.
  5. Scan only what you own or have written permission to test; practise on deliberately vulnerable apps like Juice Shop.

Check yourself

Finishing the quiz marks the lesson as done.

1. Which tool analyses your source code without running the program?

2. Trivy reports a HIGH vulnerability in a package that your code never uses. What is the sensible response?

3. What is a false positive?

4. Why run ZAP against Juice Shop on your own machine rather than a random website?

5. Which pipeline setting makes a CI job fail when serious vulnerabilities are found by Trivy?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free