From ab0e8cb600672bef0202fdde147c4e6a9a81f655 Mon Sep 17 00:00:00 2001 From: Jeremy Choi Date: Mon, 16 Dec 2024 10:25:48 +1000 Subject: [PATCH 1/3] support replacer job --- config/config-template-zap-long.yaml | 15 +++++++++ scanners/zap/zap.py | 24 +++++++++++++++ tests/scanners/zap/test_setup.py | 46 ++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/config/config-template-zap-long.yaml b/config/config-template-zap-long.yaml index b477395..46b02ac 100644 --- a/config/config-template-zap-long.yaml +++ b/config/config-template-zap-long.yaml @@ -146,11 +146,26 @@ scanners: url: "" # url to start spidering from, default: application.url set above browserId: firefox-headless + + passiveScan: # Optional comma-separated list of passive rules to disable # Use https://www.zaproxy.org/docs/alerts/ to match rule with its ID disabledRules: "2,10015,10024,10027,10054,10096,10109,10112" + #replacer: # replacer replaces strings in requests and responses during active scan + # parameters: + # deleteAllRules: True # Boolean, if true then will delete all existing replacer rules, default false + # rules: # list of rules + # - description: # String, the name of the rule + # url: # String, a regex which will be used to match URLs, if empty then it will match all + # matchType: # String, one of req_header, req_header_str, req_body_str, resp_header, resp_header_str, resp_body_str + # matchString: # String, will be used to identify what should be replaced + # matchRegex: # Boolean, if set then the matchString will be treated as a regex, default false + # replacementString: # String, the new string that will replace the specified selection + # tokenProcessing: # Boolean, when enabled the replacementString may contain a single token + # initiators: # A list of integers representing the initiators (see the help) + activeScan: # The list of parameters: https://www.zaproxy.org/docs/desktop/addons/ajax-spider/automation/ #maxRuleDurationInMins: max scan time for each Rule (default: unlimited) diff --git a/scanners/zap/zap.py b/scanners/zap/zap.py index 20f624d..21e7896 100644 --- a/scanners/zap/zap.py +++ b/scanners/zap/zap.py @@ -365,6 +365,7 @@ def _setup_zap_automation(self): self._setup_api() self._setup_graphql() self._setup_import_urls() + self._setup_replacer() self._setup_active_scan() self._setup_passive_wait() self._setup_report() @@ -618,6 +619,29 @@ def _setup_passive_wait(self): } self.automation_config["jobs"].append(waitfor) + def _setup_replacer(self): + """Adds the replacer to the job list""" + + if self.my_conf("replacer", default=False) is False: + return + + rules = self.my_conf("replacer.rules") + if rules: + if not isinstance(rules, list): + raise ValueError("replacer.rules must be a list") + + # replacer schema + replacer = { + "name": "replacer", + "type": "replacer", + "parameters": { + "deleteAllRules": self.my_conf("replacer.parameters.deleteAllRules", default=True), + }, + "rules": rules, + } + + self.automation_config["jobs"].append(replacer) + def _setup_active_scan(self): """Adds an active scan job list, if there is one""" diff --git a/tests/scanners/zap/test_setup.py b/tests/scanners/zap/test_setup.py index e98b007..435fecc 100644 --- a/tests/scanners/zap/test_setup.py +++ b/tests/scanners/zap/test_setup.py @@ -236,6 +236,52 @@ def test_setup_include_urls(test_config): assert "def" in find_context(test_zap.automation_config)["includePaths"] +def test_setup_replacer(test_config): + # testing if deleteAllRules is set to True when it is not + + test_zap = ZapNone(config=test_config) + test_zap.setup() + + for item in test_zap.automation_config["jobs"]: + if item["type"] == "replacer": + assert item["parameters"]["deleteAllRules"] is True + + # test deleteAllRules option False + test_config.set("scanners.zap.replacer.parameters.deleteAllRules", False) + test_zap = ZapNone(config=test_config) + test_zap.setup() + + for item in test_zap.automation_config["jobs"]: + if item["type"] == "replacer": + assert item["parameters"]["deleteAllRules"] is False + + # test rules1 + test_rule1 = { + "description": "test_rule1", # String, the name of the rule + "url": ".*", # String, a regex which will be used to match URLs, if empty then it will match all + "matchType": "req_body_str", # String, one of req_header, req_header_str, req_body_str, resp_header, resp_header_str, resp_body_str + "matchString": "John Doe,", # String, will be used to identify what should be replaced + "matchRegex": "false", # Boolean, if set then the matchString will be treated as a regex, default false + "replacementString": "JeremyC", + } + test_rule2 = { + "description": "test_rule2", # String, the name of the rule + "matchType": "req_header", # String, one of req_header, req_header_str, req_body_str, resp_header, resp_header_str, resp_body_str + "matchString": "Cookie", # String, will be used to identify what should be replaced + "matchRegex": "false", # Boolean, if set then the matchString will be treated as a regex, default false + "replacementString": "session=abc123", + } + + test_config.set("scanners.zap.replacer.rules", [test_rule1, test_rule2]) + test_zap = ZapNone(config=test_config) + test_zap.setup() + + for item in test_zap.automation_config["jobs"]: + if item["type"] == "replacer": + assert item["rules"][0] is test_rule1 + assert item["rules"][1] is test_rule2 + + @patch("scanners.zap.zap.validate_active_scan_policy") def test_setup_active_scan(mock_validate_active_scan_policy, test_config): test_config.set("scanners.zap.activeScan.maxRuleDurationInMins", 10) From 7058b45d162e6eecedba26ad3bd5cc34dd357ba9 Mon Sep 17 00:00:00 2001 From: Jeremy Choi Date: Mon, 16 Dec 2024 10:29:39 +1000 Subject: [PATCH 2/3] removed some blank lines and updated texts --- config/config-template-zap-long.yaml | 2 -- tests/scanners/zap/test_setup.py | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/config/config-template-zap-long.yaml b/config/config-template-zap-long.yaml index 46b02ac..2cac469 100644 --- a/config/config-template-zap-long.yaml +++ b/config/config-template-zap-long.yaml @@ -146,8 +146,6 @@ scanners: url: "" # url to start spidering from, default: application.url set above browserId: firefox-headless - - passiveScan: # Optional comma-separated list of passive rules to disable # Use https://www.zaproxy.org/docs/alerts/ to match rule with its ID diff --git a/tests/scanners/zap/test_setup.py b/tests/scanners/zap/test_setup.py index 435fecc..a3608e1 100644 --- a/tests/scanners/zap/test_setup.py +++ b/tests/scanners/zap/test_setup.py @@ -237,8 +237,7 @@ def test_setup_include_urls(test_config): def test_setup_replacer(test_config): - # testing if deleteAllRules is set to True when it is not - + # test if deleteAllRules is set to True by default when it is not set test_zap = ZapNone(config=test_config) test_zap.setup() @@ -255,7 +254,7 @@ def test_setup_replacer(test_config): if item["type"] == "replacer": assert item["parameters"]["deleteAllRules"] is False - # test rules1 + # test rules test_rule1 = { "description": "test_rule1", # String, the name of the rule "url": ".*", # String, a regex which will be used to match URLs, if empty then it will match all From a4dcde4719893f89a5400851bdf73e3879b99687 Mon Sep 17 00:00:00 2001 From: Jeremy Choi Date: Tue, 17 Dec 2024 15:46:17 +1000 Subject: [PATCH 3/3] added validation for boolean values. separated tests. updated comments. removed 'initiator' for now which is to be supported when required --- config/config-template-zap-long.yaml | 5 ++- scanners/zap/zap.py | 20 +++++++++-- tests/scanners/zap/test_setup.py | 52 +++++++++++++++++++++++----- 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/config/config-template-zap-long.yaml b/config/config-template-zap-long.yaml index 2cac469..bde4827 100644 --- a/config/config-template-zap-long.yaml +++ b/config/config-template-zap-long.yaml @@ -156,13 +156,12 @@ scanners: # deleteAllRules: True # Boolean, if true then will delete all existing replacer rules, default false # rules: # list of rules # - description: # String, the name of the rule - # url: # String, a regex which will be used to match URLs, if empty then it will match all + # url: # (optional) String, a regex which will be used to match URLs, if empty then it will match all # matchType: # String, one of req_header, req_header_str, req_body_str, resp_header, resp_header_str, resp_body_str # matchString: # String, will be used to identify what should be replaced # matchRegex: # Boolean, if set then the matchString will be treated as a regex, default false # replacementString: # String, the new string that will replace the specified selection - # tokenProcessing: # Boolean, when enabled the replacementString may contain a single token - # initiators: # A list of integers representing the initiators (see the help) + # tokenProcessing: # (optional) Boolean, when enabled the replacementString may contain a single token activeScan: # The list of parameters: https://www.zaproxy.org/docs/desktop/addons/ajax-spider/automation/ diff --git a/scanners/zap/zap.py b/scanners/zap/zap.py index 21e7896..56aaf4b 100644 --- a/scanners/zap/zap.py +++ b/scanners/zap/zap.py @@ -622,7 +622,14 @@ def _setup_passive_wait(self): def _setup_replacer(self): """Adds the replacer to the job list""" - if self.my_conf("replacer", default=False) is False: + def _validate_rule_boolean_values(rule): + if not isinstance(rule["matchRegex"], bool): + raise ValueError("The matchRegex in the replacer rule must be set to a Boolean value") + + if "tokenProcessing" in rule and not isinstance(rule["tokenProcessing"], bool): + raise ValueError("The tokenProcessing in the replacer rule must be set to a Boolean value") + + if not self.my_conf("replacer"): return rules = self.my_conf("replacer.rules") @@ -630,12 +637,21 @@ def _setup_replacer(self): if not isinstance(rules, list): raise ValueError("replacer.rules must be a list") + for item in rules: + _validate_rule_boolean_values(item) + else: + raise ValueError("replacer must have a rule at least") + + delete_all_rules = self.my_conf("replacer.parameters.deleteAllRules", default=True) + if not isinstance(delete_all_rules, bool): + raise ValueError("replacer.parameters.deleteAllRules must be set to a Boolean value") + # replacer schema replacer = { "name": "replacer", "type": "replacer", "parameters": { - "deleteAllRules": self.my_conf("replacer.parameters.deleteAllRules", default=True), + "deleteAllRules": delete_all_rules, }, "rules": rules, } diff --git a/tests/scanners/zap/test_setup.py b/tests/scanners/zap/test_setup.py index a3608e1..535453e 100644 --- a/tests/scanners/zap/test_setup.py +++ b/tests/scanners/zap/test_setup.py @@ -236,49 +236,85 @@ def test_setup_include_urls(test_config): assert "def" in find_context(test_zap.automation_config)["includePaths"] -def test_setup_replacer(test_config): - # test if deleteAllRules is set to True by default when it is not set +def test_setup_replacer_no_rule(test_config): + test_config.set("scanners.zap.replacer.parameters.deleteAllRules", False) + test_zap = ZapNone(config=test_config) + + # test: not having a rule raises ValueError + with pytest.raises(ValueError): + test_zap.setup() + + +def test_setup_replacer_parameter(test_config): + test_rule = { + "description": "test_rule1", # String, the name of the rule + "url": ".*", # (optional) String, a regex which will be used to match URLs, if empty then it will match all + "matchType": "req_body_str", # String, one of req_header, req_header_str, req_body_str, resp_header, resp_header_str, resp_body_str + "matchString": "John Doe", # String, will be used to identify what should be replaced + "matchRegex": False, # Boolean, if set then the matchString will be treated as a regex, default false + "replacementString": "test_string", # String, the new string that will replace the specified selection + "tokenProcessing": False, # (optional) Boolean, when enabled the replacementString may contain a single token + } + + test_config.set("scanners.zap.replacer.rules", [test_rule]) + test_zap = ZapNone(config=test_config) test_zap.setup() + # test: deleteAllRules is True when it is not set for item in test_zap.automation_config["jobs"]: if item["type"] == "replacer": assert item["parameters"]["deleteAllRules"] is True - # test deleteAllRules option False + # test: deleteAllRules parameter is set to False test_config.set("scanners.zap.replacer.parameters.deleteAllRules", False) test_zap = ZapNone(config=test_config) test_zap.setup() for item in test_zap.automation_config["jobs"]: if item["type"] == "replacer": + assert isinstance(item["parameters"]["deleteAllRules"], bool) assert item["parameters"]["deleteAllRules"] is False + # test: when deleteAllRules parameter is not Boolean type + test_config.set("scanners.zap.replacer.parameters.deleteAllRules", "non-boolean") + test_zap = ZapNone(config=test_config) + with pytest.raises(ValueError): + test_zap.setup() + + +def test_setup_replacer_rules(test_config): # test rules test_rule1 = { "description": "test_rule1", # String, the name of the rule - "url": ".*", # String, a regex which will be used to match URLs, if empty then it will match all + "url": ".*", # (optional) String, a regex which will be used to match URLs, if empty then it will match all "matchType": "req_body_str", # String, one of req_header, req_header_str, req_body_str, resp_header, resp_header_str, resp_body_str - "matchString": "John Doe,", # String, will be used to identify what should be replaced - "matchRegex": "false", # Boolean, if set then the matchString will be treated as a regex, default false - "replacementString": "JeremyC", + "matchString": "John Doe", # String, will be used to identify what should be replaced + "matchRegex": False, # Boolean, if set then the matchString will be treated as a regex, default false + "replacementString": "test_string", # String, the new string that will replace the specified selection + "tokenProcessing": False, # (optional) Boolean, when enabled the replacementString may contain a single token } test_rule2 = { "description": "test_rule2", # String, the name of the rule "matchType": "req_header", # String, one of req_header, req_header_str, req_body_str, resp_header, resp_header_str, resp_body_str "matchString": "Cookie", # String, will be used to identify what should be replaced - "matchRegex": "false", # Boolean, if set then the matchString will be treated as a regex, default false + "matchRegex": True, # Boolean, if set then the matchString will be treated as a regex, default false "replacementString": "session=abc123", } test_config.set("scanners.zap.replacer.rules", [test_rule1, test_rule2]) + test_zap = ZapNone(config=test_config) test_zap.setup() for item in test_zap.automation_config["jobs"]: if item["type"] == "replacer": assert item["rules"][0] is test_rule1 + assert isinstance(item["rules"][0]["matchRegex"], bool) + assert isinstance(item["rules"][0]["tokenProcessing"], bool) + assert item["rules"][1] is test_rule2 + assert isinstance(item["rules"][1]["matchRegex"], bool) @patch("scanners.zap.zap.validate_active_scan_policy")