From c91acf6a14c0aa69974241ead13223a84dbd5334 Mon Sep 17 00:00:00 2001 From: Stefan Horning Date: Thu, 10 Feb 2022 11:36:13 +0100 Subject: [PATCH] Extended the wafv2_web_acl module with custom_response_bodies argument (#721) Extended the wafv2_web_acl module with custom_response_bodies argument SUMMARY Extended the wafv2_web_acl module to also take the custom_response_bodies argument, improved docs and extended tests ISSUE TYPE Feature Pull Request COMPONENT NAME wafv2_web_acl ADDITIONAL INFORMATION Also touched docs of aws_waf_web_acl to make it easier to find the WAF v2 modules as I had trouble finding that at first. Reviewed-by: Markus Bergholz Reviewed-by: Stefan Horning Reviewed-by: Mark Chappell Reviewed-by: Alina Buzachis --- changelogs/fragments/721-wafv2_web_acl.yml | 3 + plugins/modules/aws_waf_web_acl.py | 6 +- plugins/modules/wafv2_web_acl.py | 144 ++++-- tests/integration/targets/wafv2/meta/main.yml | 6 + tests/integration/targets/wafv2/tasks/alb.yml | 1 - .../targets/wafv2/tasks/create_webacl.yml | 410 ++++++++++++------ .../integration/targets/wafv2/tasks/main.yml | 9 +- .../targets/wafv2/tasks/test_webacl.yml | 3 +- 8 files changed, 405 insertions(+), 177 deletions(-) create mode 100644 changelogs/fragments/721-wafv2_web_acl.yml create mode 100644 tests/integration/targets/wafv2/meta/main.yml diff --git a/changelogs/fragments/721-wafv2_web_acl.yml b/changelogs/fragments/721-wafv2_web_acl.yml new file mode 100644 index 00000000000..a5bcf2f7330 --- /dev/null +++ b/changelogs/fragments/721-wafv2_web_acl.yml @@ -0,0 +1,3 @@ +minor_changes: +- wafv2_web_acl - Extended the wafv2_web_acl module to also take the ``custom_response_bodies`` argument (https://github.com/ansible-collections/community.aws/pull/721). +- wafv2_web_acl - Documentation updates wafv2_web_acl and aws_waf_web_acl (https://github.com/ansible-collections/community.aws/pull/721). diff --git a/plugins/modules/aws_waf_web_acl.py b/plugins/modules/aws_waf_web_acl.py index 7cdf770aa38..609df528a0a 100644 --- a/plugins/modules/aws_waf_web_acl.py +++ b/plugins/modules/aws_waf_web_acl.py @@ -8,11 +8,11 @@ DOCUMENTATION = r''' module: aws_waf_web_acl -short_description: Create and delete WAF Web ACLs. +short_description: Create and delete WAF Web ACLs version_added: 1.0.0 description: - - Read the AWS documentation for WAF - U(https://aws.amazon.com/documentation/waf/). + - Module for WAF classic, for WAF v2 use the I(wafv2_*) modules. + - Read the AWS documentation for WAF U(https://docs.aws.amazon.com/waf/latest/developerguide/classic-waf-chapter.html). author: - Mike Mochan (@mmochan) diff --git a/plugins/modules/wafv2_web_acl.py b/plugins/modules/wafv2_web_acl.py index 5306c2e047f..b11b0872b0e 100644 --- a/plugins/modules/wafv2_web_acl.py +++ b/plugins/modules/wafv2_web_acl.py @@ -11,9 +11,10 @@ version_added: 1.5.0 author: - "Markus Bergholz (@markuman)" -short_description: wafv2_web_acl +short_description: Create and delete WAF Web ACLs description: - - Create, modify or delete a wafv2 web acl. + - Create, modify or delete AWS WAF v2 web ACLs (not for classic WAF). + - See docs at U(https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) options: state: description: @@ -28,9 +29,9 @@ type: str scope: description: - - Scope of wafv2 web acl. + - Geographical scope of the web acl. required: true - choices: ["CLOUDFRONT","REGIONAL"] + choices: ["CLOUDFRONT", "REGIONAL"] type: str description: description: @@ -39,7 +40,7 @@ default_action: description: - Default action of the wafv2 web acl. - choices: ["Block","Allow"] + choices: ["Block", "Allow"] type: str sampled_requests: description: @@ -87,6 +88,14 @@ description: - Rule configuration. type: dict + custom_response_bodies: + description: + - A map of custom response keys and content bodies. Define response bodies here and reference them in the rules by providing + - the key of the body dictionary element. + - Each element must have a unique dict key and in the dict two keys for I(content_type) and I(content). + - Requires botocore >= 1.21.0 + type: dict + version_added: 3.1.0 purge_rules: description: - When set to C(no), keep the existing load balancer rules in place. Will modify and add, but will not delete. @@ -100,16 +109,15 @@ ''' EXAMPLES = ''' -- name: create web acl +- name: Create test web acl community.aws.wafv2_web_acl: name: test05 - state: present description: hallo eins scope: REGIONAL default_action: Allow sampled_requests: no cloudwatch_metrics: yes - metric_name: blub + metric_name: test05-acl-metric rules: - name: zwei priority: 0 @@ -191,10 +199,56 @@ text_transformations: - type: LOWERCASE priority: 0 + purge_rules: yes tags: A: B C: D - register: out + state: present + +- name: Create IP filtering web ACL + community.aws.wafv2_web_acl: + name: ip-filtering-traffic + description: ACL that filters web traffic based on rate limits and whitelists some IPs + scope: REGIONAL + default_action: Allow + sampled_requests: yes + cloudwatch_metrics: yes + metric_name: ip-filtering-traffic + rules: + - name: whitelist-own-IPs + priority: 0 + action: + allow: {} + statement: + ip_set_reference_statement: + arn: 'arn:aws:wafv2:us-east-1:520789123123:regional/ipset/own-public-ips/1c4bdfc4-0f77-3b23-5222-123123123' + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: waf-acl-rule-whitelist-own-IPs + - name: rate-limit-per-IP + priority: 1 + action: + block: + custom_response: + response_code: 429 + custom_response_body_key: too_many_requests + statement: + rate_based_statement: + limit: 5000 + aggregate_key_type: IP + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: waf-acl-rule-rate-limit-per-IP + purge_rules: yes + custom_response_bodies: + too_many_requests: + content_type: APPLICATION_JSON + content: '{ message: "Your request has been blocked due to too many HTTP requests coming from your IP" }' + region: us-east-1 + state: present + ''' RETURN = """ @@ -218,6 +272,12 @@ sample: test02 returned: Always, as long as the web acl exists type: str +default_action: + description: Default action of ACL + returned: Always, as long as the web acl exists + sample: + allow: {} + type: dict rules: description: Current rules of the web acl returned: Always, as long as the web acl exists @@ -235,6 +295,14 @@ cloud_watch_metrics_enabled: true metric_name: admin_protect sampled_requests_enabled: true +custom_response_bodies: + description: Custom response body configurations to be used in rules + type: dict + sample: + too_many_requests: + content_type: APPLICATION_JSON + content: '{ message: "Your request has been blocked due to too many HTTP requests coming from your IP" }' + returned: Always, as long as the web acl exists visibility_config: description: Visibility config of the web acl returned: Always, as long as the web acl exists @@ -267,22 +335,27 @@ def __init__(self, wafv2, name, scope, fail_json_aws): self.fail_json_aws = fail_json_aws self.existing_acl, self.id, self.locktoken = self.get_web_acl() - def update(self, default_action, description, rules, sampled_requests, cloudwatch_metrics, metric_name): + def update(self, default_action, description, rules, sampled_requests, cloudwatch_metrics, metric_name, custom_response_bodies): + req_obj = { + 'Name': self.name, + 'Scope': self.scope, + 'Id': self.id, + 'DefaultAction': default_action, + 'Description': description, + 'Rules': rules, + 'VisibilityConfig': { + 'SampledRequestsEnabled': sampled_requests, + 'CloudWatchMetricsEnabled': cloudwatch_metrics, + 'MetricName': metric_name + }, + 'LockToken': self.locktoken + } + + if custom_response_bodies: + req_obj['CustomResponseBodies'] = custom_response_bodies + try: - response = self.wafv2.update_web_acl( - Name=self.name, - Scope=self.scope, - Id=self.id, - DefaultAction=default_action, - Description=description, - Rules=rules, - VisibilityConfig={ - 'SampledRequestsEnabled': sampled_requests, - 'CloudWatchMetricsEnabled': cloudwatch_metrics, - 'MetricName': metric_name - }, - LockToken=self.locktoken - ) + response = self.wafv2.update_web_acl(**req_obj) except (BotoCoreError, ClientError) as e: self.fail_json_aws(e, msg="Failed to update wafv2 web acl.") return response @@ -331,7 +404,7 @@ def get_web_acl(self): def list(self): return wafv2_list_web_acls(self.wafv2, self.scope, self.fail_json_aws) - def create(self, default_action, rules, sampled_requests, cloudwatch_metrics, metric_name, tags, description): + def create(self, default_action, rules, sampled_requests, cloudwatch_metrics, metric_name, tags, description, custom_response_bodies): req_obj = { 'Name': self.name, 'Scope': self.scope, @@ -343,6 +416,9 @@ def create(self, default_action, rules, sampled_requests, cloudwatch_metrics, me 'MetricName': metric_name } } + + if custom_response_bodies: + req_obj['CustomResponseBodies'] = custom_response_bodies if description: req_obj['Description'] = description if tags: @@ -370,6 +446,7 @@ def main(): cloudwatch_metrics=dict(type='bool', default=True), metric_name=dict(type='str'), tags=dict(type='dict'), + custom_response_bodies=dict(type='dict'), purge_rules=dict(default=True, type='bool') ) @@ -392,6 +469,14 @@ def main(): purge_rules = module.params.get("purge_rules") check_mode = module.check_mode + custom_response_bodies = module.params.get("custom_response_bodies") + if custom_response_bodies: + module.require_botocore_at_least('1.21.0', reason='to set custom response bodies') + custom_response_bodies = {} + + for custom_name, body in module.params.get("custom_response_bodies").items(): + custom_response_bodies[custom_name] = snake_dict_to_camel_dict(body, capitalize_first=True) + if default_action == 'Block': default_action = {'Block': {}} elif default_action == 'Allow': @@ -422,7 +507,8 @@ def main(): rules, sampled_requests, cloudwatch_metrics, - metric_name + metric_name, + custom_response_bodies ) else: @@ -438,7 +524,8 @@ def main(): cloudwatch_metrics, metric_name, tags, - description + description, + custom_response_bodies ) elif state == 'absent': @@ -453,7 +540,8 @@ def main(): rules, sampled_requests, cloudwatch_metrics, - metric_name + metric_name, + custom_response_bodies ) else: change = True diff --git a/tests/integration/targets/wafv2/meta/main.yml b/tests/integration/targets/wafv2/meta/main.yml new file mode 100644 index 00000000000..4afae0b9340 --- /dev/null +++ b/tests/integration/targets/wafv2/meta/main.yml @@ -0,0 +1,6 @@ +dependencies: + - setup_remote_tmp_dir + - role: setup_botocore_pip + vars: + boto3_version: "1.18.0" + botocore_version: "1.21.0" diff --git a/tests/integration/targets/wafv2/tasks/alb.yml b/tests/integration/targets/wafv2/tasks/alb.yml index 6ecb0abb01d..32aeb376a3b 100644 --- a/tests/integration/targets/wafv2/tasks/alb.yml +++ b/tests/integration/targets/wafv2/tasks/alb.yml @@ -101,6 +101,5 @@ - assert: that: - - alb.changed - alb.listeners|length == 1 - alb.listeners[0].rules|length == 1 diff --git a/tests/integration/targets/wafv2/tasks/create_webacl.yml b/tests/integration/targets/wafv2/tasks/create_webacl.yml index 8b195ab570c..978ab282a60 100644 --- a/tests/integration/targets/wafv2/tasks/create_webacl.yml +++ b/tests/integration/targets/wafv2/tasks/create_webacl.yml @@ -1,151 +1,275 @@ ####################### ## Create web acl ####################### -- name: check_mode create web acl - wafv2_web_acl: - name: "{{ web_acl_name }}" - state: present - description: hallo eins - scope: REGIONAL - default_action: Allow - sampled_requests: no - cloudwatch_metrics: yes - metric_name: blub - rules: - - name: zwei - priority: 2 - action: - block: {} - visibility_config: - sampled_requests_enabled: yes - cloud_watch_metrics_enabled: yes - metric_name: ddos - statement: - xss_match_statement: - field_to_match: - body: {} - text_transformations: - - type: NONE - priority: 0 - - name: admin_protect - priority: 1 - override_action: - none: {} - visibility_config: - sampled_requests_enabled: yes - cloud_watch_metrics_enabled: yes - metric_name: fsd - statement: - managed_rule_group_statement: - vendor_name: AWS - name: AWSManagedRulesAdminProtectionRuleSet - tags: - A: B - C: D - register: out - check_mode: yes -- name: check_mode verify create - assert: - that: - - out is changed +- name: Wrap test in virtualenv created above (use other python interpreter) + vars: + ansible_python_interpreter: "{{ botocore_virtualenv_interpreter }}" + block: -- name: create web acl - wafv2_web_acl: - name: "{{ web_acl_name }}" - state: present - description: hallo eins - scope: REGIONAL - default_action: Allow - sampled_requests: no - cloudwatch_metrics: yes - metric_name: blub - rules: - - name: zwei - priority: 2 - action: - block: {} - visibility_config: - sampled_requests_enabled: yes - cloud_watch_metrics_enabled: yes - metric_name: ddos - statement: - xss_match_statement: - field_to_match: - body: {} - text_transformations: - - type: NONE - priority: 0 - - name: admin_protect - priority: 1 - override_action: - none: {} - visibility_config: - sampled_requests_enabled: yes - cloud_watch_metrics_enabled: yes - metric_name: fsd - statement: - managed_rule_group_statement: - vendor_name: AWS - name: AWSManagedRulesAdminProtectionRuleSet - tags: - A: B - C: D - register: ACL + - name: check_mode create web acl + wafv2_web_acl: + name: "{{ web_acl_name }}" + state: present + description: hallo eins + scope: REGIONAL + default_action: Allow + sampled_requests: no + cloudwatch_metrics: yes + metric_name: blub + rules: + - name: zwei + priority: 2 + action: + block: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: ddos + statement: + xss_match_statement: + field_to_match: + body: {} + text_transformations: + - type: NONE + priority: 0 + - name: admin_protect + priority: 1 + override_action: + none: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: fsd + statement: + managed_rule_group_statement: + vendor_name: AWS + name: AWSManagedRulesAdminProtectionRuleSet + custom_response_bodies: + too_many_requests: + content_type: APPLICATION_JSON + content: '{ message: "Your request has been blocked due to too many HTTP requests coming from your IP" }' + tags: + A: B + C: D + register: out + check_mode: yes -- name: verify create - assert: - that: - - ACL is changed - - ACL.web_acl.name == web_acl_name - - not ACL.web_acl.visibility_config.sampled_requests_enabled - - ACL.web_acl.rules | count == 2 - - ACL.web_acl.description == 'hallo eins' + - name: check_mode verify create + assert: + that: + - out is changed -- name: immutable create web acl - wafv2_web_acl: - name: "{{ web_acl_name }}" - state: present - description: hallo eins - scope: REGIONAL - default_action: Allow - sampled_requests: no - cloudwatch_metrics: yes - metric_name: blub - rules: - - name: zwei - priority: 2 - action: - block: {} - visibility_config: - sampled_requests_enabled: yes - cloud_watch_metrics_enabled: yes - metric_name: ddos - statement: - xss_match_statement: - field_to_match: - body: {} - text_transformations: - - type: NONE - priority: 0 - - name: admin_protect - priority: 1 - override_action: - none: {} - visibility_config: - sampled_requests_enabled: yes - cloud_watch_metrics_enabled: yes - metric_name: fsd - statement: - managed_rule_group_statement: - vendor_name: AWS - name: AWSManagedRulesAdminProtectionRuleSet - tags: - A: B - C: D - register: out + - name: Create web acl with custom response bodies + wafv2_web_acl: + name: "{{ resource_prefix }}-acl-with-response-body" + state: present + description: foo + scope: REGIONAL + default_action: Allow + sampled_requests: no + cloudwatch_metrics: no + rules: + - name: rate-limit-per-IP + priority: 1 + action: + block: + custom_response: + response_code: 429 + custom_response_body_key: too_many_requests + statement: + rate_based_statement: + limit: 1000 + aggregate_key_type: IP + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: no + metric_name: unused + custom_response_bodies: + too_many_requests: + content_type: APPLICATION_JSON + content: '{ message: "Your request has been blocked due to too many HTTP requests coming from your IP" }' + register: acl_with_response_body -- name: verify create - assert: - that: - - out is not changed \ No newline at end of file + - name: Web acl with custom response bodies verify create + assert: + that: + - acl_with_response_body is changed + - acl_with_response_body.web_acl.rules | count == 1 + - acl_with_response_body.web_acl.custom_response_bodies.too_many_requests is defined + + - name: Update web acl with custom response bodies to remove custom response + wafv2_web_acl: + name: "{{ resource_prefix }}-acl-with-response-body" + state: present + scope: REGIONAL + description: foo + default_action: Allow + sampled_requests: no + cloudwatch_metrics: no + rules: + - name: rate-limit-per-IP + priority: 1 + action: + block: {} + statement: + rate_based_statement: + limit: 1000 + aggregate_key_type: IP + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: no + metric_name: unused + custom_response_bodies: {} + + # unfortunately the wafv2_web_acl does not return the ACL structure after an update + # hence we have to do another task here using the info module to retrieve the latest state + # of the ACL and then to check it + - name: check if custom response body was really removed + wafv2_web_acl_info: + name: "{{ resource_prefix }}-acl-with-response-body" + scope: REGIONAL + register: acl_without_response_bodies + + - name: Web acl with custom response bodies verify removal of custom response + assert: + that: + - acl_without_response_bodies.custom_response_bodies is undefined + + - name: create web acl + wafv2_web_acl: + name: "{{ web_acl_name }}" + state: present + description: hallo eins + scope: REGIONAL + default_action: Allow + sampled_requests: no + cloudwatch_metrics: yes + metric_name: blub + rules: + - name: zwei + priority: 2 + action: + block: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: ddos + statement: + xss_match_statement: + field_to_match: + body: {} + text_transformations: + - type: NONE + priority: 0 + - name: admin_protect + priority: 1 + override_action: + none: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: fsd + statement: + managed_rule_group_statement: + vendor_name: AWS + name: AWSManagedRulesAdminProtectionRuleSet + - name: rate-limit-per-IP + priority: 3 + action: + block: + custom_response: + response_code: 429 + custom_response_body_key: too_many_requests + statement: + rate_based_statement: + limit: 5000 + aggregate_key_type: IP + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: waf-acl-rule-rate-limit-per-IP + custom_response_bodies: + too_many_requests: + content_type: APPLICATION_JSON + content: '{ message: "Your request has been blocked due to too many HTTP requests coming from your IP" }' + tags: + A: B + C: D + register: ACL + + - name: verify create + assert: + that: + - ACL is changed + - ACL.web_acl.name == web_acl_name + - not ACL.web_acl.visibility_config.sampled_requests_enabled + - ACL.web_acl.rules | count == 3 + - ACL.web_acl.description == 'hallo eins' + + - name: immutable create web acl + wafv2_web_acl: + name: "{{ web_acl_name }}" + state: present + description: hallo eins + scope: REGIONAL + default_action: Allow + sampled_requests: no + cloudwatch_metrics: yes + metric_name: blub + rules: + - name: zwei + priority: 2 + action: + block: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: ddos + statement: + xss_match_statement: + field_to_match: + body: {} + text_transformations: + - type: NONE + priority: 0 + - name: admin_protect + priority: 1 + override_action: + none: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: fsd + statement: + managed_rule_group_statement: + vendor_name: AWS + name: AWSManagedRulesAdminProtectionRuleSet + - name: rate-limit-per-IP + priority: 3 + action: + block: + custom_response: + response_code: 429 + custom_response_body_key: too_many_requests + statement: + rate_based_statement: + limit: 5000 + aggregate_key_type: IP + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: waf-acl-rule-rate-limit-per-IP + custom_response_bodies: + too_many_requests: + content_type: APPLICATION_JSON + content: '{ message: "Your request has been blocked due to too many HTTP requests coming from your IP" }' + tags: + A: B + C: D + register: out + + - name: verify create + assert: + that: + - out is not changed diff --git a/tests/integration/targets/wafv2/tasks/main.yml b/tests/integration/targets/wafv2/tasks/main.yml index fa6e7fb3d86..547c4c15105 100644 --- a/tests/integration/targets/wafv2/tasks/main.yml +++ b/tests/integration/targets/wafv2/tasks/main.yml @@ -64,7 +64,7 @@ assert: that: - out is not changed - + always: ################################### # always delete wafv2 components @@ -91,6 +91,13 @@ scope: REGIONAL ignore_errors: true + - name: Ensure ACL with response body is removed + wafv2_web_acl: + name: "{{ resource_prefix }}-acl-with-response-body" + state: absent + scope: REGIONAL + ignore_errors: true + ######################### # remove alb and its deps ######################### diff --git a/tests/integration/targets/wafv2/tasks/test_webacl.yml b/tests/integration/targets/wafv2/tasks/test_webacl.yml index 2749450ab26..2d09eb3ebc9 100644 --- a/tests/integration/targets/wafv2/tasks/test_webacl.yml +++ b/tests/integration/targets/wafv2/tasks/test_webacl.yml @@ -10,7 +10,7 @@ - name: verify rules assert: that: - - out.rules | count == 2 + - out.rules | count == 3 - name: change web acl description wafv2_web_acl: @@ -116,6 +116,7 @@ sampled_requests: no cloudwatch_metrics: yes metric_name: blub + purge_rules: yes rules: - name: admin_protect priority: 1