Security tools
Nmap Basics on Your Own Lab
Nmap maps the open ports on your own machine. Learn connect and SYN scans, service detection and output files, then match each port to its process.
Run ZAP's passive baseline scan against Juice Shop on your own machine, read the report, and check one alert by hand before trusting it.
The Zed Attack Proxy, usually called ZAP, is a free tool for finding problems in web applications. It does two jobs that matter for this guide. It works as an intercepting proxy, so you can read every request your browser sends and every response it gets back. It also runs scanners that check that traffic for weak patterns.
A proxy is a middleman. Normally your browser sends a request straight to 127.0.0.1:3000. When it goes through ZAP, the browser sends the request to ZAP instead. ZAP forwards it to the application, receives the answer, stores the pair in its history, and returns the answer to the browser. The application does not know anything changed. You only get a copy of the conversation.
That placement is why the tool helps. A scanner sees what the server sends back. A proxy shows you what the browser actually did, including headers, cookies, redirects and the JavaScript the page loaded. Many web problems only show up when you compare those two views.
ZAP has two kinds of scanning, and the difference decides what you are allowed to run and where.
Passive scanning reads traffic that already passed through ZAP. Each response is checked for things like a missing Content-Security-Policy header, cookies without the HttpOnly flag, or stack traces in error pages. The passive rules do not send attack strings. They only look at what came back.
Active scanning sends new requests built to provoke a reaction. It changes parameters, submits odd input and watches how the application responds. Most of the serious findings come from this kind of testing, and it is also the kind that behaves like an attack. Run it only against a lab you own, or against a system where you have written permission and a defined scope.
The baseline scan in this guide is passive. The ZAP documentation describes it as a script that runs the spider against the target for one minute by default, waits for the passive rules to finish, and then writes the report. It does not run attack payloads, so it is a reasonable first step on your own lab.
Each ZAP alert is a rule that matched. A rule can match a header that is missing, a pattern in a response body, or a combination of both. It cannot know what your application is for, which data it protects, or whether an attacker can reach the path that matters. An alert tells you where to look. It does not confirm that a weakness can be exploited.
Two kinds of error show up all the time. Some alerts are false positives: the rule matched something harmless. Other problems are missed completely, because the scanner never reached the page or never sent the input that triggers the flaw. What turns a lead into a finding is a step you can repeat yourself: a request you sent, the response you saw, and a plain explanation of why it matters. Until you have all three, the alert stays in a list marked "to verify."
Keep that rule in mind through the rest of the guide. The scanner gives you a starting point, and the verification work is yours.
You need four things:
docker version.The lab stays on your machine. Publish its port on 127.0.0.1 only. A port published without that prefix can listen on every network interface, and Juice Shop is built to be broken.
Start Juice Shop in the background with a name you can use later:
docker run -d --name juice-local -p 127.0.0.1:3000:3000 bkimminich/juice-shopThe -d flag runs the container detached. I left out --rm on purpose, so the container stays around until you stop and remove it by name. The 127.0.0.1: prefix keeps the port off your network.
Check that it answers:
curl -i http://127.0.0.1:3000/On my machine the first lines of the response looked like this. I removed the progress meter that curl prints when it is not run with -s:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Feature-Policy: payment 'self'
X-Recruiting: /#/jobs
Accept-Ranges: bytes
Cache-Control: public, max-age=0
Last-Modified: Sun, 08 Nov 2026 09:06:18 GMT
ETag: W/"2700-1a1ba6896a5"
Content-Type: text/html; charset=UTF-8
Content-Length: 9984Your headers and dates will differ. Record the Juice Shop version too, because the application changes between releases. This endpoint returns it:
curl -s http://127.0.0.1:3000/rest/admin/application-versionOn my machine it printed {"version":"20.3.0"}. Put that number in your notes next to the scan results.
Run the scanner from the folder where you want the report to appear. This is the command for Docker Desktop:
docker run --rm -t -v "$(pwd)":/zap/wrk ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://host.docker.internal:3000 -r zap-report.htmlEach part has a job:
--rm removes the scanner container when the scan ends.-v "$(pwd)":/zap/wrk mounts your current folder inside the container, so the report is written to your disk.-t http://host.docker.internal:3000 is the target. From inside a container, host.docker.internal points at your computer, where the lab listens.-r zap-report.html writes the full HTML report into the mounted folder.On Linux, Docker Desktop's host alias does not exist by default. Add a host mapping to the same command:
docker run --rm -t --add-host=host.docker.internal:host-gateway -v "$(pwd)":/zap/wrk ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://host.docker.internal:3000 -r zap-report.htmlI tested the Docker Desktop form only. On Linux, a port published on 127.0.0.1 may not accept connections that arrive through the Docker bridge address. If the scanner reports connection errors, read the first lines of its output before changing anything else.
The first attempt failed without scanning anything. The console said A file based option has been specified but the directory '/zap/wrk' is not mounted, then printed the usage text and exited with code 3. The mount did not work in my Git Bash session, and I did not find out why. Running the command with MSYS_NO_PATHCONV=1 and the full Windows path in the -v flag fixed it:
MSYS_NO_PATHCONV=1 docker run --rm -t -v "D:/path/to/your/folder:/zap/wrk" ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://host.docker.internal:3000 -r zap-report.htmlReplace the path with your own folder. The first run also downloads the ZAP image, and on my connection that took most of twelve minutes. Later runs start in seconds.
The scanner prints one line per rule. Here is the part that matters, from my run, with the long list of passing rules shortened:
Using the Automation Framework
Total of 116 URLs
PASS: Vulnerable JS Library (Powered by Retire.js) [10003]
PASS: Cookie No HttpOnly Flag [10010]
...
PASS: CSP [10055]
...
WARN-NEW: Content Security Policy (CSP) Header Not Set [10038] x 5
WARN-NEW: Cross-Domain Misconfiguration [10098] x 5
WARN-NEW: Modern Web Application [10109] x 5
FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 3 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 59WARN-NEW means a rule flagged something that the scan has not seen before. PASS means the rule ran and found nothing. The number after each rule name is ZAP's rule identifier, which you can search for in the ZAP documentation.
The command exited with code 2 in my run. The baseline documentation lists the exit codes: 0 means success, 1 means at least one FAIL, 2 means at least one WARN and no FAILs, and 3 means any other failure. A code of 2 is normal here. It only means the scanner found something to read.
Open zap-report.html in a browser. The summary at the top of my report showed High 0, Medium 2, Low 0 and Informational 1. Three alerts made up the whole list:
Content-Security-Policy header, which lets the browser decide which scripts and frames to load.Access-Control-Allow-Origin: *, which allows any website to read the response in a browser.Every alert has the same parts: a description, the URLs where it fired, the evidence ZAP saw, some extra notes, and a suggested fix. Read the evidence and the URL first. The description tells you what the rule means in general. The evidence tells you what happened in this response.
Do not rank the report by the risk label alone. ZAP assigns a risk level from its rule, not from your application's data. A Medium on a page with no sensitive data may matter less than a Low on an endpoint that handles accounts.
Pick the CSP alert. Its evidence is an absence, so the test is simple: request the page, look for the header, and record the result. This command prints the number of matching lines:
curl -s -i http://127.0.0.1:3000/ -o /dev/null -D - | grep -i -c '^content-security-policy'On my machine it printed 0. The full header list I captured earlier had no Content-Security-Policy line either. The alert matches what the server sends.
Now the second alert. The evidence is the Access-Control-Allow-Origin: * line you already saw in the curl -i output. Your check should answer a different question: does this response contain anything a third-party site should not read? The lab's root page is public HTML, so the header itself is not the problem. ZAP's text makes the same point. Browsers do not let a third-party page read authenticated responses this way. The alert becomes serious only when an endpoint that returns private data also sends a wildcard origin. If you find such an endpoint, test it with your own lab account and record what it returns.
For the third alert there is nothing to prove. It describes the application type. Leave it in the report as context and move on.
Write each verified result in a table or a text file with four columns: the alert, the request you sent, what you observed, and your verdict. A verdict can be "confirmed," "not reproduced," or "needs more context." Those three answers cover most of what you will do with scanner output.
This step lets you watch real requests, which is the part that makes the proxy useful beyond scanning. You can do it with the desktop application or with a container running ZAP in daemon mode. I describe both. I ran the daemon version. I did not run the desktop walkthrough, so you will see no output from it below.
Install ZAP from the official site. The getting-started page links to the current installer. Open it and choose Manual Explore, which starts a browser that already sends its traffic through ZAP. Enter http://127.0.0.1:3000 and click through the home page. Then open the History tab. Each row is one request and response pair, with the method, URL and status code. Compare those rows with what you see in your own browser's developer tools. The differences show what the browser sent that you did not expect.
Start ZAP as a background proxy on port 8080:
docker run -d --name zap-proxy-lab -p 127.0.0.1:8080:8080 ghcr.io/zaproxy/zaproxy:stable zap.sh -daemon -host 0.0.0.0 -port 8080 -config api.disablekey=true -config api.addrs.addr.name=.* -config api.addrs.addr.regex=trueThe api.disablekey=true option removes the API key check so the commands below work without extra setup. Keep the port published on 127.0.0.1, and remove the container when you finish. Anyone who can reach the API can control ZAP.
Confirm the API answers:
curl -s http://127.0.0.1:8080/JSON/core/view/version/Output on my machine:
{"version":"2.17.0"}Send two requests through the proxy. The -x flag tells curl to use ZAP. The target name is host.docker.internal because ZAP runs inside Docker, and the name it uses for your machine is the one it resolves from there:
curl -s -x http://127.0.0.1:8080 -o /dev/null -w 'proxied root http=%{http_code}\n' http://host.docker.internal:3000/
curl -s -x http://127.0.0.1:8080 -o /dev/null -w 'proxied api http=%{http_code}\n' "http://host.docker.internal:3000/rest/products/search?q=apple"Output:
proxied root http=200
proxied api http=200Ask ZAP how many messages it has recorded:
curl -s "http://127.0.0.1:8080/JSON/core/view/numberOfMessages/"On my machine it returned {"numberOfMessages":"5"}. Then list them with a short Python one-liner that prints each request line and response status line:
curl -s "http://127.0.0.1:8080/JSON/core/view/messages/?start=1&count=5" | python -c "import json,sys; d=json.load(sys.stdin); [print(m['requestHeader'].splitlines()[0], '->', m['responseHeader'].splitlines()[0]) for m in d['messages']]"Output:
GET http://host.docker.internal:3000/ HTTP/1.1 -> HTTP/1.1 200 OK
GET http://host.docker.internal:3000 HTTP/1.1 -> HTTP/1.0 0
GET http://host.docker.internal:3000/rest/products/search?q=apple HTTP/1.1 -> HTTP/1.1 200 OK
GET http://host.docker.internal:3000/rest HTTP/1.1 -> HTTP/1.0 0
GET http://host.docker.internal:3000/rest/products HTTP/1.1 -> HTTP/1.0 0I sent two requests through the proxy, but the history holds five entries. Three of them have no response, which shows as status 0. I did not trace where they came from. Do not read them as evidence about Juice Shop. This is the reason to check the history yourself rather than trust a count. When your own list differs, write down what you see and ask why before you draw a conclusion.
Stop and remove the proxy when you finish:
docker rm -f zap-proxy-labPointing the scanner at a target you do not own. The -t flag accepts any URL, and the scanner will send requests to it. Keep the target on the lab alias for your own machine.
Publishing the lab on all interfaces. -p 3000:3000 without an address listens on every interface. Always write 127.0.0.1:3000:3000.
Treating warnings as a score. A report with three warnings tells you that three rules matched. Verify each one before you count it.
Skipping the version. Juice Shop changes between releases, and so do ZAP's rules. Write down both versions with the report. The version I used was Juice Shop 20.3.0 and ZAP 2.17.0.
Leaving the proxy running. The daemon has its API key disabled. Remove the container after each session.
Reading the status column as proof. A row with status 0 means no response was recorded. It is not a finding.
Mounting the wrong folder. If the report is missing, check the -v mount first. The scanner can run successfully and still write nothing where you expect.
Keep one notes file for the lab. Each line should hold one alert, the request that tested it, what you saw, and your verdict. After a few scans, the file becomes a record of what the tool found, what you confirmed, and what it got wrong.
Read the Juice Shop setup guide if you skipped it, since this article assumes the same lab. The next article uses the same lab to explain SQL injection. That is a better place to see why a scanner alert needs a manual check: the flaw is about how input reaches a query, and a tool can only hint at that.
The ZAP documentation covers contexts, authentication and active scanning. Those topics matter once you can tell a confirmed issue from a rule match. Learn them against the lab, with permission, and do not carry them to other systems.
The Juice Shop project keeps its challenge list and setup notes in its GitHub repository. Check its releases page for the current version before you pull the image.
Security tools
Nmap maps the open ports on your own machine. Learn connect and SYN scans, service detection and output files, then match each port to its process.