diff --git a/scanners/zap-advanced/examples/nginx/0-foo.yaml b/scanners/zap-advanced/examples/nginx/0-foo.yaml new file mode 100644 index 0000000000..821b83b3ad --- /dev/null +++ b/scanners/zap-advanced/examples/nginx/0-foo.yaml @@ -0,0 +1,52 @@ +# tmp file for local testing + +global: + # Sets the ZAP Session name + sessionName: nginx + +# ZAP Contexts Configuration +contexts: + # Name to be used to refer to this context in other jobs, mandatory + - name: nginx + # The top level url, mandatory, everything under this will be included. IMPORTANT: must be the hostname without any subpath! + url: http://localhost:8082/ + # An optional list of regexes to include + includePaths: + - "http://localhost:8082/.*" + # An optional list of regexes to exclude + excludePaths: + - ".*\\.css" + - ".*\\.png" + - ".*\\.jpeg" + alertFilters: + - ruleId: 10106 + newLevel: "False Positive" + matches: + attack: ".*" + attackIsRegex: true + + evidence: ".*" + evidenceIsRegex: true + + parameter: ".*" + parameterIsRegex: true + + url: ".*" + urlIsRegex: true + + +# ZAP Spiders Configuration +spiders: + - name: nginx-spider + # String: Name of the context to spider, default: first context + context: nginx + # String: Url to start spidering from, default: first context URL + url: http://localhost:8082/ + +# ZAP ActiveScans Configuration +scanners: + - name: nginx-scan + # String: Name of the context to attack, default: first context + context: nginx + # String: Url to start scanning from, default: first context URL + url: http://localhost:8082/ diff --git a/scanners/zap-advanced/scanner/tests/mocks/scan-full-petstore-alert-filter-docker/1_zap-advanced-scan-config.yaml b/scanners/zap-advanced/scanner/tests/mocks/scan-full-petstore-alert-filter-docker/1_zap-advanced-scan-config.yaml new file mode 100644 index 0000000000..79e2e405f5 --- /dev/null +++ b/scanners/zap-advanced/scanner/tests/mocks/scan-full-petstore-alert-filter-docker/1_zap-advanced-scan-config.yaml @@ -0,0 +1,138 @@ +# SPDX-FileCopyrightText: 2021 iteratec GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +--- +# Global ZAP Configurations +global: + # True to create another ZAP session (overwrite the former if the same name already exists), False to use an existing on + isNewSession: true + # Sets the ZAP Session name + sessionName: SCB + +# List of 1 or more contexts, mandatory +contexts: + # Name to be used to refer to this context in other jobs, mandatory + - name: scb-petstore-context + # The top level url, mandatory, everything under this will be included. IMPORTANT: must be the hostname without any subpath! + url: http://petstore:8080/ + # An optional list of regexes to include + includePaths: + - "http://petstore:8080/v2.*" + # An optional list of regexes to exclude + excludePaths: + - ".*\\.css" + - ".*\\.png" + - ".*\\.jpeg" + alertFilters: + # ignore a bunch of rules to reduce number of findings in tests + - ruleId: 10020 + newLevel: "False Positive" + - ruleId: 10021 + newLevel: "False Positive" + - ruleId: 10024 + newLevel: "False Positive" + - ruleId: 10036 + newLevel: "False Positive" + - ruleId: 10038 + newLevel: "False Positive" + - ruleId: 10049 + newLevel: "False Positive" + - ruleId: 10063 + newLevel: "False Positive" + - ruleId: 10098 + newLevel: "False Positive" + - ruleId: 10109 + newLevel: "False Positive" + - ruleId: 40033 + newLevel: "False Positive" + - ruleId: 40039 + newLevel: "False Positive" + - ruleId: 40040 + newLevel: "False Positive" + - ruleId: 90003 + newLevel: "False Positive" + +apis: + # -- The name of the spider configuration + - name: scb-petstore-api + # -- The Name of the context (zapConfiguration.contexts[x].name) to spider, default: first context available. + context: scb-petstore-context + # -- format of the API ('openapi', 'grapql', 'soap') + format: openapi + # -- Url to start spidering from, default: first context URL + url: http://petstore:8080/v2/swagger.json + # -- Override host setting in swagger.json + hostOverride: http://petstore:8080 + +spiders: + - name: scb-petstore-spider + # String: Name of the context to spider, default: first context + context: scb-petstore-context + # String: Url to start spidering from, default: first context URL + url: http://petstore:8080/v2/ + # Int: Fail if spider finds less than the specified number of URLs, default: 0 + failIfFoundUrlsLessThan: 0 + # Int: Warn if spider finds less than the specified number of URLs, default: 0 + warnIfFoundUrlsLessThan: 0 + # Int: The max time in minutes the spider will be allowed to run for, default: 0 unlimited + maxDuration: 1 + # Int: The maximum tree depth to explore, default 5 + maxDepth: 5 + # Int: The maximum number of children to add to each node in the tree + maxChildren: 10 + # # Int: The max size of a response that will be parsed, default: 2621440 - 2.5 Mb + # maxParseSizeBytes: 2621440 + # Bool: Whether the spider will accept cookies, default: true + acceptCookies: true + # Bool: Whether the spider will handle OData responses, default: false + handleODataParametersVisited: false + # 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 + handleParameters: use_all + # Bool: Whether the spider will parse HTML comments in order to find URLs, default: true + parseComments: true + # Bool: Whether the spider will parse Git metadata in order to find URLs, default: false + parseGit: false + # Bool: Whether the spider will parse 'robots.txt' files in order to find URLs, default: true + parseRobotsTxt: false + # Bool: Whether the spider will parse 'sitemap.xml' files in order to find URLs, default: true + parseSitemapXml: false + # Bool: Whether the spider will parse SVN metadata in order to find URLs, default: false + parseSVNEntries: false + # Bool: Whether the spider will submit POST forms, default: true + postForm: true + # Bool: Whether the spider will process forms, default: true + processForm: true + # Int: The time between the requests sent to a server in milliseconds, default: 200 + requestWaitTime: 200 + # Bool: Whether the spider will send the referer header, default: true + sendRefererHeader: true + # Int: The number of spider threads, default: 2 + threadCount: 5 + # String: The user agent to use in requests, default: '' - use the default ZAP one + userAgent: "secureCodeBox / ZAP Spider" + +scanners: + - name: scb-petstore-scan + # String: Name of the context to attack, default: first context + context: scb-petstore-context + # String: Url to start scaning from, default: first context URL + url: http://petstore:8080/v2/ + # String: Name of the scan policy to be used, default: Default Policy + policy: "API-Minimal" + # Int: The max time in minutes any individual rule will be allowed to run for, default: 0 unlimited + maxRuleDurationInMins: 1 + # Int: The max time in minutes the active scanner will be allowed to run for, default: 0 unlimited + maxScanDurationInMins: 5 + # Int: The max number of threads per host, default: 2 + threadPerHost: 5 + # Int: The delay in milliseconds between each request, use to reduce the strain on the target, default 0 + delayInMs: 0 + # Bool: If set will add an extra query parameter to requests that do not have one, default: false + addQueryParam: false + # Bool: If set then automatically handle anti CSRF tokens, default: false + handleAntiCSRFTokens: false + # Bool: If set then the relevant rule Id will be injected into the X-ZAP-Scan-ID header of each request, default: false + injectPluginIdInHeader: false + # Bool: If set then the headers of requests that do not include any parameters will be scanned, default: false + scanHeadersAllRequests: false diff --git a/scanners/zap-advanced/scanner/tests/test_integration_docker_local.py b/scanners/zap-advanced/scanner/tests/test_integration_docker_local.py index 03dc5ab5d5..95af9d30e5 100644 --- a/scanners/zap-advanced/scanner/tests/test_integration_docker_local.py +++ b/scanners/zap-advanced/scanner/tests/test_integration_docker_local.py @@ -190,3 +190,20 @@ def test_petstore_scan_with_config(get_petstore_url, get_zap_instance: ZAPv2): logging.info('Found ZAP Alerts: %s', str(len(alerts))) assert int(len(alerts)) >= 1 + +@pytest.mark.integrationtest +def test_petstore_scan_with_alert_filters(get_petstore_url, get_zap_instance: ZAPv2): + + zap = get_zap_instance + test_config_yaml = "./tests/mocks/scan-full-petstore-alert-filter-docker/" + test_target = "http://petstore:8080/" + + zap_automation = ZapAutomation(zap=zap, config_dir=test_config_yaml) + zap_automation.scan_target(target=test_target) + + alerts = zap_automation.get_zap_scanner.get_alerts(test_target, [], []) + + logging.info('Found ZAP Alerts: %d', len(alerts)) + + # should normally be 13 alerts but most of them are ignored using alertFilters in the scan config + assert int(len(alerts)) < 10 diff --git a/scanners/zap-advanced/scanner/zapclient/context/zap_context.py b/scanners/zap-advanced/scanner/zapclient/context/zap_context.py index 934333c9ce..66015f5bd6 100644 --- a/scanners/zap-advanced/scanner/zapclient/context/zap_context.py +++ b/scanners/zap-advanced/scanner/zapclient/context/zap_context.py @@ -99,6 +99,9 @@ def _configure_context(self, context: collections.OrderedDict): # TODO: Open a new ZAP GH Issue: Why (or) is this difference (context_id vs. context_name) here really necessary? self._configure_context_technologies(context["technologies"], context_name) + if self._is_not_empty("alertFilters", context): + self._configure_alert_filters(context["alertFilters"], context_id) + def _configure_context_include(self, context: collections.OrderedDict): """Protected method to configure the ZAP 'Context / Include Settings' based on a given ZAP config. @@ -277,3 +280,68 @@ def _configure_context_technologies(self, technology: collections.OrderedDict, c technologies = ", ".join(technology["included"]) logging.debug("Exclude technologies '%s' in context with name %s", technologies, context_name) self.get_zap.context.exclude_context_technologies(contextname=context_name, technologynames=technologies) + + def _get_or_none(self, dict: collections.OrderedDict, key: str): + if dict == None or not isinstance(dict, collections.OrderedDict): + return None + + if key in dict: + return dict[key] + else: + return None + + def _get_or_none_stringified(self, dict: collections.OrderedDict, key: str): + value = self._get_or_none(dict, key) + + if value == None: + return None + else: + return str(value) + + def _get_level(self, level: str): + # lowercase input to catch simple typos + level = level.lower() + if level == "false positive": + return -1 + elif level == "info" or level == "informational": + return 0 + elif level == "low": + return 1 + elif level == "medium": + return 2 + elif level == "high": + return 3 + + logging.warn("AlertFilter configured with unknown level: '%s'. This rule will be ignored!", level) + return None + + def _configure_alert_filters(self, alert_filters: list[collections.OrderedDict], context_id: int): + """Protected method to configure the ZAP 'Context / Alert Filters' Settings based on a given ZAP config. + + Parameters + ---------- + alert_filters : collections.OrderedDict + The current alert filter configuration object containing the ZAP alert filter configuration (based on the class ZapConfiguration). + context_id : int + The zap context id to configure the ZAP alert filters for (based on the class ZapConfiguration). + """ + + if(alert_filters): + for alert_filter in alert_filters: + logging.debug("Adding AlertFilter for rule '%d' in context with id %s", alert_filter["ruleId"], context_id) + + matches = alert_filter["matches"] if "matches" in alert_filter else collections.OrderedDict() + self.get_zap.alertFilter.add_alert_filter( + contextid = context_id, + ruleid = str(alert_filter["ruleId"]), + newlevel = str(self._get_level(alert_filter["newLevel"])), + # optional matchers + url = self._get_or_none(matches, "url"), + urlisregex = self._get_or_none_stringified(matches, "urlIsRegex"), + parameter = self._get_or_none(matches, "parameter"), + parameterisregex = self._get_or_none_stringified(matches, "parameterIsRegex"), + attack = self._get_or_none(matches, "attack"), + attackisregex = self._get_or_none_stringified(matches, "attackIsRegex"), + evidence = self._get_or_none(matches, "evidence"), + evidenceisregex = self._get_or_none_stringified(matches, "evidenceIsRegex"), + ) \ No newline at end of file