OWASP ZAP is an open-source web application security scanner.
By enabling ZAP in the integration test, the ZAP full scan GitHub action will run a ZAP full scan that attacks the web application to find additional vulnerabilities.
The alerts will be maintained as a GitHub issue in the corresponding repository as well as an artifact in the Integration Test workflow.
- The script can potentially run for a long period. The default value for
zap-cmd-options
prevents that by setting a max time of 60 minutes. Mind that when changing this parameter. - By deciding to run against an external target, since it simulates an attack, you should only use the full scan against targets that you have permission to test.
If there is no need for customization, the test can be enabled by setting the parameter zap-enabled
to true.
Then, after the integration tests, the ZAP full scan will run against the Charm unit IP address (port 80) and the logs can be viewed in the Job output, the artifact, or if vulnerabilities are found, in an issue entitled 'OWASP ZAP report'.
If future scans identify a fixed issue or new alerts the action will update the issue with the required information.
jobs:
integration-tests:
uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main
secrets: inherit
with:
zap-enabled: true
Since zap-target is not set, the unit IP address will be used as the target.
A ZAP_TARGET
environment variable is available with the zap_target
parameter value or if is not set, with the unit IP address.
jobs:
integration-tests:
uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main
secrets: inherit
with:
zap-enabled: true
zap-target: localhost
zap-target-port: 80
zap-before-command: "curl -H \"Host: indico.local\" $ZAP_TARGET/bootstrap --data-raw 'csrf_token=00000000-0000-0000-0000-000000000000&first_name=admin&last_name=admin&email=admin%40admin.com&username=admin&password=lunarlobster&confirm_password=lunarlobster&affiliation=Canonical'"
See more information about authorization headers in Authentication Env Vars.
jobs:
integration-tests:
uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main
secrets: inherit
with:
zap-enabled: true
zap-auth-header: Auth
zap-auth-value: SomeValue
It's possible to override or modify the behavior of the script components by using a Scan Hook. Within the hook, a script can be loaded to change the request and add a new header.
First, create a hook.py
file with the following content:
import logging
def zap_started(zap, target):
"""Actions when starts
Args:
zap (ZAPv2): ZAPv2 instance
target (string): Target being scanned
"""
logging.info(
zap.script.load(
"Add Header Script",
"httpsender",
"python : jython",
"/zap/wrk/tests/zap/add_header_request.py",
)
)
logging.info(zap.script.enable("Add Header Script"))
def zap_pre_shutdown(zap):
"""Actions before shutdown
Args:
zap (ZAPv2): ZAPv2 instance
Returns:
None
"""
logging.info("script.listEngines: %s", zap.script.list_engines)
logging.info("script.listScripts: %s", zap.script.list_scripts)
Note: The zap_pre_shutdown
was altered just to show the script list and confirm if it was loaded as expected.
Then, create a add_header_request.py
file with the following content:
def sendingRequest(msg, initiator, helper): # noqa: N802
"""sendingRequest is a name defined and expected by ZAP tool
Args:
msg (HttpMessage): all requests/responses sent/received by ZAP
initiator (int): the component that initiated the request
helper (HttpSender): returns the HttpSender instance used to send the request
Returns:
None
"""
msg.getRequestHeader().setHeader("Some-Header", "Some-Value")
def responseReceived(msg, initiator, helper): # noqa: N802
"""responseReceived is a name defined and expected by ZAP tool
Args:
msg (HttpMessage): all requests/responses sent/received by ZAP
initiator (int): the component that initiated the request
helper (HttpSender): returns the HttpSender instance used to send the request
Returns:
None
"""
This will add every header defined in headers
to every request made by ZAP.
The workflow should look like this:
jobs:
integration-tests:
uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main
secrets: inherit
with:
zap-enabled: true
zap-cmd-options: '-T 60 -z "-addoninstall jython" --hook "/zap/wrk/tests/zap/hook.py"'
- Install the
jython
addon. See addons for a complete list. - Set the hook parameter to the
hook.py
file inside the repository. - The
hook.py
should be updated accordingly to theadd_header_request.py
path in the repository as well.
The add_header_request.py
file should have the following content:
For more information, see this ZAP Proxy issue.
def sendingRequest(msg, initiator, helper): # noqa: N802
"""sendingRequest is a name defined and expected by ZAP tool
Args:
msg (HttpMessage): all requests/responses sent/received by ZAP
initiator (int): the component that initiated the request
helper (HttpSender): returns the HttpSender instance used to send the request
Returns:
None
"""
msg.setUserObject({"host": "indico.local"})
def responseReceived(msg, initiator, helper): # noqa: N802
"""responseReceived is a name defined and expected by ZAP tool
Args:
msg (HttpMessage): all requests/responses sent/received by ZAP
initiator (int): the component that initiated the request
helper (HttpSender): returns the HttpSender instance used to send the request
Returns:
None
"""
Same procedure as the previous item but with the rewrite_and_log_request.py
file.
import os
log_filename = r'/zap/wrk/requests.log'
target = os.getenv('ZAP_TARGET')
def sendingRequest(msg, initiator, helper): # noqa: N802
"""sendingRequest is a name defined and expected by ZAP tool
Args:
msg (HttpMessage): all requests/responses sent/received by ZAP
initiator (int): the component that initiated the request
helper (HttpSender): returns the HttpSender instance used to send the request
Returns:
None
"""
host = msg.getRequestHeader().getURI().getHost()
if "indico.local" in host:
uri = msg.getRequestHeader().getURI()
uri.setEscapedAuthority(target)
msg.getRequestHeader().setURI(uri);
with open(log_filename, 'a') as f:
f.write(msg.getRequestHeader().toString())
def responseReceived(msg, initiator, helper): # noqa: N802
"""responseReceived is a name defined and expected by ZAP tool
Args:
msg (HttpMessage): all requests/responses sent/received by ZAP
initiator (int): the component that initiated the request
helper (HttpSender): returns the HttpSender instance used to send the request
Returns:
None
"""