Free · Self-hosted · Docker-based
Pandora

One script. Five tools.
Two vulnerable targets.

A self-hosted lab that installs SonarQube, OWASP ZAP, and Nessus alongside two intentionally vulnerable applications — DVWA and the custom-built VulnShop — so you can practice static and dynamic application security testing end to end, on your own machine.

5
security tools
2
vulnerable targets
10
documented CVEs
free
& open source
// the stack

Five tools, two jobs

Two tools read source code without running it. Two tools test an application while it's running. One tool seeds the data they all point at. Together they cover both halves of an AppSec programme — catching issues early in the code, and catching what only shows up once the app is alive.

Static Analysis (SAST) — reads the code
SonarQube:9000

Parses VulnShop's source code without executing it. Flags weak cryptography, hardcoded secrets, debug configuration, and risky patterns like raw SQL string concatenation — directly in the codebase, before anything ever runs.

Dynamic Analysis (DAST) — tests the running app
OWASP ZAP:8090 + GUI

Tests a running application the way an attacker would — crawling pages, fuzzing parameters, injecting payloads, and reading the responses. Available both as a point-and-click desktop app and a scriptable API daemon.

Nessus:8834

General infrastructure and vulnerability scanning, aimed at DVWA in this lab to show a different angle from app-specific DAST: known CVEs, missing patches, exposed services, and misconfigurations.

Targets — what gets scanned
DVWA:8888

The long-standing reference vulnerable web app. Useful for general scanning practice and as Nessus's target in this lab.

VulnShop:4040

A custom-built Laravel e-commerce app with ten documented, intentional vulnerabilities — the dataset behind the coverage matrix further down this page.


// setup

Get the lab running

Four steps from a clean machine to all five tools running. Each one explains what's actually happening before you run the command.

RequirementMinimum
OSUbuntu 22.04 LTS (or any Debian-based distro)
RAM6 GB — 8 GB recommended, SonarQube alone needs ~3 GB
Disk20 GB free
Run asYour own user via sudo — not logged in directly as root
InternetRequired on first run, to pull ~2 GB of Docker images
1

Clone the repository

Everything — the compose file, the setup script, and VulnShop's full source — lives in one repo.

terminal
git clone https://github.com/secretguard/SASTandDAST.git cd SASTandDAST
2

Run the setup script

student-setup.sh does five things in order: installs Docker if it isn't already present, installs the ZAP desktop application separately from the dockerized ZAP daemon, tunes a kernel parameter (vm.max_map_count) that SonarQube's embedded Elasticsearch needs to start reliably, creates a .env file and asks for an optional free Nessus activation code, then builds and starts every container. The whole run typically takes 5–10 minutes the first time, mostly spent downloading images.

terminal
sudo bash student-setup.sh
Run this with sudo as your normal user, not while logged in directly as root — the script needs to know which account to hand Docker permissions to afterward.
3

Confirm everything's healthy

healthcheck.sh checks Docker itself, then each of the eight containers individually — whether it's running, whether Docker's own health check passes, and where relevant, whether the service actually responds over HTTP with the content expected. A clean run prints every service's URL and login at the bottom.

terminal
bash healthcheck.sh
If SonarQube shows as still starting, that's normal — its embedded Elasticsearch can take 2–3 minutes on first boot. Re-run the check, or use bash healthcheck.sh --fix to auto-restart anything unhealthy.
4

Log in to each service

The containers are running, but three of the five need a one-time step in the browser before you can use them.

SonarQube — open http://localhost:9000, log in with admin / admin, and change the password when prompted. That forced change is a security default, not a bug.

Nessus — open https://localhost:8834 and accept the self-signed certificate. If you skipped the activation code during setup, it starts as an unregistered server: click the gear icon → Settings → Overview, and enter a free code from tenable.com/products/nessus/nessus-essentials — it's emailed instantly. Either way, plugin download takes 15–30 minutes before scans will run, with a progress bar on the dashboard.

DVWA and VulnShop need no setup beyond logging in — both databases are migrated and seeded automatically by their entrypoint scripts.

ServiceURLCredentials
SonarQube SASThttp://localhost:9000admin / admin (change on first login)
OWASP ZAP DASThttp://localhost:8090 or the zaproxy desktop appAPI key: lab-api-key-2024
Nessus DASThttps://localhost:8834admin / admin123 if prompted
DVWAhttp://localhost:8888admin / password
VulnShop — adminhttp://localhost:4040[email protected] / admin123
VulnShop — user 1http://localhost:4040[email protected] / password123
VulnShop — user 2http://localhost:4040[email protected] / password123

// day to day

Starting, stopping, and resetting

SonarQube projects, scan results, and DVWA/VulnShop state all persist in Docker volumes across start/stop cycles. Only one specific flag wipes that data — everything else is safe to run freely.

ActionCommand
Start labdocker compose up -d
Stop labdocker compose down
Check healthbash healthcheck.sh
Full reset (deletes all data)docker compose down -v && docker compose up -d
terminal — full command reference
# Start or stop a single service docker compose up -d sonarqube docker compose stop sonarqube # Restart a single service docker compose restart sonarqube # View live logs for a service docker compose logs -f vulnshop # Auto-restart anything unhealthy bash healthcheck.sh --fix # Rebuild VulnShop after source changes docker compose up -d --build vulnshop # Full reset — wipes all volumes and starts fresh docker compose down -v && docker compose up -d
Only docker compose down -v — the -v flag specifically — deletes volumes. A plain docker compose down always leaves your data intact for next time.

// the matrix

What each method actually catches

All ten of VulnShop's documented vulnerabilities, mapped against what SonarQube's static analysis flags and what ZAP/Nessus's dynamic testing flags. One of them gets past both.

#VulnerabilityStatic (SonarQube)Dynamic (ZAP / Nessus)OWASPCWE
1SQL Injection✓ caught✓ caughtA03:2021CWE-89
2Stored XSS✓ caught✓ caughtA03:2021CWE-79
3IDOR✕ neither✕ neitherA01:2021CWE-639
4CSRF✓ caught✓ caughtA01:2021CWE-352
5Weak Crypto (MD5)✓ caught— blindA02:2021CWE-328
6Hardcoded Credentials✓ caught— blindA07:2021CWE-798
7Debug Mode✓ caught✓ caughtA05:2021CWE-215
8Insecure File Upload~ partial✓ caughtA04:2021CWE-434
9No Rate Limiting~ partial✓ caughtA07:2021CWE-307
10Info Disclosure~ partial✓ caughtA05:2021CWE-200
IDOR is the one finding neither tool reliably catches. SonarQube has no way to know that order #4 belongs to a different customer than order #7 — that's a fact about your data, not a pattern in your code. ZAP and Nessus would need to log in as two different users and deliberately try to read each other's records, which isn't what a default scan does. Catching this requires a person who understands what correct authorization looks like for this specific app — which is exactly what the manual walkthrough below demonstrates.

// architecture

Eight containers, one command

docker compose up -d brings up every service below in dependency order — each database waits for nothing, each app waits for its database.

docker compose up -d
docker compose up -d │ ├─ sonar-db (PostgreSQL 15) ├─ sonarqube (SonarQube Community) ── :9000 ├─ zap (OWASP ZAP daemon) ── :8090 ├─ nessus (Nessus Essentials) ── :8834 ├─ dvwa-db (MariaDB 10) ├─ dvwa (DVWA) ── :8888 ├─ vulnshop-db (MariaDB 10) └─ vulnshop (Laravel app) ── :4040
sonarqube + sonar-db
Static analysis engine and its Postgres backend
zap
Dynamic scanning daemon, API-key protected
nessus
Infrastructure vulnerability scanner
dvwa + dvwa-db
Reference vulnerable app and its MariaDB backend
vulnshop + vulnshop-db
Custom Laravel target and its MariaDB backend

// walkthroughs

Five guided exercises

Work through these in order if you're new to SAST/DAST — each one builds on context from the last. Jump straight to #5 if you've already read the matrix above and want the punchline first.

🔍
Your First SonarQube Scan
Generate a token, scan VulnShop's source, read the findings.
SAST
🖱
ZAP via the GUI
Point-and-click dynamic scanning against VulnShop.
DAST
ZAP via the API
Same engine, driven entirely through HTTP calls.
DAST
🛰
Nessus vs DVWA
Infrastructure-level scanning, a different angle on DAST.
DAST
🔓
Manual IDOR Walkthrough
The one finding from the matrix that needs a human.
Manual
What SAST actually does: SonarQube parses source code into a structure it can pattern-match against known-bad rules — it never executes the application. That's why it's fast and deterministic enough to run on every commit in a pipeline, and also why it has no idea how the app behaves once real users start clicking around.
1

Generate a token

Open http://localhost:9000 → click your avatar → My AccountSecurityGenerate Token. Name it anything — vulnshop-scan works fine.

2

Run the scanner against VulnShop's source

This runs from the same directory you cloned the repo into.

terminal
docker run --rm \ --network sastanddast-main_default \ -v "$(pwd)/vulnshop:/usr/src" \ sonarsource/sonar-scanner-cli \ -Dsonar.projectKey=vulnshop \ -Dsonar.sources=/usr/src \ -Dsonar.host.url=http://sonarqube:9000 \ -Dsonar.token=<your-token>
3

Read the results

Open the vulnshop project in SonarQube's dashboard. Look specifically for the MD5 password-hashing flag, the hardcoded credentials warning, and the issue raised on the raw SQL query in ProductController.php — three of the matrix's "caught by SAST" rows, found without anything ever running.

Why start here: the desktop ZAP app is the easier on-ramp into DAST — a visible alert list and a single "Attack" button, good for understanding what a dynamic scan is actually doing before you start scripting it.
1

Launch ZAP

Search for zaproxy in your applications menu, or run it directly from a terminal.

terminal
zaproxy
2

Set the target

In the Quick Start tab, enter http://localhost:4040 in the URL field and click Attack.

3

Read the Alerts tab

ZAP crawls VulnShop, fuzzes parameters, and lists findings by severity as it goes. Expect to see the stored XSS in product reviews and the missing CSRF token on /change-password flagged directly — both of them rows the matrix marks as "caught" by dynamic analysis too.

Why this matters: this is the same scanning engine as the GUI, but driven entirely through HTTP calls to the daemon already running on :8090 inside Docker — this is how ZAP gets wired into an automated pipeline instead of waiting for someone to click a button.
1

Confirm the daemon is reachable

terminal
curl "http://localhost:8090/JSON/core/view/version/?apikey=lab-api-key-2024"
2

Spider the target

Targeting vulnshop by its service name — ZAP's container shares a Docker network with VulnShop, so it resolves directly.

terminal
curl "http://localhost:8090/JSON/spider/action/scan/?apikey=lab-api-key-2024&url=http://vulnshop"
3

Run an active scan

terminal
curl "http://localhost:8090/JSON/ascan/action/scan/?apikey=lab-api-key-2024&url=http://vulnshop"
4

Pull the results

terminal
curl "http://localhost:8090/JSON/core/view/alerts/?apikey=lab-api-key-2024"
Why it's here: Nessus works one level below app-specific DAST. It's a general vulnerability scanner built around plugins that check for known CVEs, missing patches, and misconfigurations, rather than crawling and fuzzing a single app's pages. Running it against DVWA shows what that angle looks like in practice.
1

Confirm activation

Settings → Overview should show the plugin feed as up to date. This can take 15–30 minutes after first setup if you're seeing it for the first time.

2

Create a new scan

Click New Scan → Basic Network Scan, name it, and set the target to dvwa — or run docker inspect dvwa on the host to get its container IP if the hostname doesn't resolve from Nessus's container.

3

Launch and review

Once the scan completes, open it and review the findings list grouped by severity — outdated packages and exposed services in the base image, alongside whatever DVWA's own intentional vulnerabilities surface.

Why this one's different: this is the finding from the coverage matrix that neither SonarQube nor ZAP caught on their own. Walking through it by hand is the point — it's the clearest demonstration on this page of why automated tooling still needs a person who understands the application's intended behavior.
1

Log in as Alice

At http://localhost:4040, sign in with [email protected] / password123.

2

Place an order

Buy anything, then note the order number in the resulting URL — for example /order/1.

3

Log out, log in as Bob

Sign out, then sign back in with [email protected] / password123.

4

Request Alice's order directly

Type Alice's order URL straight into the address bar — for example http://localhost:4040/order/1.

5

Observe the result

Bob sees Alice's shipping address, phone number, and full order contents — despite never being authorized to view this order. OrderController::show() calls findOrFail($id) with no check against the logged-in user's ID at all. The fix is a single ownership check; finding the need for it in the first place is what neither scanner managed on its own.


// troubleshooting

When something doesn't come up

Most of these resolve themselves with a little patience — Elasticsearch and Nessus's plugin feed are the usual culprits.

IssueFix
SonarQube shows 503Wait 2–3 min — Elasticsearch takes time to start
SonarQube keeps restartingCheck sysctl vm.max_map_count — should be ≥ 524288. Run sudo bash student-setup.sh to fix it.
VulnShop not loadingdocker compose logs vulnshop — usually still waiting on its database on first start
DVWA login failsdocker compose restart dvwa — its database may not have been ready yet
Nessus 503 / blank pageNormal during plugin download — check docker compose logs nessus
Port already in useEdit .env to change the port, then docker compose up -d
Want fresh datadocker compose down -v && docker compose up -d — deletes all volumes