Skip to content

Commit d5cfd57

Browse files
authored
Merge pull request #562 from secureCodeBox/feature/zap-advanced-alert-filters
Add Alert Filters Config to ZAP Advanced
2 parents bbd9415 + 86565a5 commit d5cfd57

File tree

4 files changed

+275
-0
lines changed

4 files changed

+275
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# tmp file for local testing
2+
3+
global:
4+
# Sets the ZAP Session name
5+
sessionName: nginx
6+
7+
# ZAP Contexts Configuration
8+
contexts:
9+
# Name to be used to refer to this context in other jobs, mandatory
10+
- name: nginx
11+
# The top level url, mandatory, everything under this will be included. IMPORTANT: must be the hostname without any subpath!
12+
url: http://localhost:8082/
13+
# An optional list of regexes to include
14+
includePaths:
15+
- "http://localhost:8082/.*"
16+
# An optional list of regexes to exclude
17+
excludePaths:
18+
- ".*\\.css"
19+
- ".*\\.png"
20+
- ".*\\.jpeg"
21+
alertFilters:
22+
- ruleId: 10106
23+
newLevel: "False Positive"
24+
matches:
25+
attack: ".*"
26+
attackIsRegex: true
27+
28+
evidence: ".*"
29+
evidenceIsRegex: true
30+
31+
parameter: ".*"
32+
parameterIsRegex: true
33+
34+
url: ".*"
35+
urlIsRegex: true
36+
37+
38+
# ZAP Spiders Configuration
39+
spiders:
40+
- name: nginx-spider
41+
# String: Name of the context to spider, default: first context
42+
context: nginx
43+
# String: Url to start spidering from, default: first context URL
44+
url: http://localhost:8082/
45+
46+
# ZAP ActiveScans Configuration
47+
scanners:
48+
- name: nginx-scan
49+
# String: Name of the context to attack, default: first context
50+
context: nginx
51+
# String: Url to start scanning from, default: first context URL
52+
url: http://localhost:8082/
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# SPDX-FileCopyrightText: 2021 iteratec GmbH
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
---
6+
# Global ZAP Configurations
7+
global:
8+
# True to create another ZAP session (overwrite the former if the same name already exists), False to use an existing on
9+
isNewSession: true
10+
# Sets the ZAP Session name
11+
sessionName: SCB
12+
13+
# List of 1 or more contexts, mandatory
14+
contexts:
15+
# Name to be used to refer to this context in other jobs, mandatory
16+
- name: scb-petstore-context
17+
# The top level url, mandatory, everything under this will be included. IMPORTANT: must be the hostname without any subpath!
18+
url: http://petstore:8080/
19+
# An optional list of regexes to include
20+
includePaths:
21+
- "http://petstore:8080/v2.*"
22+
# An optional list of regexes to exclude
23+
excludePaths:
24+
- ".*\\.css"
25+
- ".*\\.png"
26+
- ".*\\.jpeg"
27+
alertFilters:
28+
# ignore a bunch of rules to reduce number of findings in tests
29+
- ruleId: 10020
30+
newLevel: "False Positive"
31+
- ruleId: 10021
32+
newLevel: "False Positive"
33+
- ruleId: 10024
34+
newLevel: "False Positive"
35+
- ruleId: 10036
36+
newLevel: "False Positive"
37+
- ruleId: 10038
38+
newLevel: "False Positive"
39+
- ruleId: 10049
40+
newLevel: "False Positive"
41+
- ruleId: 10063
42+
newLevel: "False Positive"
43+
- ruleId: 10098
44+
newLevel: "False Positive"
45+
- ruleId: 10109
46+
newLevel: "False Positive"
47+
- ruleId: 40033
48+
newLevel: "False Positive"
49+
- ruleId: 40039
50+
newLevel: "False Positive"
51+
- ruleId: 40040
52+
newLevel: "False Positive"
53+
- ruleId: 90003
54+
newLevel: "False Positive"
55+
56+
apis:
57+
# -- The name of the spider configuration
58+
- name: scb-petstore-api
59+
# -- The Name of the context (zapConfiguration.contexts[x].name) to spider, default: first context available.
60+
context: scb-petstore-context
61+
# -- format of the API ('openapi', 'grapql', 'soap')
62+
format: openapi
63+
# -- Url to start spidering from, default: first context URL
64+
url: http://petstore:8080/v2/swagger.json
65+
# -- Override host setting in swagger.json
66+
hostOverride: http://petstore:8080
67+
68+
spiders:
69+
- name: scb-petstore-spider
70+
# String: Name of the context to spider, default: first context
71+
context: scb-petstore-context
72+
# String: Url to start spidering from, default: first context URL
73+
url: http://petstore:8080/v2/
74+
# Int: Fail if spider finds less than the specified number of URLs, default: 0
75+
failIfFoundUrlsLessThan: 0
76+
# Int: Warn if spider finds less than the specified number of URLs, default: 0
77+
warnIfFoundUrlsLessThan: 0
78+
# Int: The max time in minutes the spider will be allowed to run for, default: 0 unlimited
79+
maxDuration: 1
80+
# Int: The maximum tree depth to explore, default 5
81+
maxDepth: 5
82+
# Int: The maximum number of children to add to each node in the tree
83+
maxChildren: 10
84+
# # Int: The max size of a response that will be parsed, default: 2621440 - 2.5 Mb
85+
# maxParseSizeBytes: 2621440
86+
# Bool: Whether the spider will accept cookies, default: true
87+
acceptCookies: true
88+
# Bool: Whether the spider will handle OData responses, default: false
89+
handleODataParametersVisited: false
90+
# Enum [ignore_completely, ignore_value, use_all]: How query string parameters are used when checking if a URI has already been visited, default: use_all
91+
handleParameters: use_all
92+
# Bool: Whether the spider will parse HTML comments in order to find URLs, default: true
93+
parseComments: true
94+
# Bool: Whether the spider will parse Git metadata in order to find URLs, default: false
95+
parseGit: false
96+
# Bool: Whether the spider will parse 'robots.txt' files in order to find URLs, default: true
97+
parseRobotsTxt: false
98+
# Bool: Whether the spider will parse 'sitemap.xml' files in order to find URLs, default: true
99+
parseSitemapXml: false
100+
# Bool: Whether the spider will parse SVN metadata in order to find URLs, default: false
101+
parseSVNEntries: false
102+
# Bool: Whether the spider will submit POST forms, default: true
103+
postForm: true
104+
# Bool: Whether the spider will process forms, default: true
105+
processForm: true
106+
# Int: The time between the requests sent to a server in milliseconds, default: 200
107+
requestWaitTime: 200
108+
# Bool: Whether the spider will send the referer header, default: true
109+
sendRefererHeader: true
110+
# Int: The number of spider threads, default: 2
111+
threadCount: 5
112+
# String: The user agent to use in requests, default: '' - use the default ZAP one
113+
userAgent: "secureCodeBox / ZAP Spider"
114+
115+
scanners:
116+
- name: scb-petstore-scan
117+
# String: Name of the context to attack, default: first context
118+
context: scb-petstore-context
119+
# String: Url to start scaning from, default: first context URL
120+
url: http://petstore:8080/v2/
121+
# String: Name of the scan policy to be used, default: Default Policy
122+
policy: "API-Minimal"
123+
# Int: The max time in minutes any individual rule will be allowed to run for, default: 0 unlimited
124+
maxRuleDurationInMins: 1
125+
# Int: The max time in minutes the active scanner will be allowed to run for, default: 0 unlimited
126+
maxScanDurationInMins: 5
127+
# Int: The max number of threads per host, default: 2
128+
threadPerHost: 5
129+
# Int: The delay in milliseconds between each request, use to reduce the strain on the target, default 0
130+
delayInMs: 0
131+
# Bool: If set will add an extra query parameter to requests that do not have one, default: false
132+
addQueryParam: false
133+
# Bool: If set then automatically handle anti CSRF tokens, default: false
134+
handleAntiCSRFTokens: false
135+
# Bool: If set then the relevant rule Id will be injected into the X-ZAP-Scan-ID header of each request, default: false
136+
injectPluginIdInHeader: false
137+
# Bool: If set then the headers of requests that do not include any parameters will be scanned, default: false
138+
scanHeadersAllRequests: false

scanners/zap-advanced/scanner/tests/test_integration_docker_local.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,20 @@ def test_petstore_scan_with_config(get_petstore_url, get_zap_instance: ZAPv2):
190190
logging.info('Found ZAP Alerts: %s', str(len(alerts)))
191191

192192
assert int(len(alerts)) >= 1
193+
194+
@pytest.mark.integrationtest
195+
def test_petstore_scan_with_alert_filters(get_petstore_url, get_zap_instance: ZAPv2):
196+
197+
zap = get_zap_instance
198+
test_config_yaml = "./tests/mocks/scan-full-petstore-alert-filter-docker/"
199+
test_target = "http://petstore:8080/"
200+
201+
zap_automation = ZapAutomation(zap=zap, config_dir=test_config_yaml)
202+
zap_automation.scan_target(target=test_target)
203+
204+
alerts = zap_automation.get_zap_scanner.get_alerts(test_target, [], [])
205+
206+
logging.info('Found ZAP Alerts: %d', len(alerts))
207+
208+
# should normally be 13 alerts but most of them are ignored using alertFilters in the scan config
209+
assert int(len(alerts)) < 10

scanners/zap-advanced/scanner/zapclient/context/zap_context.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ def _configure_context(self, context: collections.OrderedDict):
9999
# TODO: Open a new ZAP GH Issue: Why (or) is this difference (context_id vs. context_name) here really necessary?
100100
self._configure_context_technologies(context["technologies"], context_name)
101101

102+
if self._is_not_empty("alertFilters", context):
103+
self._configure_alert_filters(context["alertFilters"], context_id)
104+
102105
def _configure_context_include(self, context: collections.OrderedDict):
103106
"""Protected method to configure the ZAP 'Context / Include Settings' based on a given ZAP config.
104107
@@ -277,3 +280,68 @@ def _configure_context_technologies(self, technology: collections.OrderedDict, c
277280
technologies = ", ".join(technology["included"])
278281
logging.debug("Exclude technologies '%s' in context with name %s", technologies, context_name)
279282
self.get_zap.context.exclude_context_technologies(contextname=context_name, technologynames=technologies)
283+
284+
def _get_or_none(self, dict: collections.OrderedDict, key: str):
285+
if dict == None or not isinstance(dict, collections.OrderedDict):
286+
return None
287+
288+
if key in dict:
289+
return dict[key]
290+
else:
291+
return None
292+
293+
def _get_or_none_stringified(self, dict: collections.OrderedDict, key: str):
294+
value = self._get_or_none(dict, key)
295+
296+
if value == None:
297+
return None
298+
else:
299+
return str(value)
300+
301+
def _get_level(self, level: str):
302+
# lowercase input to catch simple typos
303+
level = level.lower()
304+
if level == "false positive":
305+
return -1
306+
elif level == "info" or level == "informational":
307+
return 0
308+
elif level == "low":
309+
return 1
310+
elif level == "medium":
311+
return 2
312+
elif level == "high":
313+
return 3
314+
315+
logging.warn("AlertFilter configured with unknown level: '%s'. This rule will be ignored!", level)
316+
return None
317+
318+
def _configure_alert_filters(self, alert_filters: list[collections.OrderedDict], context_id: int):
319+
"""Protected method to configure the ZAP 'Context / Alert Filters' Settings based on a given ZAP config.
320+
321+
Parameters
322+
----------
323+
alert_filters : collections.OrderedDict
324+
The current alert filter configuration object containing the ZAP alert filter configuration (based on the class ZapConfiguration).
325+
context_id : int
326+
The zap context id to configure the ZAP alert filters for (based on the class ZapConfiguration).
327+
"""
328+
329+
if(alert_filters):
330+
for alert_filter in alert_filters:
331+
logging.debug("Adding AlertFilter for rule '%d' in context with id %s", alert_filter["ruleId"], context_id)
332+
333+
matches = alert_filter["matches"] if "matches" in alert_filter else collections.OrderedDict()
334+
self.get_zap.alertFilter.add_alert_filter(
335+
contextid = context_id,
336+
ruleid = str(alert_filter["ruleId"]),
337+
newlevel = str(self._get_level(alert_filter["newLevel"])),
338+
# optional matchers
339+
url = self._get_or_none(matches, "url"),
340+
urlisregex = self._get_or_none_stringified(matches, "urlIsRegex"),
341+
parameter = self._get_or_none(matches, "parameter"),
342+
parameterisregex = self._get_or_none_stringified(matches, "parameterIsRegex"),
343+
attack = self._get_or_none(matches, "attack"),
344+
attackisregex = self._get_or_none_stringified(matches, "attackIsRegex"),
345+
evidence = self._get_or_none(matches, "evidence"),
346+
evidenceisregex = self._get_or_none_stringified(matches, "evidenceIsRegex"),
347+
)

0 commit comments

Comments
 (0)