Aboard Jenkins with ZAP Vulnerability Scanner: Developing One-Click Active Scan Pipeline
Who doesn’t like a one-click problem solver? In web app security scan, it’s doable, but behind the scene, it could be all Python scripting and configuring Jenkins — our security scan orchestrator that is capable to sew it with other web app deployment activities. This sewing of jobs would result in a pipeline that triggers its jobs sequentially. Welcome to Continuous Integration and Continuous Delivery or CI/CD (drum rolls). Anyhow, this article is limited to security scan orchestration. In a perfect world, it would be integrated with web app deployment pipeline as a part of post-deployment’s non-functional testing.
System Requirements:
You’d probably be surprised that this article talks about Windows Server when most servers on Earth are on Linux. That’s exactly the attraction — Who wouldn’t want a change of scenery once in a while? See below for the basic system requirements.
OS: Windows Server 2012 R2
CI/CD Tool: Jenkins (a highly customizable de facto CI/CD tool)
Jenkins Plug-in: HTML Publisher (to publish HTML reports that the Jenkins’ build generates to the job and build pages)
Security Scanner Tool: OWASP ZAP (it’s reputable and free — that’s why)
Passive VS Active Scan
There are two (2) types of scan that could be automated: passive and active. Passive scan a.k.a. read-only scan is meant for quick insight to see the most fundamental security status of packet’s header. It is done by merely sending a normal (unfuzzed) request just to check the response packet’s security header — whether it has security compliance in check e.g., setting of HTTP Strict Transport Security (HSTS), Cross-Site Scripting (XSS) Protection, Content Security Policy, X-Content-Type Options, Cookie’s SameSite, Cookie’s secure flag, Cookie’s HttpOnly flag, etc. It is normally easy and quick to accomplish since only the packet’s header is being observed.
While active scan a.k.a. fuzzing is a proactive sending of fuzzed requests to a target server — in order to trigger “meaningful” responses or force the target to execute malicious code. A fuzzed request is an altered request filled with malicious or unexpected payload. Numerous fuzzing payloads could be created, therefore this would usually be complex and time-consuming. A professional vulnerability scanner such as ZAP would provide ready-to-use fuzzing feature that we could utilize and automate.
Command Line Automation of Active Scanning using ZAP
Firstly, Jenkins is command-based configuration pipeline, hence any software that is planned to be called by Jenkins must be callable via command line. Luckily, ZAP is.
Collaboration between ZAP’s Python API Script and ZAP’s native command line delivers reporting without the need of session file, because the reporting would be generated by the ZAP’s Python API Script. Below is the required procedure.
- Download ZAP’s python APIs from https://files.pythonhosted.org/packages/83/39/fd8d2ad9f2221a95a12a3ae0eeff1acb7f1c5b1802e48b380937940b650d/python-owasp-zap-v2.4-0.0.14.tar.gz
- Extract the archive and go to \python-owasp-zap-v2.4–0.0.14.tar\dist\python-owasp-zap-v2.4–0.0.14\, where ‘setup.py’ is located
- Open command prompt from the previous directory and build the script by typing: python setup.py build
- Install the script by typing: python setup.py install
- Write a python script that calls necessary ZAP’s APIs as presented below. For example name it ‘webapp-scan.py’. Below script is for unauthenticated scan test case:
#!/usr/bin/env pythonimport time
import sys
reload(sys)
from pprint import pprint
from zapv2 import ZAPv2
sys.setdefaultencoding('utf-8')target = 'https://webappurl.com' # Change this to the URL of the web app targetapi = "123456" # Change to match the API key set in ZAP, or use None if the API key is disabled# By default ZAP API client will connect to port 8080
zap = ZAPv2(apikey=api)# Use the line below if ZAP is not listening on port 8080, for example, if listening on port 9090zap = ZAPv2(apikey=api, proxies={'http': 'http://127.0.0.1:9090', 'https': 'http://127.0.0.1:9090'})# Establish connection
print 'Accessing target %s' % target# Establish a unique session
zap.urlopen(target)# Give the sites tree time to be updated
time.sleep(2)print 'Spidering target %s' % targetscanid = zap.spider.scan(target)# Give the Spider time to initiate
time.sleep(2)while (int(zap.spider.status(scanid)) < 100):print 'Spider progress %: ' + zap.spider.status(scanid)time.sleep(2)print 'Spider completed'# Give the passive scanner time to complete
time.sleep(5)# Start active scan
print 'Scanning target %s' % target
scanid = zap.ascan.scan(target)
while (int(zap.ascan.status(scanid)) < 100):
print 'Scan progress %: ' + zap.ascan.status(scanid)time.sleep(5)print 'Scan completed'# Report the result
# option 'w' to ovewrite, 'a' to append
#with open('baselinereport.txt', 'w') as f:
# print >> f, 'Hosts: ', zap.core.hosts
# print >> f, '\n'
# print >> f, '\n'
# print >> f, 'Alerts: ', zap.core.alerts()
# print >> f, '\n'
# print >> f, '\n'
print 'Hosts: ' + ', '.join(zap.core.hosts)
print 'Sites: ' + ', '.join(zap.core.sites)
print 'URLs: ' + ', '.join(zap.core.urls)
print 'Alerts: '
print (zap.core.alerts())# Write the XML and HTML reports that are exported to the workspace. In most cases, XML reporting can be omitted, hence the respective lines are commented#f1 = open('C:\\Users\\Administrator\\Downloads\\ZAP Automation Script\\xmlreport.xml','w')f2 = open('C:\\Users\\Administrator\\Downloads\\ZAP Automation Script\\htmlreport.html','w')#f1.write(zap.core.xmlreport(apikey = None))f2.write(zap.core.htmlreport(apikey = api))#f1.close()f2.close()
6. In case an authenticated scan is needed, then the following script applies:
#!/usr/bin/env python
import time
import sys
reload(sys)
from pprint import pprint
from zapv2 import ZAPv2
sys.setdefaultencoding('utf-8')context = 'new_active_scan'
authmethodname = 'formBasedAuthentication'
authmethodconfigparams = "".join('loginUrl=https://webappurl.com/portal/login' '&loginRequestData=username%3D%7B%25username%25%7D%26' 'password%3D%7B%25password%25%7D')target = 'https://webappurl.com' # Change this to the URL of the web app targetapi = "123456" # Change to match the API key set in ZAP, or use None if the API key is disabled# By default ZAP API client will connect to port 8080# Use the line below if ZAP is not listening on port 8080, for example, if listening on port 9090zap = ZAPv2(apikey=api, proxies={'http': 'http://127.0.0.1:9090', 'https': 'http://127.0.0.1:9090'})contextid = zap.context.new_context(context)print contextidprint zap.context.include_in_context(context, 'https://webappurl.com.*')print zap.context.context(context)print zap.authentication.set_authentication_method(contextid, authmethodname, authmethodconfigparams)print zap.authentication.set_logged_in_indicator(contextid, loggedinindicatorregex='Sign Out')print zap.authentication.set_logged_out_indicator(contextid, 'Please check your credentials and try again.')userid = zap.users.new_user(contextid, 'User 1')print useridprint zap.users.set_authentication_credentials(contextid, userid, 'username=userX&password=abc123')print zap.users.set_user_enabled(contextid, userid, True)# Establish connection
print 'Accessing target %s' % target# Establish a unique session
zap.urlopen(target)# Give the sites tree time to get updated
time.sleep(2)print 'Spidering target %s' % targetscanid = zap.spider.scan_as_user(contextid, userid, target)# Give the Spider time to initiate
time.sleep(2)while (int(zap.spider.status(scanid)) < 100):print 'Spider progress %: ' + zap.spider.status(scanid)time.sleep(2)print 'Spider completed'# Give the passive scanner time to complete
time.sleep(5)# Start active scan
print 'Scanning target %s' % target
scanid = zap.ascan.scan(target)
while (int(zap.ascan.status(scanid)) < 100):
print 'Scan progress %: ' + zap.ascan.status(scanid)time.sleep(5)print 'Scan completed'# Report the results
# option 'w' to ovewrite, 'a' to append
#with open('baselinereport.txt', 'w') as f:
# print >> f, 'Hosts: ', zap.core.hosts
# print >> f, '\n'
# print >> f, '\n'
# print >> f, 'Alerts: ', zap.core.alerts()
# print >> f, '\n'
# print >> f, '\n'
print 'Hosts: ' + ', '.join(zap.core.hosts)
print 'Sites: ' + ', '.join(zap.core.sites)
print 'URLs: ' + ', '.join(zap.core.urls)
print 'Alerts: '
print (zap.core.alerts())#Write the XML and HTML reports that are exported to the workspace. In most cases, XML reporting can be omitted, hence the respective lines are commented#f1 = open('C:\\Users\\Administrator\\Downloads\\ZAP Automation Script\\xmlreport.xml','w')f2 = open('C:\\Users\\Administrator\\Downloads\\ZAP Automation Script\\htmlreport.html','w')#f1.write(zap.core.xmlreport(apikey = None))f2.write(zap.core.htmlreport(apikey = api))#f1.close()f2.close()
7. Open command line tool and switch directory to the location of ZAP installation e.g., C:\Program Files\OWASP\Zed Attack Proxy
8. Run the ZAP’s batch script along with the ZAP’s host address, ZAP’s port number, and location of the creation of the new session file options e.g., zap.bat -daemon -host 127.0.0.1 -port 9090 -config api.disablekey=true
Note: Additional option to create new session file may be added if it is decided to be necessary, by appending -newsession C:\Users\Administrator\Downloads\. However, creating new session is not recommended if repeated scan will be conducted, because the previously created session may prevent the generation of new report, especially the one in HTML format.
Usage of api key is highly recommended for fluency of scan. Option to enable api key: -config api.key=”12345".
9. Run the previously written python script by typing: python webapp-scan.py
10. Capture the generated vulnerability report.
P.S. Every new scan must be preceded by initiating/restarting the ZAP (zap.bat -daemon -host 127.0.0.1 -port 9090 -config api.key=”12345"). Otherwise, error in generating report may occur (script syntax somehow is detected as erroneous).
Integration of Command Line based ZAP Active Scan into Jenkins
Since now our ZAP’s active scan is callable via command line, it can now be integrated into Jenkins for better management and pipelining with other project activities. The integration is comprised of multiple settings that are described below.
- Install Jenkins on the Windows Server 2012 R2 by installing the following pre-requisite packages and Jenkins itself:
- Open cmd or PowerShell with administrative privilege
- Type below script if the Windows Server 2012 version is less than R2:
if($(Get-ExecutionPolicy) -ne “RemoteSigned”)
{ Set-ExecutionPolicy RemoteSigned -Confirm:$false }
- Type this command to install Chocolatey package manager:
iwr https://chocolatey.org/install.ps1 -UseBasicParsing | iex
- Type the following command to install JDK8:
choco install jdk8 -y
- Type below command to install Jenkins via Chocolatey:
choco install jenkins -y - From Jenkins homepage/dashboard, click ‘Manage Jenkins’, then click ‘Configure System’. Below are the items to be set:
- At ‘Global properties’ section, tick ‘Environment variables’ and click ‘Add’ to enter the following key-value variables:
a. Name: PYTHON_PATH, Value: C:\Python27;C:\Python27\python.exe;C:\Python27\lib;C:\Python27\DLLs;C:\Python27\Lib\site-packages;
b. Name: USERPROFILE, Value: C:\Users\Administrator
c. Name: ZAPROXY_HOME, Value: C:\Program%20Files\OWASP\Zed%20Attack%20Proxy
- Save the system configuration - From Jenkins homepage/dashboard, create ‘New Item’ to represent a new project that automates the initiation of ZAP server in daemon mode (headless). The Jenkins project type for this is Freestyle. Name this job ‘Initiate ZAP Server’.
- Here are the things to be set on the ‘Configure’ page of the project:
- At the ‘General’ section, click ‘Advanced’ and tick ‘Use custom workspace’, then fill up the ‘Directory’ field with the path of ZAP installation folder e.g., C:\Program Files\OWASP\Zed Attack Proxy
- At the ‘Build’ section, click ‘Add build step’, then choose ‘Execute Windows batch command’. In the ‘Command’ field, type the necessary ZAP’s batch file command: zap.bat -daemon -host 127.0.0.1 -port 9090
Note: Modify the content of ‘zap.bat’ file, where the line that says “%ZAPROXY_HOME%\zap-2.7.0.jar” is changed into “zap-2.7.0.jar” (The ZAPROXY_HOME must be erased, otherwise Jenkins can’t comprehend the supplied path). Option to enable api key: -config api.key=”12345". Usage of api key is highly recommended for fluency of scan
- Save the configuration page - From Jenkins homepage/dashboard, create ‘New Item’ to represent a new project that automates the trigger of ZAP active scan. The Jenkins project type for this is Freestyle. Name this job ‘Start ZAP Active Scan’
- The things that need to be set on this project’s ‘Configure’ page are as follows:
- At the ‘General’ section, click ‘Advanced’ and tick ‘Use custom workspace’, then fill up the ‘Directory’ field with the location where ZAP’s python script for active scan resides e.g., C:\Users\Administrator\ZAP Automation Script
- Still under the same ‘Advanced’ section, tick ‘Quiet period’ and enter ‘60’ (seconds) in its input field. This is to anticipate in case both jobs (Initiate ZAP Server & Start ZAP Active Scan) are to be executed at the same time, therefore the latter job will wait for 60 seconds till the ZAP server is initiated before it starts the active scan job
- At the ‘Build’ section, click ‘Add build step’, then choose ‘Execute Windows batch command’. In the ‘Command’ field, type the necessary python command to launch the ZAP active scan e.g., python “C:\Users\Administrator\ZAP Automation Script\webapp-scan.py”
Note: The full path to the python script must be enclosed in double-quotes, so that Jenkins is able to comprehend the supplied path. Or even better if the directory name does not contain spaces, thus double-quotes are not needed.
- At the ‘Post-build Actions’, click ‘Add post-build action’, and select ‘Archive the artefacts’, then inside the ‘Files to archive’ input field, type “*.html,*.xml”
- At the ‘Post-build Actions’, click ‘Add post-build action’, and select ‘Publish HTML reports’, then inside the ‘HTML directory to archive’ input field, type the location of the report generation i.e., “C:\Users\Administrator\Downloads\ZAPAutomationScript”. Inside the ‘Index page[s]’ input field, type the name of the generated report i.e., ‘htmlreport’. Inside the ‘Report title’ input field, type the title of the generated report i.e., ‘Web App Active Scan HTML Report by ZAP (Python)’
- Save the configuration page
P.S. Every new scan must be preceded by initiation/restart of ZAP server (preferably via its Jenkins project that has been created in step №3 named ‘Initiate ZAP Server’). Otherwise, error in generating report may occur (script syntax somehow is detected as erroneous).
Parameterization of Jenkins-based ZAP Active Scan
The previous integration of ZAP active scan can be parameterized, so that the python script for active scan does not have to be modified in case different URLs are to be scanned. Below is the procedure to accomplish parameterization.
- From Jenkins homepage/dashboard, click the name of the project for ZAP active scan automation (Start ZAP Active Scan).
- On the project’s homepage, click ‘Configure’ and set the following items:
- At ‘General’ section, tick ‘This project is parameterized’ and click ‘Add Parameter’ to add the below parameters one by one:
a. Name: ENV, Default Value: https://webappurl.com, Description: URL choice of environment
b. Name: LOGINURL, Default Value: https://webappurl.com/portal/login, Description: Web app login URL
c. Name: CONTEXT, Default Value: https://webappurl.com.*, Description: Scope of web app’s sub-URLs
- At the ‘Build’ section, click ‘Add build step’, then choose ‘Execute Windows batch command’ . In the ‘Command’ field, type the necessary python command to launch the ZAP active scan with the needed parameters e.g., python C:\Users\Administrator\Downloads\ZAPAutomationScript\webapp-scan-parameterized.py %ENV% %LOGINURL% %CONTEXT%
Note: Under Windows OS, the parameters are enclosed within % character, while under UNIX/LINUX OS, the parameters are enclosed within ${} e.g., ${ENV}. - Modify the python script for ZAP active scan automation (webapp-scan.py), such that it accepts externally-supplied parameters. The respective modification is presented in below script.
#!/usr/bin/env python
import time
import sys
reload(sys)
from pprint import pprint
from zapv2 import ZAPv2
sys.setdefaultencoding('utf-8')context = 'new_active_scan'
authmethodname = 'formBasedAuthentication'
target = sys.argv[1]
loginpath = sys.argv[2]
includecontext = sys.argv[3]authmethodconfigparams = "".join('loginUrl=.join(loginpath)' '&loginRequestData=username%3D%7B%25username%25%7D%26' 'password%3D%7B%25password%25%7D')api = "123456" # Change to match the API key set in ZAP, or use None if the API key is disabled# By default ZAP API client will connect to port 8080
# Use the line below if ZAP is not listening on port 8080, for example, if listening on port 9090zap = ZAPv2(apikey=api, proxies={'http': 'http://127.0.0.1:9090', 'https': 'http://127.0.0.1:9090'})
contextid = zap.context.new_context(context)
print contextid
print zap.context.include_in_context(context, includecontext)print zap.context.context(context)print zap.authentication.set_authentication_method(contextid, authmethodname, authmethodconfigparams)
print zap.authentication.set_logged_in_indicator(contextid, loggedinindicatorregex='Sign Out')
print zap.authentication.set_logged_out_indicator(contextid, 'Please check your credentials and try again.')userid = zap.users.new_user(contextid, 'User 1')
print userid
print zap.users.set_authentication_credentials(contextid, userid, 'username=userX&password=abc123')
print zap.users.set_user_enabled(contextid, userid, True)# Establish connection
print 'Accessing target %s' % target# Establish a unique session
zap.urlopen(target)# Give the sites tree a chance to get updated
time.sleep(2)print 'Spidering target %s' % target
scanid = zap.spider.scan_as_user(contextid, userid, target)# Give the Spider time to initiate
time.sleep(2)
while (int(zap.spider.status(scanid)) < 100):
print 'Spider progress %: ' + zap.spider.status(scanid)
time.sleep(2)print 'Spider completed'# Give the passive scanner time to complete
time.sleep(5)# Start active scan
print 'Scanning target %s' % target
scanid = zap.ascan.scan(target)
while (int(zap.ascan.status(scanid)) < 100):
print 'Scan progress %: ' + zap.ascan.status(scanid)
time.sleep(5)print 'Scan completed'# Report the result
# option 'w' to ovewrite, 'a' to append
#with open('baselinereport.txt', 'w') as f:
# print >> f, 'Hosts: ', zap.core.hosts
# print >> f, '\n'
# print >> f, '\n'
# print >> f, 'Alerts: ', zap.core.alerts()
# print >> f, '\n'
# print >> f, '\n'
print 'Hosts: ' + ', '.join(zap.core.hosts)
print 'Sites: ' + ', '.join(zap.core.sites)
print 'URLs: ' + ', '.join(zap.core.urls)
print 'Alerts: '
print (zap.core.alerts())#Write the XML and HTML reports that are exported to the workspace. In most cases, XML reporting can be omitted, hence the respective lines are commented#f1 = open('C:\\Users\\Administrator\\Downloads\\ZAP Automation Script\\xmlreport.xml','w')f2 = open('C:\\Users\\Administrator\\Downloads\\ZAP Automation Script\\htmlreport.html','w')#f1.write(zap.core.xmlreport(apikey = None))f2.write(zap.core.htmlreport(apikey = api))#f1.close()f2.close()
4. On the project’s homepage, parameterized build can be executed by clicking ‘Build with Parameters’, and then modify the parameters’ values according to the environment/URL name to be scanned.
Give Permission to Jenkins to Run as Administrator
There is an anomaly when Jenkins is not explicitly given permission to run as Administrator on Windows Server 2012 R2; There is significant difference of scanning output when starting ZAP server from Jenkins and from CMD. In case of starting ZAP server from CMD, then followed by running ZAP’s active scan script from Jenkins, the scanning output will complete until the report creation phase. In contrast, when starting ZAP server from Jenkins, and afterwards running ZAP’s active scan script via Jenkins, the scanning output will not complete. This issue is solved by explicitly giving Jenkins permission to run as Administrator. Below are the steps to accomplish such setting.
- Right click on the Windows icon and click ‘Run’. Then type ‘services.msc’.
- The ‘Services’ window will pop up, then find Jenkins service among the list.
- Right click on Jenkins service and click ‘Properties’.
- Click on ‘Log On’ tab menu, then click on ‘This account’ radio button to type ‘administrator’ in the provided input field along with its password in the input fields below it.
- Click ‘OK’ to confirm the permission setting.
Ultimate Procedure to Execute ZAP Active Scan via Jenkins
Till now we have created jobs on Jenkins related to ZAP active scan activities. To clear the air about how we eventually will execute them. Refer to below steps.
- Run the Jenkins job that represents the starting of ZAP server, in this case it is named ‘Initiate ZAP Server’ by clicking on ‘Build’ on the left menu of this job.
2. Afterwards, run the Jenkins job that represents the starting of ZAP active scan by clicking on ‘Build with Parameters’ on the left menu of this job.
3. Fill up the parameters that represent the target host’s identification, then click on ‘Build’ button.
4. Wait till the active scan finishes and the report is generated.
One-Click Active Scan Jenkins Pipeline
Without a pipeline, the Jenkins-based active scan requires three (3) steps as explained in the previous section. While with a pipeline, they all can be compiled sequentially into one. Thus the name; one-click active scan.
Below are the steps involved to put the already-established jobs into a pipeline:
- Go to Jenkins homepage, then click ‘New Item’ on the left panel to create a new Pipeline, by selecting ‘Pipeline’ job type on the new job creation page.
- On the home page of the newly created Pipeline job, click ‘Configure’ and edit the settings as follows for a pipeline that we name ‘ZAP Authenticated Active Scan’:
- On Pipeline menu tab, from the ‘Definition’ drop-down menu, select ‘Pipeline script’, then type the following script into the field:
pipeline {
agent any
stages {
stage ('Initiate ZAP Server') {
steps {
build job: 'InitiateZAPServer', propagate: false, wait: false
}
}
stage ('Start ZAP Authenticated Active Scan') {
steps {
build job: 'StartZAPAuthenticatedActiveScan', parameters: [string(name: 'ENV', value: 'https://webappurl.com'), string(name: 'LOGINURL', value: 'https://webappurl.com/portal/login'), string(name: 'CONTEXT', value: 'https://webappurl.com.*')], propagate: false, quietPeriod: 40
}
}
}
}
3. Repeat steps 1–2 above, this time for ‘ZAP Unauthenticated Active Scan’ with the following script:
pipeline {
agent any
stages {
stage ('Initiate ZAP Server') {
steps {
build job: 'InitiateZAPServer', propagate: false, wait: false
}
}
stage ('Start ZAP Unauthenticated Active Scan') {
steps {
build job: 'StartZAPUnauthenticatedActiveScan', parameters: [string(name: 'ENV', value: 'https://webappurl.com')], propagate: false, quietPeriod: 40
}
}
}
}
4. And now, we have one-click active scan launcher jobs for ZAP. Click any of it and go make a coffee, then come back for the report viewing. Easy-peasy.
Note: There could be glitches e.g., the ZAP server is late to get ready for the active scan. This issue could be solved by either:
- Increase the Quiet Period of the active scan job so that it would not overlap the ZAP server initiation
- Restart the pipeline so that the active scan job could be re-sent by Jenkins when the ZAP server is ready
Final Words
Any software callable via command line could be integrated into Jenkins and that brings up the idea that other command-available vulnerability scanners could be used e.g., Burp Suite.
Even better if this one-click security scan job is combined/linked with the web app deployment pipeline as a post-deployment activity. However, this may not be preferred if regression test and/or testing new features would be done; In case, the regression test or new feature test fails, this may not give “legal” green light for security test to proceed.