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.
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.
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.
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.
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.
The long-standing reference vulnerable web app. Useful for general scanning practice and as Nessus's target in this lab.
A custom-built Laravel e-commerce app with ten documented, intentional vulnerabilities — the dataset behind the coverage matrix further down this page.
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.
| Requirement | Minimum |
|---|---|
| OS | Ubuntu 22.04 LTS (or any Debian-based distro) |
| RAM | 6 GB — 8 GB recommended, SonarQube alone needs ~3 GB |
| Disk | 20 GB free |
| Run as | Your own user via sudo — not logged in directly as root |
| Internet | Required on first run, to pull ~2 GB of Docker images |
Clone the repository
Everything — the compose file, the setup script, and VulnShop's full source — lives in one repo.
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.
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.
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.
bash healthcheck.sh --fix to auto-restart anything unhealthy.
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.
| Service | URL | Credentials |
|---|---|---|
| SonarQube SAST | http://localhost:9000 | admin / admin (change on first login) |
| OWASP ZAP DAST | http://localhost:8090 or the zaproxy desktop app | API key: lab-api-key-2024 |
| Nessus DAST | https://localhost:8834 | admin / admin123 if prompted |
| DVWA | http://localhost:8888 | admin / password |
| VulnShop — admin | http://localhost:4040 | [email protected] / admin123 |
| VulnShop — user 1 | http://localhost:4040 | [email protected] / password123 |
| VulnShop — user 2 | http://localhost:4040 | [email protected] / password123 |
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.
| Action | Command |
|---|---|
| Start lab | docker compose up -d |
| Stop lab | docker compose down |
| Check health | bash healthcheck.sh |
| Full reset (deletes all data) | docker compose down -v && docker compose up -d |
docker compose down -v — the -v flag specifically — deletes volumes. A plain docker compose down always leaves your data intact for next time.
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.
| # | Vulnerability | Static (SonarQube) | Dynamic (ZAP / Nessus) | OWASP | CWE |
|---|---|---|---|---|---|
| 1 | SQL Injection | ✓ caught | ✓ caught | A03:2021 | CWE-89 |
| 2 | Stored XSS | ✓ caught | ✓ caught | A03:2021 | CWE-79 |
| 3 | IDOR | ✕ neither | ✕ neither | A01:2021 | CWE-639 |
| 4 | CSRF | ✓ caught | ✓ caught | A01:2021 | CWE-352 |
| 5 | Weak Crypto (MD5) | ✓ caught | — blind | A02:2021 | CWE-328 |
| 6 | Hardcoded Credentials | ✓ caught | — blind | A07:2021 | CWE-798 |
| 7 | Debug Mode | ✓ caught | ✓ caught | A05:2021 | CWE-215 |
| 8 | Insecure File Upload | ~ partial | ✓ caught | A04:2021 | CWE-434 |
| 9 | No Rate Limiting | ~ partial | ✓ caught | A07:2021 | CWE-307 |
| 10 | Info Disclosure | ~ partial | ✓ caught | A05:2021 | CWE-200 |
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.
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.
Generate a token
Open http://localhost:9000 → click your avatar → My Account → Security → Generate Token. Name it anything — vulnshop-scan works fine.
Run the scanner against VulnShop's source
This runs from the same directory you cloned the repo into.
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.
Launch ZAP
Search for zaproxy in your applications menu, or run it directly from a terminal.
Set the target
In the Quick Start tab, enter http://localhost:4040 in the URL field and click Attack.
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.
:8090 inside Docker — this is how ZAP gets wired into an automated pipeline instead of waiting for someone to click a button.
Confirm the daemon is reachable
Spider the target
Targeting vulnshop by its service name — ZAP's container shares a Docker network with VulnShop, so it resolves directly.
Run an active scan
Pull the results
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.
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.
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.
Log in as Alice
At http://localhost:4040, sign in with [email protected] / password123.
Place an order
Buy anything, then note the order number in the resulting URL — for example /order/1.
Log out, log in as Bob
Sign out, then sign back in with [email protected] / password123.
Request Alice's order directly
Type Alice's order URL straight into the address bar — for example http://localhost:4040/order/1.
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.
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.
| Issue | Fix |
|---|---|
| SonarQube shows 503 | Wait 2–3 min — Elasticsearch takes time to start |
| SonarQube keeps restarting | Check sysctl vm.max_map_count — should be ≥ 524288. Run sudo bash student-setup.sh to fix it. |
| VulnShop not loading | docker compose logs vulnshop — usually still waiting on its database on first start |
| DVWA login fails | docker compose restart dvwa — its database may not have been ready yet |
| Nessus 503 / blank page | Normal during plugin download — check docker compose logs nessus |
| Port already in use | Edit .env to change the port, then docker compose up -d |
| Want fresh data | docker compose down -v && docker compose up -d — deletes all volumes |