For a very long time, I’ve been interested in automated security testing. Alan Parkinson’s “Automated Security Testing” presentation at Selenium Conference 2012 really highlighted the possibilities, for me. Since then, I’ve wanted to get a simple, yet powerful and effective, automated security-scanning and reporting tool integrated into our development, testing, and release process (which I’ll also refer to as “pipeline.”)
Separately, I’ve also been very interested in exploring and learning Docker. Last quarter, I was happy to learn that there is a Dockerized OWASP ZAP container, but I didn’t then have the time set aside to learn both Docker and ZAP. By using Docker to containerize/Dockerize our OWASP-ZAP instance, we could get it running in our Jenkins continuous-integration environment, and essentially take the Docker image and run it in other (developers’, operations’, etc.) instances.
This quarter, the two are coming together, now that I’ve set aside some focused time and my personal deliverable to “Get a Dockererized OWASP ZAP (CLI) instance up and running against a staged instance of one of our key sites: either AMO, Mozilla.org, or MDN, in Web QA’s Jenkins instance, on either/both a cronjob or on-demand.”
When I was first pointed to https://github.com/zaproxy/zaproxy/wiki/Docker I saw a dizzying array of options: did I want the CLI? ZAPR? Headless? Headless, with Xvfb? GUI VNC?
Because our stack is primarily Python-based, and I’ll want to stand this up in a Jenkins instance, along with, hopefully, our WebDriver tests (which use pytest-selenium), I chose the ZAP CLI, which is a Python wrapper. It’ll also be lighter weight than the GUI VNC, but should integrate well with our setup.
Let’s start!
So, I downloaded and installed Docker for Mac OS X, from http://www.docker.com/
Next, I launched the Docker Quickstart Terminal
Then I did:
$ docker pull owasp/zap2docker-stable
This command pulls the image “zap2docker-stable” from Docker’s Hub (specifically, https://hub.docker.com/r/owasp/zap2docker-stable/)
The 1st thing I tried was just a sample command from the first set of docs, to see if it worked:
$ docker run -i owasp/zap2docker-stable zap-cli quick-scan --self-contained --start-options '-config api.disablekey=true' https://www.allizom.org
[INFO] Starting ZAP daemon
[INFO] Running a quick scan for https://www.allizom.org
[INFO] Issues found: 0
[INFO] Shutting down ZAP daemon
Voila! No issues found – it started, scanned, and shut down, all very quickly. Good.
Looking to go further with the scan, I glanced at https://github.com/Grunny/zap-cli just to get up and running, then tried substituting in ZAP’s active-scan
command for quick-scan
, which didn’t work (try it yourself, if you’d like) and after whittling away the given options one by one: --self-contained
, --start-options
, and -config
, I ended up with this, which I tried to run:
$ docker run -i owasp/zap2docker-stable zap-cli active-scan https://www.allizom.org
I got this:
IOError: [Errno socket error] [Errno 111] Connection refused
It was then that I was helpfully pointed back to the documentation (https://github.com/Grunny/zap-cli#getting-started-running-a-scan), which does point out that zap-cli *only* starts ZAP automatically when using the ‘quick-scan
’ option. So, I went back to https://github.com/zaproxy/zaproxy/wiki/Docker and re-read the headless section, where it shows you how to start ZAP first, in its own container, which, I did, like so:
$ docker run -p 8090:8090 -i owasp/zap2docker-stable zap.sh -daemon -port 8090 -host 0.0.0.0
That gave me the following output, which told me that ZAP was indeed running:
4370 [ZAP-daemon] INFO org.zaproxy.zap.DaemonBootstrap - ZAP is now listening on 0.0.0.0:8090
So, ZAP is running, but it’s running in a terminal/console window, and if we CTRL+C, we’ll lose the ZAP process. As a quick exercise, issue the docker run command above, then open a new Docker terminal window (Quickstart, if you’re on OS X), and do a docker ps. You should see something like so:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f6e9a636f8c2 owasp/zap2docker-stable "zap.sh -daemon -port" About a minute ago Up About a minute 0.0.0.0:8090->8090/tcp determined_pike
It’s rather obvious now, but in my initial excitement at having ZAP still running in the above container, I failed to understand how to correctly address/reach the already-running ZAP, and so when I ran this:
$ docker run owasp/zap2docker-stable zap-cli open-url https://www.allizom.org
It naturally gave me the same error I got earlier:
IOError: [Errno socket error] [Errno 111] Connection refused
So, we know ZAP is already running, but the zap-cli isn’t accessing it. What can we do? Docker’s docs on run specify that the command is for running an image, which we’ve already done. That’s the ‘owasp/zap2docker-stable’ in all of our above commands.
So, what we want, instead of running another new image or even a new container, is to run our zap-cli command *against* (i.e. to use) the already-running ZAP container, which is /from/ the owasp/zap2docker image.
For that, Docker’s doc says to use exec
, which “runs a new command in a running container.” Perfecto! I found this StackOverflow article on “exec vs. attach” helpful.
You can find and address Docker containers by either their name or their unique ID, both of which Docker will create for you. See Docker’s docs on run
, here, for more help. For our purposes here, it’s much easier to reference by name, rather than a really long ID like “0801f5872da47202beabb4aa122a2a5beeed6cf39759e312a0a67d225025b049”
To do that, it’s a matter of finding the container name, which you can do by:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f6e9a636f8c2 owasp/zap2docker-stable "zap.sh -daemon -port" About a minute ago Up About a minute 0.0.0.0:8090->8090/tcp determined_pike
Here, we can see that CONTAINER ID is f6e9a636f8c2 and NAMES gives us determined_pike
So we can be sure ZAP is accessible outside of the Docker container, we follow the advice and commands from https://github.com/zaproxy/zaproxy/wiki/Docker#accessing-the-api-from-outside-of-the-docker-container which tells us to pass in -host 0.0.0.0
For now, we also don’t have nor need an API key, so following the last section of https://github.com/Grunny/zap-cli#extra-start-options, we set -config.api-disablekey=true
$ docker run -u zap -p 8090:8090 -d owasp/zap2docker-stable zap.sh -daemon -port 8090 -host 0.0.0.0 -config api.disablekey=true
Here, we’re running as the “zap” user, rather than Docker’s default user, which is the root.
After issuing this command, you should see a long dynamically-generated container ID, like so:
Dfc68c6c9880a02d0242aece2afdbd083c0ac9bac99e4809a903aae458ef5436
Finally, we can reference the Docker ZAP container by either its name or its container ID, and here I’ll choose to use its Docker-generated name, which is determined_pike
$ docker exec determined_pike zap-cli open-url 'https://www.allizom.org'
[INFO] Accessing URL https://www.allizom.org
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ac68d9afcfad owasp/zap2docker-stable "zap.sh -daemon -port" 29 seconds ago Up 28 seconds 0.0.0.0:8090->8090/tcp determined_pike
$ docker exec determined_pike zap-cli active-scan 'https://www.allizom.org'
[INFO] Running an active scan...
$ docker logs determined_pike
Found Java version 1.7.0_91
Available memory: 2002 MB
Setting jvm heap size: -Xmx512m
275 [main] INFO org.zaproxy.zap.DaemonBootstrap - OWASP ZAP 2.4.3 started.
701 [main] INFO hsqldb.db.HSQLDB379AF3DEBD.ENGINE - dataFileCache open start
707 [main] INFO hsqldb.db.HSQLDB379AF3DEBD.ENGINE - dataFileCache open end
1108 [main] INFO org.parosproxy.paros.common.AbstractParam - Setting config api.disablekey = true was null
1111 [main] INFO org.parosproxy.paros.network.SSLConnector - Reading supported SSL/TLS protocols...
1112 [main] INFO org.parosproxy.paros.network.SSLConnector - Using a SSLEngine...
1360 [main] INFO org.parosproxy.paros.network.SSLConnector - Done reading supported SSL/TLS protocols: [SSLv2Hello, SSLv3, TLSv1, TLSv1.1, TLSv1.2]
1367 [main] INFO org.parosproxy.paros.extension.option.OptionsParamCertificate - Unsafe SSL renegotiation disabled.
1376 [ZAP-daemon] INFO org.zaproxy.zap.control.ExtensionFactory - Loading extensions
2332 [ZAP-daemon] INFO org.zaproxy.zap.control.ExtensionFactory - Extensions loaded
Snipped many lines, here…
141243 [Thread-9] INFO org.parosproxy.paros.core.scanner.HostProcess - start host https://www.allizom.org | TestPersistentXSSSpider strength MEDIUM threshold MEDIUM
141262 [Thread-9] INFO org.parosproxy.paros.core.scanner.HostProcess - completed host/plugin https://www.allizom.org | TestPersistentXSSSpider in 0.019s
141263 [Thread-9] INFO org.parosproxy.paros.core.scanner.HostProcess - start host https://www.allizom.org | TestPersistentXSSAttack strength MEDIUM threshold MEDIUM
141264 [Thread-9] INFO org.parosproxy.paros.core.scanner.HostProcess - completed host/plugin https://www.allizom.org | TestPersistentXSSAttack in 0.002s
141265 [Thread-9] INFO org.parosproxy.paros.core.scanner.HostProcess - start host https://www.allizom.org | ScriptsActiveScanner strength MEDIUM threshold MEDIUM
141266 [Thread-9] INFO org.parosproxy.paros.core.scanner.HostProcess - completed host/plugin https://www.allizom.org | ScriptsActiveScanner in 0.002s
141266 [Thread-9] INFO org.parosproxy.paros.core.scanner.HostProcess - completed host https://www.allizom.org in 1.184s
141267 [Thread-8] INFO org.parosproxy.paros.core.scanner.Scanner - scanner completed in 1.216s
While it’s complete, having *just* the log output doesn’t tell us actionable items – at least, not in an easy-to-read way.
So, that’s next on our list of many things to do! See the follow-up blog posts, coming within the next couple of weeks, where I aim to address this, and the rest of the issues I’ve raised, and will be raising in my GitHub repo: https://github.com/stephendonner/docker-zap
Until then!
Roman wrote on
Stephen Donner wrote on