From ceba50642f934d2a8e677a7d4caaf9103d539e2f Mon Sep 17 00:00:00 2001 From: Jannik Hollenbach Date: Wed, 21 Jul 2021 22:15:35 +0200 Subject: [PATCH 1/9] Add option to add alert filters for contexts Signed-off-by: Jannik Hollenbach --- .../zap-advanced/examples/nginx/0-foo.yaml | 52 +++++++++++++++ .../scanner/zapclient/context/zap_context.py | 64 ++++++++++++++++++- 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 scanners/zap-advanced/examples/nginx/0-foo.yaml 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/zapclient/context/zap_context.py b/scanners/zap-advanced/scanner/zapclient/context/zap_context.py index 934333c9ce..69ca378e40 100644 --- a/scanners/zap-advanced/scanner/zapclient/context/zap_context.py +++ b/scanners/zap-advanced/scanner/zapclient/context/zap_context.py @@ -17,7 +17,7 @@ # set up logging to file - see previous section for more details logging.basicConfig( - level=logging.INFO, + level=logging.DEBUG, format='%(asctime)s %(name)-12s %(levelname)-8s: %(message)s', datefmt='%Y-%m-%d %H:%M') @@ -99,6 +99,12 @@ 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) + logging.info('Starting AlertFilter Config: ' + context_name) + if self._is_not_empty("alertFilters", context): + logging.info('AlertFilter Config not empty: ' + context_name) + logging.info('AlertFilter Config not empty: %s', context["alertFilters"]) + 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 +283,59 @@ 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 key in dict: + return dict[key] + else: + return None + + 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 unkown level: '%s'. This rule will be ignored!", level) + # todo better to crash, but how? + 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: + + matches = alert_filter["matches"] if "matches" in alert_filter else OrderedDict() + + logging.info("Adding AlertFilter for rule '%d' in context with id %s", alert_filter["ruleId"], context_id) + 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 = str(self._get_or_none(matches, "urlIsRegex")), + parameter = self._get_or_none(matches, "parameter"), + parameterisregex = str(self._get_or_none(matches, "parameterIsRegex")), + attack = self._get_or_none(matches, "attack"), + attackisregex = str(self._get_or_none(matches, "attackIsRegex")), + evidence = self._get_or_none(matches, "evidence"), + evidenceisregex = str(self._get_or_none(matches, "evidenceIsRegex")), + ) \ No newline at end of file From 8fbc3f7efc01e1debdba933268f130f2160edb09 Mon Sep 17 00:00:00 2001 From: Jannik Hollenbach Date: Fri, 23 Jul 2021 14:57:16 +0200 Subject: [PATCH 2/9] Fix init of empty collection if no matcher is configured Signed-off-by: Jannik Hollenbach --- scanners/zap-advanced/scanner/zapclient/context/zap_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanners/zap-advanced/scanner/zapclient/context/zap_context.py b/scanners/zap-advanced/scanner/zapclient/context/zap_context.py index 69ca378e40..04ad04a34b 100644 --- a/scanners/zap-advanced/scanner/zapclient/context/zap_context.py +++ b/scanners/zap-advanced/scanner/zapclient/context/zap_context.py @@ -322,7 +322,7 @@ def _configure_alert_filters(self, alert_filters: list[collections.OrderedDict], if(alert_filters): for alert_filter in alert_filters: - matches = alert_filter["matches"] if "matches" in alert_filter else OrderedDict() + matches = alert_filter["matches"] if "matches" in alert_filter else collections.OrderedDict() logging.info("Adding AlertFilter for rule '%d' in context with id %s", alert_filter["ruleId"], context_id) self.get_zap.alertFilter.add_alert_filter( From 9526b23c0587df00c8c824b0a4ad55d76c697411 Mon Sep 17 00:00:00 2001 From: Jannik Hollenbach Date: Fri, 23 Jul 2021 14:57:38 +0200 Subject: [PATCH 3/9] Tweak log levels Signed-off-by: Jannik Hollenbach --- .../zap-advanced/scanner/zapclient/context/zap_context.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scanners/zap-advanced/scanner/zapclient/context/zap_context.py b/scanners/zap-advanced/scanner/zapclient/context/zap_context.py index 04ad04a34b..d066103a98 100644 --- a/scanners/zap-advanced/scanner/zapclient/context/zap_context.py +++ b/scanners/zap-advanced/scanner/zapclient/context/zap_context.py @@ -17,7 +17,7 @@ # set up logging to file - see previous section for more details logging.basicConfig( - level=logging.DEBUG, + level=logging.INFO, format='%(asctime)s %(name)-12s %(levelname)-8s: %(message)s', datefmt='%Y-%m-%d %H:%M') @@ -304,8 +304,7 @@ def _get_level(self, level: str): elif level == "high": return 3 - logging.warn("AlertFilter configured with unkown level: '%s'. This rule will be ignored!", level) - # todo better to crash, but how? + 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): @@ -321,10 +320,9 @@ def _configure_alert_filters(self, alert_filters: list[collections.OrderedDict], 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() - - logging.info("Adding AlertFilter for rule '%d' in context with id %s", alert_filter["ruleId"], context_id) self.get_zap.alertFilter.add_alert_filter( contextid = context_id, ruleid = str(alert_filter["ruleId"]), From 6d3519f2ad41c1cf768c73e95b1e01306b2316b6 Mon Sep 17 00:00:00 2001 From: Jannik Hollenbach Date: Fri, 23 Jul 2021 14:59:01 +0200 Subject: [PATCH 4/9] Add intergration test for alertFilters Signed-off-by: Jannik Hollenbach --- .../1_zap-advanced-scan-config.yaml | 138 ++++++++++++++++++ .../tests/test_integration_docker_local.py | 17 +++ 2 files changed, 155 insertions(+) create mode 100644 scanners/zap-advanced/scanner/tests/mocks/scan-full-petstore-alert-filter-docker/1_zap-advanced-scan-config.yaml 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..8c17d3534b 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: %s', str(len(alerts))) + + # should normally be 13 alerts but most of them are ignored using alertFilters in the scan config + assert int(len(alerts)) < 10 From 2a70f95547df5b236716727551197f327aed035d Mon Sep 17 00:00:00 2001 From: Jannik Hollenbach Date: Mon, 26 Jul 2021 09:17:19 +0200 Subject: [PATCH 5/9] Log out integer directly Signed-off-by: Jannik Hollenbach --- .../zap-advanced/scanner/tests/test_integration_docker_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8c17d3534b..95af9d30e5 100644 --- a/scanners/zap-advanced/scanner/tests/test_integration_docker_local.py +++ b/scanners/zap-advanced/scanner/tests/test_integration_docker_local.py @@ -203,7 +203,7 @@ def test_petstore_scan_with_alert_filters(get_petstore_url, get_zap_instance: ZA alerts = zap_automation.get_zap_scanner.get_alerts(test_target, [], []) - logging.info('Found ZAP Alerts: %s', str(len(alerts))) + 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 From d6e7816dfbf4647c876f9b061614bf0c69a188e6 Mon Sep 17 00:00:00 2001 From: Jannik Hollenbach Date: Mon, 26 Jul 2021 09:21:36 +0200 Subject: [PATCH 6/9] Remove purly debugging logs Signed-off-by: Jannik Hollenbach --- scanners/zap-advanced/scanner/zapclient/context/zap_context.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/scanners/zap-advanced/scanner/zapclient/context/zap_context.py b/scanners/zap-advanced/scanner/zapclient/context/zap_context.py index d066103a98..2c723729fd 100644 --- a/scanners/zap-advanced/scanner/zapclient/context/zap_context.py +++ b/scanners/zap-advanced/scanner/zapclient/context/zap_context.py @@ -99,10 +99,7 @@ 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) - logging.info('Starting AlertFilter Config: ' + context_name) if self._is_not_empty("alertFilters", context): - logging.info('AlertFilter Config not empty: ' + context_name) - logging.info('AlertFilter Config not empty: %s', context["alertFilters"]) self._configure_alert_filters(context["alertFilters"], context_id) def _configure_context_include(self, context: collections.OrderedDict): From 6c373147d6526637f1092e71354d73e89c1e21b6 Mon Sep 17 00:00:00 2001 From: Jannik Hollenbach Date: Mon, 26 Jul 2021 13:41:35 +0200 Subject: [PATCH 7/9] Passing None as acutal nones instead of a stringified 'None' Signed-off-by: Jannik Hollenbach --- .../zap-advanced/scanner/zapclient/context/zap_context.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scanners/zap-advanced/scanner/zapclient/context/zap_context.py b/scanners/zap-advanced/scanner/zapclient/context/zap_context.py index 2c723729fd..e178683332 100644 --- a/scanners/zap-advanced/scanner/zapclient/context/zap_context.py +++ b/scanners/zap-advanced/scanner/zapclient/context/zap_context.py @@ -326,11 +326,11 @@ def _configure_alert_filters(self, alert_filters: list[collections.OrderedDict], newlevel = str(self._get_level(alert_filter["newLevel"])), # optional matchers url = self._get_or_none(matches, "url"), - urlisregex = str(self._get_or_none(matches, "urlIsRegex")), + urlisregex = str(self._get_or_none(matches, "urlIsRegex")) if self._get_or_none(matches, "urlIsRegex") != None else None, parameter = self._get_or_none(matches, "parameter"), - parameterisregex = str(self._get_or_none(matches, "parameterIsRegex")), + parameterisregex = str(self._get_or_none(matches, "parameterIsRegex")) if self._get_or_none(matches, "parameterIsRegex") != None else None, attack = self._get_or_none(matches, "attack"), - attackisregex = str(self._get_or_none(matches, "attackIsRegex")), + attackisregex = str(self._get_or_none(matches, "attackIsRegex")) if self._get_or_none(matches, "attackIsRegex") != None else None, evidence = self._get_or_none(matches, "evidence"), - evidenceisregex = str(self._get_or_none(matches, "evidenceIsRegex")), + evidenceisregex = str(self._get_or_none(matches, "evidenceIsRegex")) if self._get_or_none(matches, "evidenceIsRegex") != None else None, ) \ No newline at end of file From 45738f15a4a7bbc1c133154987bb99a2957dfcb7 Mon Sep 17 00:00:00 2001 From: Jannik Hollenbach Date: Mon, 26 Jul 2021 13:42:29 +0200 Subject: [PATCH 8/9] Add null & sanity check for helper method to better catch empty like values Signed-off-by: Jannik Hollenbach --- scanners/zap-advanced/scanner/zapclient/context/zap_context.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scanners/zap-advanced/scanner/zapclient/context/zap_context.py b/scanners/zap-advanced/scanner/zapclient/context/zap_context.py index e178683332..cec7cd50f7 100644 --- a/scanners/zap-advanced/scanner/zapclient/context/zap_context.py +++ b/scanners/zap-advanced/scanner/zapclient/context/zap_context.py @@ -282,6 +282,9 @@ def _configure_context_technologies(self, technology: collections.OrderedDict, c 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: From 86565a565f152020908017223c287a09ce6eb7d8 Mon Sep 17 00:00:00 2001 From: Jannik Hollenbach Date: Mon, 26 Jul 2021 15:49:40 +0200 Subject: [PATCH 9/9] Add additional helper method to reduce complexity Signed-off-by: Jannik Hollenbach --- .../scanner/zapclient/context/zap_context.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/scanners/zap-advanced/scanner/zapclient/context/zap_context.py b/scanners/zap-advanced/scanner/zapclient/context/zap_context.py index cec7cd50f7..66015f5bd6 100644 --- a/scanners/zap-advanced/scanner/zapclient/context/zap_context.py +++ b/scanners/zap-advanced/scanner/zapclient/context/zap_context.py @@ -289,6 +289,14 @@ def _get_or_none(self, dict: collections.OrderedDict, key: str): 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 @@ -329,11 +337,11 @@ def _configure_alert_filters(self, alert_filters: list[collections.OrderedDict], newlevel = str(self._get_level(alert_filter["newLevel"])), # optional matchers url = self._get_or_none(matches, "url"), - urlisregex = str(self._get_or_none(matches, "urlIsRegex")) if self._get_or_none(matches, "urlIsRegex") != None else None, + urlisregex = self._get_or_none_stringified(matches, "urlIsRegex"), parameter = self._get_or_none(matches, "parameter"), - parameterisregex = str(self._get_or_none(matches, "parameterIsRegex")) if self._get_or_none(matches, "parameterIsRegex") != None else None, + parameterisregex = self._get_or_none_stringified(matches, "parameterIsRegex"), attack = self._get_or_none(matches, "attack"), - attackisregex = str(self._get_or_none(matches, "attackIsRegex")) if self._get_or_none(matches, "attackIsRegex") != None else None, + attackisregex = self._get_or_none_stringified(matches, "attackIsRegex"), evidence = self._get_or_none(matches, "evidence"), - evidenceisregex = str(self._get_or_none(matches, "evidenceIsRegex")) if self._get_or_none(matches, "evidenceIsRegex") != None else None, + evidenceisregex = self._get_or_none_stringified(matches, "evidenceIsRegex"), ) \ No newline at end of file