diff --git a/meta/runtime.yml b/meta/runtime.yml index bce9a45f7d5..307cb77459e 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -221,6 +221,12 @@ action_groups: - sts_session_token - wafv2_ip_set - wafv2_ip_set_info + - wafv2_resources + - wafv2_resources_info + - wafv2_rule_group + - wafv2_rule_group_info + - wafv2_web_acl + - wafv2_web_acl_info plugin_routing: modules: diff --git a/plugins/module_utils/wafv2.py b/plugins/module_utils/wafv2.py new file mode 100644 index 00000000000..7b1b63d6fcf --- /dev/null +++ b/plugins/module_utils/wafv2.py @@ -0,0 +1,146 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +try: + from botocore.exceptions import ClientError, BotoCoreError +except ImportError: + pass # caught by AnsibleAWSModule + + +def wafv2_list_web_acls(wafv2, scope, fail_json_aws, nextmarker=None): + # there is currently no paginator for wafv2 + req_obj = { + 'Scope': scope, + 'Limit': 100 + } + if nextmarker: + req_obj['NextMarker'] = nextmarker + + try: + response = wafv2.list_web_acls(**req_obj) + except (BotoCoreError, ClientError) as e: + fail_json_aws(e, msg="Failed to list wafv2 web acl.") + + if response.get('NextMarker'): + response['WebACLs'] += wafv2_list_web_acls(wafv2, scope, fail_json_aws, nextmarker=response.get('NextMarker')).get('WebACLs') + return response + + +def wafv2_list_rule_groups(wafv2, scope, fail_json_aws, nextmarker=None): + # there is currently no paginator for wafv2 + req_obj = { + 'Scope': scope, + 'Limit': 100 + } + if nextmarker: + req_obj['NextMarker'] = nextmarker + + try: + response = wafv2.list_rule_groups(**req_obj) + except (BotoCoreError, ClientError) as e: + fail_json_aws(e, msg="Failed to list wafv2 rule group.") + + if response.get('NextMarker'): + response['RuleGroups'] += wafv2_list_rule_groups(wafv2, scope, fail_json_aws, nextmarker=response.get('NextMarker')).get('RuleGroups') + return response + + +def wafv2_snake_dict_to_camel_dict(a): + retval = {} + for item in a.keys(): + if isinstance(a.get(item), dict): + if 'Ip' in item: + retval[item.replace('Ip', 'IP')] = wafv2_snake_dict_to_camel_dict(a.get(item)) + elif 'Arn' == item: + retval['ARN'] = wafv2_snake_dict_to_camel_dict(a.get(item)) + else: + retval[item] = wafv2_snake_dict_to_camel_dict(a.get(item)) + elif isinstance(a.get(item), list): + retval[item] = [] + for idx in range(len(a.get(item))): + retval[item].append(wafv2_snake_dict_to_camel_dict(a.get(item)[idx])) + elif 'Ip' in item: + retval[item.replace('Ip', 'IP')] = a.get(item) + elif 'Arn' == item: + retval['ARN'] = a.get(item) + else: + retval[item] = a.get(item) + return retval + + +def nested_byte_values_to_strings(rule, keyname): + """ + currently valid nested byte values in statements array are + - OrStatement + - AndStatement + - NotStatement + """ + if rule.get('Statement', {}).get(keyname): + for idx in range(len(rule.get('Statement', {}).get(keyname, {}).get('Statements'))): + if rule['Statement'][keyname]['Statements'][idx].get('ByteMatchStatement'): + rule['Statement'][keyname]['Statements'][idx]['ByteMatchStatement']['SearchString'] = \ + rule.get('Statement').get(keyname).get('Statements')[idx].get('ByteMatchStatement').get('SearchString').decode('utf-8') + + return rule + + +def byte_values_to_strings_before_compare(rules): + for idx in range(len(rules)): + if rules[idx].get('Statement', {}).get('ByteMatchStatement', {}).get('SearchString'): + rules[idx]['Statement']['ByteMatchStatement']['SearchString'] = \ + rules[idx].get('Statement').get('ByteMatchStatement').get('SearchString').decode('utf-8') + + else: + for statement in ['AndStatement', 'OrStatement', 'NotStatement']: + if rules[idx].get('Statement', {}).get(statement): + rules[idx] = nested_byte_values_to_strings(rules[idx], statement) + + return rules + + +def compare_priority_rules(existing_rules, requested_rules, purge_rules, state): + diff = False + existing_rules = sorted(existing_rules, key=lambda k: k['Priority']) + existing_rules = byte_values_to_strings_before_compare(existing_rules) + requested_rules = sorted(requested_rules, key=lambda k: k['Priority']) + + if purge_rules and state == 'present': + merged_rules = requested_rules + if len(existing_rules) == len(requested_rules): + for idx in range(len(existing_rules)): + if existing_rules[idx] != requested_rules[idx]: + diff = True + break + else: + diff = True + + else: + # find same priority rules + # * pop same priority rule from existing rule + # * compare existing rule + merged_rules = [] + ex_idx_pop = [] + for existing_idx in range(len(existing_rules)): + for requested_idx in range(len(requested_rules)): + if existing_rules[existing_idx].get('Priority') == requested_rules[requested_idx].get('Priority'): + if state == 'present': + ex_idx_pop.append(existing_idx) + if existing_rules[existing_idx] != requested_rules[requested_idx]: + diff = True + elif existing_rules[existing_idx] == requested_rules[requested_idx]: + ex_idx_pop.append(existing_idx) + diff = True + + prev_count = len(existing_rules) + for idx in ex_idx_pop: + existing_rules.pop(idx) + + if state == 'present': + merged_rules = existing_rules + requested_rules + + if len(merged_rules) != prev_count: + diff = True + else: + merged_rules = existing_rules + + return diff, merged_rules diff --git a/plugins/modules/wafv2_resources.py b/plugins/modules/wafv2_resources.py new file mode 100644 index 00000000000..4bf5f1dcca3 --- /dev/null +++ b/plugins/modules/wafv2_resources.py @@ -0,0 +1,177 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: wafv2_resources +version_added: 1.5.0 +author: + - "Markus Bergholz (@markuman)" +short_description: wafv2_web_acl +description: + - Apply or remove wafv2 to other aws resources. +requirements: + - boto3 + - botocore +options: + state: + description: + - Whether the rule is present or absent. + choices: ["present", "absent"] + required: true + type: str + name: + description: + - The name of the web acl. + type: str + scope: + description: + - Scope of waf + choices: ["CLOUDFRONT","REGIONAL"] + type: str + arn: + description: + - AWS resources (ALB, API Gateway or AppSync GraphQL API) ARN + type: str + required: true + +extends_documentation_fragment: +- amazon.aws.aws +- amazon.aws.ec2 + +''' + +EXAMPLES = ''' +- name: add test alb to waf string03 + community.aws.wafv2_resources: + name: string03 + scope: REGIONAL + state: present + arn: "arn:aws:elasticloadbalancing:eu-central-1:111111111:loadbalancer/app/test03/dd83ea041ba6f933" +''' + +RETURN = """ +resource_arns: + description: Current resources where the wafv2 is applied on + sample: + - "arn:aws:elasticloadbalancing:eu-central-1:111111111:loadbalancer/app/test03/dd83ea041ba6f933" + returned: Always, as long as the wafv2 exists + type: list +""" +from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule, is_boto3_error_code, get_boto3_client_method_parameters +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict, ansible_dict_to_boto3_tag_list +from ansible_collections.community.aws.plugins.module_utils.wafv2 import wafv2_list_web_acls + +try: + from botocore.exceptions import ClientError, BotoCoreError +except ImportError: + pass # caught by AnsibleAWSModule + + +def get_web_acl(wafv2, name, scope, id, fail_json_aws): + try: + response = wafv2.get_web_acl( + Name=name, + Scope=scope, + Id=id + ) + except (BotoCoreError, ClientError) as e: + fail_json_aws(e, msg="Failed to get wafv2 web acl.") + return response + + +def list_wafv2_resources(wafv2, arn, fail_json_aws): + try: + response = wafv2.list_resources_for_web_acl( + WebACLArn=arn + ) + except (BotoCoreError, ClientError) as e: + fail_json_aws(e, msg="Failed to list wafv2 web acl.") + return response + + +def add_wafv2_resources(wafv2, waf_arn, arn, fail_json_aws): + try: + response = wafv2.associate_web_acl( + WebACLArn=waf_arn, + ResourceArn=arn + ) + except (BotoCoreError, ClientError) as e: + fail_json_aws(e, msg="Failed to add wafv2 web acl.") + return response + + +def remove_resources(wafv2, arn, fail_json_aws): + try: + response = wafv2.disassociate_web_acl( + ResourceArn=arn + ) + except (BotoCoreError, ClientError) as e: + fail_json_aws(e, msg="Failed to remove wafv2 web acl.") + return response + + +def main(): + + arg_spec = dict( + state=dict(type='str', required=True, choices=['present', 'absent']), + name=dict(type='str'), + scope=dict(type='str', choices=['CLOUDFRONT', 'REGIONAL']), + arn=dict(type='str', required=True) + ) + + module = AnsibleAWSModule( + argument_spec=arg_spec, + supports_check_mode=True, + required_if=[['state', 'present', ['name', 'scope']]] + ) + + state = module.params.get("state") + name = module.params.get("name") + scope = module.params.get("scope") + arn = module.params.get("arn") + check_mode = module.check_mode + + wafv2 = module.client('wafv2') + + # check if web acl exists + + response = wafv2_list_web_acls(wafv2, scope, module.fail_json_aws) + + id = None + retval = {} + change = False + + for item in response.get('WebACLs'): + if item.get('Name') == name: + id = item.get('Id') + + if id: + existing_acl = get_web_acl(wafv2, name, scope, id, module.fail_json_aws) + waf_arn = existing_acl.get('WebACL').get('ARN') + + retval = list_wafv2_resources(wafv2, waf_arn, module.fail_json_aws) + + if state == 'present': + if retval: + if arn not in retval.get('ResourceArns'): + change = True + if not check_mode: + retval = add_wafv2_resources(wafv2, waf_arn, arn, module.fail_json_aws) + + elif state == 'absent': + if retval: + if arn in retval.get('ResourceArns'): + change = True + if not check_mode: + retval = remove_resources(wafv2, arn, module.fail_json_aws) + + module.exit_json(changed=change, **camel_dict_to_snake_dict(retval)) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/wafv2_resources_info.py b/plugins/modules/wafv2_resources_info.py new file mode 100644 index 00000000000..469fc3b7184 --- /dev/null +++ b/plugins/modules/wafv2_resources_info.py @@ -0,0 +1,125 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: wafv2_resources_info +version_added: 1.5.0 +author: + - "Markus Bergholz (@markuman)" +short_description: wafv2_resources_info +description: + - List web acl resources. +requirements: + - boto3 + - botocore +options: + name: + description: + - The name wafv2 acl of interest. + type: str + required: true + scope: + description: + - Scope of wafv2 web acl. + required: true + choices: ["CLOUDFRONT","REGIONAL"] + type: str + +extends_documentation_fragment: +- amazon.aws.aws +- amazon.aws.ec2 + +''' + +EXAMPLES = ''' +- name: get web acl + community.aws.wafv2_resources_info: + name: string03 + scope: REGIONAL +''' + +RETURN = """ +resource_arns: + description: Current resources where the wafv2 is applied on + sample: + - "arn:aws:elasticloadbalancing:eu-central-1:111111111:loadbalancer/app/test03/dd83ea041ba6f933" + returned: Always, as long as the wafv2 exists + type: list +""" +from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule, is_boto3_error_code, get_boto3_client_method_parameters +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict, ansible_dict_to_boto3_tag_list +from ansible_collections.community.aws.plugins.module_utils.wafv2 import wafv2_list_web_acls + +try: + from botocore.exceptions import ClientError, BotoCoreError +except ImportError: + pass # caught by AnsibleAWSModule + + +def get_web_acl(wafv2, name, scope, id, fail_json_aws): + try: + response = wafv2.get_web_acl( + Name=name, + Scope=scope, + Id=id + ) + except (BotoCoreError, ClientError) as e: + fail_json_aws(e, msg="Failed to get wafv2 web acl.") + return response + + +def list_web_acls(wafv2, scope, fail_json_aws): + return wafv2_list_web_acls(wafv2, scope, fail_json_aws) + + +def list_wafv2_resources(wafv2, arn, fail_json_aws): + try: + response = wafv2.list_resources_for_web_acl( + WebACLArn=arn + ) + except (BotoCoreError, ClientError) as e: + fail_json_aws(e, msg="Failed to list wafv2 resources.") + return response + + +def main(): + + arg_spec = dict( + name=dict(type='str', required=True), + scope=dict(type='str', required=True, choices=['CLOUDFRONT', 'REGIONAL']) + ) + + module = AnsibleAWSModule( + argument_spec=arg_spec + ) + + name = module.params.get("name") + scope = module.params.get("scope") + + wafv2 = module.client('wafv2') + # check if web acl exists + response = list_web_acls(wafv2, scope, module.fail_json_aws) + + id = None + retval = {} + + for item in response.get('WebACLs'): + if item.get('Name') == name: + id = item.get('Id') + + if id: + existing_acl = get_web_acl(wafv2, name, scope, id, module.fail_json_aws) + arn = existing_acl.get('WebACL').get('ARN') + + retval = camel_dict_to_snake_dict(list_wafv2_resources(wafv2, arn, module.fail_json_aws)) + + module.exit_json(**retval) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/wafv2_rule_group.py b/plugins/modules/wafv2_rule_group.py new file mode 100644 index 00000000000..d66d4864bb5 --- /dev/null +++ b/plugins/modules/wafv2_rule_group.py @@ -0,0 +1,420 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: wafv2_rule_group +version_added: 1.5.0 +author: + - "Markus Bergholz (@markuman)" +short_description: wafv2_web_acl +description: + - Create, modify and delete wafv2 rule groups. +requirements: + - boto3 + - botocore +options: + state: + description: + - Whether the rule is present or absent. + choices: ["present", "absent"] + required: true + type: str + name: + description: + - The name of the rule group. + required: true + type: str + rules: + description: + - The Rule statements used to identify the web requests that you want to allow, block, or count. + type: list + elements: dict + scope: + description: + - Scope of wafv2 rule group. + required: true + choices: ["CLOUDFRONT","REGIONAL"] + type: str + description: + description: + - Description of wafv2 rule group. + type: str + sampled_requests: + description: + - Sampled requests, true or false. + type: bool + default: false + cloudwatch_metrics: + description: + - Enable cloudwatch metric for wafv2 rule group + type: bool + default: true + metric_name: + description: + - Name of cloudwatch metrics. + - If not given and cloudwatch_metrics is enabled, the name of the rule group itself will be taken. + type: str + capacity: + description: + - capacity of wafv2 rule group. + type: int + tags: + description: + - tags for wafv2 rule group. + type: dict + purge_rules: + description: + - When set to C(no), keep the existing load balancer rules in place. Will modify and add, but will not delete. + default: yes + type: bool + +extends_documentation_fragment: +- amazon.aws.aws +- amazon.aws.ec2 + +''' + +EXAMPLES = ''' +- name: change description + community.aws.wafv2_rule_group: + name: test02 + state: present + description: hallo eins zwei + scope: REGIONAL + capacity: 500 + rules: + - name: eins + priority: 1 + action: + allow: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: fsd + statement: + ip_set_reference_statement: + arn: "{{ IPSET.arn }}" + cloudwatch_metrics: yes + tags: + A: B + C: D + register: out + +- name: add rule + community.aws.wafv2_rule_group: + name: test02 + state: present + description: hallo eins zwei + scope: REGIONAL + capacity: 500 + rules: + - name: eins + priority: 1 + action: + allow: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: fsd + statement: + ip_set_reference_statement: + arn: "{{ IPSET.arn }}" + - name: zwei + priority: 2 + action: + block: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: ddos + statement: + or_statement: + statements: + - byte_match_statement: + search_string: ansible.com + positional_constraint: CONTAINS + field_to_match: + single_header: + name: host + text_transformations: + - type: LOWERCASE + priority: 0 + - xss_match_statement: + field_to_match: + body: {} + text_transformations: + - type: NONE + priority: 0 + cloudwatch_metrics: yes + tags: + A: B + C: D + register: out +''' + +RETURN = """ +arn: + description: Rule group arn + sample: arn:aws:wafv2:eu-central-1:11111111:regional/rulegroup/test02/6e90c01a-e4eb-43e5-b6aa-b1604cedf7d7 + type: str + returned: Always, as long as the web acl exists +description: + description: Description of the rule group + sample: Some rule group description + returned: Always, as long as the web acl exists + type: str +capacity: + description: Current capacity of the rule group + sample: 500 + returned: Always, as long as the rule group exists + type: int +name: + description: Rule group name + sample: test02 + returned: Always, as long as the rule group exists + type: str +rules: + description: Current rules of the rule group + returned: Always, as long as the rule group exists + type: list + sample: + - action: + allow: {} + name: eins + priority: 1 + statement: + ip_set_reference_statement: + arn: arn:aws:wafv2:eu-central-1:11111111:regional/ipset/test02/b6978915-c67b-4d1c-8832-2b1bb452143a + visibility_config: + cloud_watch_metrics_enabled: True + metric_name: fsd + sampled_requests_enabled: True +visibility_config: + description: Visibility config of the rule group + returned: Always, as long as the rule group exists + type: dict + sample: + cloud_watch_metrics_enabled: True + metric_name: blub + sampled_requests_enabled: False +""" +from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule, is_boto3_error_code, get_boto3_client_method_parameters +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import snake_dict_to_camel_dict, camel_dict_to_snake_dict, ansible_dict_to_boto3_tag_list +from ansible_collections.community.aws.plugins.module_utils.wafv2 import wafv2_list_rule_groups, compare_priority_rules, wafv2_snake_dict_to_camel_dict + +try: + from botocore.exceptions import ClientError, BotoCoreError +except ImportError: + pass # caught by AnsibleAWSModule + + +class RuleGroup: + def __init__(self, wafv2, name, scope, fail_json_aws): + self.wafv2 = wafv2 + self.name = name + self.scope = scope + self.fail_json_aws = fail_json_aws + self.existing_group, self.id, self.locktoken = self.get_group() + + def update(self, description, rules, sampled_requests, cloudwatch_metrics, metric_name): + req_obj = { + 'Name': self.name, + 'Scope': self.scope, + 'Id': self.id, + 'Rules': rules, + 'LockToken': self.locktoken, + 'VisibilityConfig': { + 'SampledRequestsEnabled': sampled_requests, + 'CloudWatchMetricsEnabled': cloudwatch_metrics, + 'MetricName': metric_name + } + } + + if description: + req_obj['Description'] = description + + try: + response = self.wafv2.update_rule_group(**req_obj) + except (BotoCoreError, ClientError) as e: + self.fail_json_aws(e, msg="Failed to update wafv2 rule group.") + return response + + def get_group(self): + response = self.list() + id = None + locktoken = None + arn = None + + for item in response.get('RuleGroups'): + if item.get('Name') == self.name: + id = item.get('Id') + locktoken = item.get('LockToken') + arn = item.get('ARN') + + existing_group = None + if id: + try: + existing_group = self.wafv2.get_rule_group( + Name=self.name, + Scope=self.scope, + Id=id + ) + except (BotoCoreError, ClientError) as e: + self.fail_json_aws(e, msg="Failed to get wafv2 rule group.") + + return existing_group, id, locktoken + + def list(self): + return wafv2_list_rule_groups(self.wafv2, self.scope, self.fail_json_aws) + + def get(self): + if self.existing_group: + return self.existing_group + return None + + def remove(self): + try: + response = self.wafv2.delete_rule_group( + Name=self.name, + Scope=self.scope, + Id=self.id, + LockToken=self.locktoken + ) + except (BotoCoreError, ClientError) as e: + self.fail_json_aws(e, msg="Failed to delete wafv2 rule group.") + return response + + def create(self, capacity, description, rules, sampled_requests, cloudwatch_metrics, metric_name, tags): + req_obj = { + 'Name': self.name, + 'Scope': self.scope, + 'Capacity': capacity, + 'Rules': rules, + 'VisibilityConfig': { + 'SampledRequestsEnabled': sampled_requests, + 'CloudWatchMetricsEnabled': cloudwatch_metrics, + 'MetricName': metric_name + } + } + + if description: + req_obj['Description'] = description + + if tags: + req_obj['Tags'] = ansible_dict_to_boto3_tag_list(tags) + + try: + response = self.wafv2.create_rule_group(**req_obj) + except (BotoCoreError, ClientError) as e: + self.fail_json_aws(e, msg="Failed to create wafv2 rule group.") + + self.existing_group, self.id, self.locktoken = self.get_group() + + return self.existing_group + + +def main(): + + arg_spec = dict( + state=dict(type='str', required=True, choices=['present', 'absent']), + name=dict(type='str', required=True), + scope=dict(type='str', required=True, choices=['CLOUDFRONT', 'REGIONAL']), + capacity=dict(type='int'), + description=dict(type='str'), + rules=dict(type='list', elements='dict'), + sampled_requests=dict(type='bool', default=False), + cloudwatch_metrics=dict(type='bool', default=True), + metric_name=dict(type='str'), + tags=dict(type='dict'), + purge_rules=dict(default=True, type='bool') + ) + + module = AnsibleAWSModule( + argument_spec=arg_spec, + supports_check_mode=True, + required_if=[['state', 'present', ['capacity', 'rules']]] + ) + + state = module.params.get("state") + name = module.params.get("name") + scope = module.params.get("scope") + capacity = module.params.get("capacity") + description = module.params.get("description") + rules = module.params.get("rules") + sampled_requests = module.params.get("sampled_requests") + cloudwatch_metrics = module.params.get("cloudwatch_metrics") + metric_name = module.params.get("metric_name") + tags = module.params.get("tags") + purge_rules = module.params.get("purge_rules") + check_mode = module.check_mode + + if rules: + rules = [] + for rule in module.params.get("rules"): + rules.append(wafv2_snake_dict_to_camel_dict(snake_dict_to_camel_dict(rule, capitalize_first=True))) + + if not metric_name: + metric_name = name + + rule_group = RuleGroup(module.client('wafv2'), name, scope, module.fail_json_aws) + + change = False + retval = {} + + if state == 'present': + if rule_group.get(): + change, rules = compare_priority_rules(rule_group.get().get('RuleGroup').get('Rules'), rules, purge_rules, state) + change = change or rule_group.get().get('RuleGroup').get('Description') != description + + if change and not check_mode: + retval = rule_group.update( + description, + rules, + sampled_requests, + cloudwatch_metrics, + metric_name + ) + else: + retval = rule_group.get().get('RuleGroup') + + else: + change = True + if not check_mode: + retval = rule_group.create( + capacity, + description, + rules, + sampled_requests, + cloudwatch_metrics, + metric_name, + tags + ) + + elif state == 'absent': + if rule_group.get(): + if rules: + if len(rules) > 0: + change, rules = compare_priority_rules(rule_group.get().get('RuleGroup').get('Rules'), rules, purge_rules, state) + if change and not check_mode: + retval = rule_group.update( + description, + rules, + sampled_requests, + cloudwatch_metrics, + metric_name + ) + else: + change = True + if not check_mode: + retval = rule_group.remove() + + module.exit_json(changed=change, **camel_dict_to_snake_dict(retval)) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/wafv2_rule_group_info.py b/plugins/modules/wafv2_rule_group_info.py new file mode 100644 index 00000000000..afd399574ff --- /dev/null +++ b/plugins/modules/wafv2_rule_group_info.py @@ -0,0 +1,157 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: wafv2_rule_group_info +version_added: 1.5.0 +author: + - "Markus Bergholz (@markuman)" +short_description: wafv2_web_acl_info +description: + - Get informations about existing wafv2 rule groups. +requirements: + - boto3 + - botocore +options: + state: + description: + - Whether the rule is present or absent. + choices: ["present", "absent"] + required: true + type: str + name: + description: + - The name of the rule group. + required: true + type: str + scope: + description: + - Scope of wafv2 rule group. + required: true + choices: ["CLOUDFRONT","REGIONAL"] + type: str + +extends_documentation_fragment: +- amazon.aws.aws +- amazon.aws.ec2 + +''' + +EXAMPLES = ''' +- name: rule group info + community.aws.wafv2_rule_group_info: + name: test02 + state: present + scope: REGIONAL +''' + +RETURN = """ +arn: + description: Rule group arn + sample: arn:aws:wafv2:eu-central-1:11111111:regional/rulegroup/test02/6e90c01a-e4eb-43e5-b6aa-b1604cedf7d7 + type: str + returned: Always, as long as the web acl exists +description: + description: Description of the rule group + sample: Some rule group description + returned: Always, as long as the web acl exists + type: str +capacity: + description: Current capacity of the rule group + sample: 500 + returned: Always, as long as the rule group exists + type: int +name: + description: Rule group name + sample: test02 + returned: Always, as long as the rule group exists + type: str +rules: + description: Current rules of the rule group + returned: Always, as long as the rule group exists + type: list + sample: + - action: + allow: {} + name: eins + priority: 1 + statement: + ip_set_reference_statement: + arn: arn:aws:wafv2:eu-central-1:111111111:regional/ipset/test02/b6978915-c67b-4d1c-8832-2b1bb452143a + visibility_config: + cloud_watch_metrics_enabled: True + metric_name: fsd + sampled_requests_enabled: True +visibility_config: + description: Visibility config of the rule group + returned: Always, as long as the rule group exists + type: dict + sample: + cloud_watch_metrics_enabled: True + metric_name: blub + sampled_requests_enabled: False +""" +from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule, is_boto3_error_code, get_boto3_client_method_parameters +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict, ansible_dict_to_boto3_tag_list +from ansible_collections.community.aws.plugins.module_utils.wafv2 import wafv2_list_rule_groups + +try: + from botocore.exceptions import ClientError, BotoCoreError +except ImportError: + pass # caught by AnsibleAWSModule + + +def get_rule_group(wafv2, name, scope, id, fail_json_aws): + try: + response = wafv2.get_rule_group( + Name=name, + Scope=scope, + Id=id + ) + except (BotoCoreError, ClientError) as e: + fail_json_aws(e, msg="Failed to get wafv2 rule group.") + return response + + +def main(): + arg_spec = dict( + state=dict(type='str', required=True, choices=['present', 'absent']), + name=dict(type='str', required=True), + scope=dict(type='str', required=True, choices=['CLOUDFRONT', 'REGIONAL']) + ) + + module = AnsibleAWSModule( + argument_spec=arg_spec, + supports_check_mode=True + ) + + state = module.params.get("state") + name = module.params.get("name") + scope = module.params.get("scope") + + wafv2 = module.client('wafv2') + + # check if rule group exists + response = wafv2_list_rule_groups(wafv2, scope, module.fail_json_aws) + id = None + retval = {} + + for item in response.get('RuleGroups'): + if item.get('Name') == name: + id = item.get('Id') + + existing_group = None + if id: + existing_group = get_rule_group(wafv2, name, scope, id, module.fail_json_aws) + retval = camel_dict_to_snake_dict(existing_group.get('RuleGroup')) + + module.exit_json(**retval) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/wafv2_web_acl.py b/plugins/modules/wafv2_web_acl.py new file mode 100644 index 00000000000..ee70c65e54c --- /dev/null +++ b/plugins/modules/wafv2_web_acl.py @@ -0,0 +1,411 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: wafv2_web_acl +version_added: 1.5.0 +author: + - "Markus Bergholz (@markuman)" +short_description: wafv2_web_acl +description: + - Create, modify or delete a wafv2 web acl. +requirements: + - boto3 + - botocore +options: + state: + description: + - Whether the rule is present or absent. + choices: ["present", "absent"] + required: true + type: str + name: + description: + - The name of the web acl. + required: true + type: str + scope: + description: + - Scope of wafv2 web acl. + required: true + choices: ["CLOUDFRONT","REGIONAL"] + type: str + description: + description: + - Description of wafv2 web acl. + type: str + default_action: + description: + - Default action of the wafv2 web acl. + choices: ["Block","Allow"] + type: str + sampled_requests: + description: + - Whether to store a sample of the web requests, true or false. + type: bool + default: false + cloudwatch_metrics: + description: + - Enable cloudwatch metric for wafv2 web acl. + type: bool + default: true + metric_name: + description: + - Name of cloudwatch metrics. + - If not given and cloudwatch_metrics is enabled, the name of the web acl itself will be taken. + type: str + tags: + description: + - tags for wafv2 web acl. + type: dict + rules: + description: + - The Rule statements used to identify the web requests that you want to allow, block, or count. + type: list + elements: dict + suboptions: + name: + description: + - The name of the wafv2 rule + type: str + priority: + description: + - The rule priority + type: int + action: + description: + - Wether a rule is blocked, allowed or counted. + type: dict + visibility_config: + description: + - Visibility of single wafv2 rule. + type: dict + statement: + description: + - Rule configuration. + type: dict + purge_rules: + description: + - When set to C(no), keep the existing load balancer rules in place. Will modify and add, but will not delete. + default: yes + type: bool + +extends_documentation_fragment: +- amazon.aws.aws +- amazon.aws.ec2 + +''' + +EXAMPLES = ''' +- name: create 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 + 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 +''' + +RETURN = """ +arn: + description: web acl arn + sample: arn:aws:wafv2:eu-central-1:11111111:regional/webacl/test05/318c1ab9-fa74-4b3b-a974-f92e25106f61 + type: str + returned: Always, as long as the web acl exists +description: + description: Description of the web acl + sample: Some web acl description + returned: Always, as long as the web acl exists + type: str +capacity: + description: Current capacity of the web acl + sample: 140 + returned: Always, as long as the web acl exists + type: int +name: + description: Web acl name + sample: test02 + returned: Always, as long as the web acl exists + type: str +rules: + description: Current rules of the web acl + returned: Always, as long as the web acl exists + type: list + sample: + - name: admin_protect + override_action: + none: {} + priority: 1 + statement: + managed_rule_group_statement: + name: AWSManagedRulesAdminProtectionRuleSet + vendor_name: AWS + visibility_config: + cloud_watch_metrics_enabled: true + metric_name: admin_protect + sampled_requests_enabled: true +visibility_config: + description: Visibility config of the web acl + returned: Always, as long as the web acl exists + type: dict + sample: + cloud_watch_metrics_enabled: true + metric_name: blub + sampled_requests_enabled: false +""" +from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule, is_boto3_error_code, get_boto3_client_method_parameters +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import snake_dict_to_camel_dict, camel_dict_to_snake_dict, ansible_dict_to_boto3_tag_list +from ansible_collections.community.aws.plugins.module_utils.wafv2 import wafv2_list_web_acls, compare_priority_rules, wafv2_snake_dict_to_camel_dict + +try: + from botocore.exceptions import ClientError, BotoCoreError +except ImportError: + pass # caught by AnsibleAWSModule + + +class WebACL: + def __init__(self, wafv2, name, scope, fail_json_aws): + self.wafv2 = wafv2 + self.name = name + self.scope = scope + 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): + 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 + ) + except (BotoCoreError, ClientError) as e: + self.fail_json_aws(e, msg="Failed to update wafv2 web acl.") + return response + + def remove(self): + try: + response = self.wafv2.delete_web_acl( + Name=self.name, + Scope=self.scope, + Id=self.id, + LockToken=self.locktoken + ) + except (BotoCoreError, ClientError) as e: + self.fail_json_aws(e, msg="Failed to remove wafv2 web acl.") + return response + + def get(self): + if self.existing_acl: + return self.existing_acl + return None + + def get_web_acl(self): + id = None + locktoken = None + arn = None + existing_acl = None + response = self.list() + + for item in response.get('WebACLs'): + if item.get('Name') == self.name: + id = item.get('Id') + locktoken = item.get('LockToken') + arn = item.get('ARN') + + if id: + try: + existing_acl = self.wafv2.get_web_acl( + Name=self.name, + Scope=self.scope, + Id=id + ) + except (BotoCoreError, ClientError) as e: + self.fail_json_aws(e, msg="Failed to get wafv2 web acl.") + return existing_acl, id, locktoken + + 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): + req_obj = { + 'Name': self.name, + 'Scope': self.scope, + 'DefaultAction': default_action, + 'Rules': rules, + 'VisibilityConfig': { + 'SampledRequestsEnabled': sampled_requests, + 'CloudWatchMetricsEnabled': cloudwatch_metrics, + 'MetricName': metric_name + } + } + if description: + req_obj['Description'] = description + if tags: + req_obj['Tags'] = ansible_dict_to_boto3_tag_list(tags) + + try: + response = self.wafv2.create_web_acl(**req_obj) + except (BotoCoreError, ClientError) as e: + self.fail_json_aws(e, msg="Failed to create wafv2 web acl.") + + self.existing_acl, self.id, self.locktoken = self.get_web_acl() + return self.existing_acl + + +def main(): + + arg_spec = dict( + state=dict(type='str', required=True, choices=['present', 'absent']), + name=dict(type='str', required=True), + scope=dict(type='str', required=True, choices=['CLOUDFRONT', 'REGIONAL']), + description=dict(type='str'), + default_action=dict(type='str', choices=['Block', 'Allow']), + rules=dict(type='list', elements='dict'), + sampled_requests=dict(type='bool', default=False), + cloudwatch_metrics=dict(type='bool', default=True), + metric_name=dict(type='str'), + tags=dict(type='dict'), + purge_rules=dict(default=True, type='bool') + ) + + module = AnsibleAWSModule( + argument_spec=arg_spec, + supports_check_mode=True, + required_if=[['state', 'present', ['default_action', 'rules']]] + ) + + state = module.params.get("state") + name = module.params.get("name") + scope = module.params.get("scope") + description = module.params.get("description") + default_action = module.params.get("default_action") + rules = module.params.get("rules") + sampled_requests = module.params.get("sampled_requests") + cloudwatch_metrics = module.params.get("cloudwatch_metrics") + metric_name = module.params.get("metric_name") + tags = module.params.get("tags") + purge_rules = module.params.get("purge_rules") + check_mode = module.check_mode + + if default_action == 'Block': + default_action = {'Block': {}} + elif default_action == 'Allow': + default_action = {'Allow': {}} + + if rules: + rules = [] + for rule in module.params.get("rules"): + rules.append(wafv2_snake_dict_to_camel_dict(snake_dict_to_camel_dict(rule, capitalize_first=True))) + + if not metric_name: + metric_name = name + + web_acl = WebACL(module.client('wafv2'), name, scope, module.fail_json_aws) + change = False + retval = {} + + if state == 'present': + if web_acl.get(): + change, rules = compare_priority_rules(web_acl.get().get('WebACL').get('Rules'), rules, purge_rules, state) + change = change or web_acl.get().get('WebACL').get('Description') != description + change = change or web_acl.get().get('WebACL').get('DefaultAction') != default_action + + if change and not check_mode: + retval = web_acl.update( + default_action, + description, + rules, + sampled_requests, + cloudwatch_metrics, + metric_name + ) + + else: + retval = web_acl.get().get('WebACL') + + else: + change = True + if not check_mode: + retval = web_acl.create( + default_action, + rules, + sampled_requests, + cloudwatch_metrics, + metric_name, + tags, + description + ) + + elif state == 'absent': + if web_acl.get(): + if rules: + if len(rules) > 0: + change, rules = compare_priority_rules(web_acl.get().get('WebACL').get('Rules'), rules, purge_rules, state) + if change and not check_mode: + retval = web_acl.update( + default_action, + description, + rules, + sampled_requests, + cloudwatch_metrics, + metric_name + ) + else: + change = True + if not check_mode: + retval = web_acl.remove() + + module.exit_json(changed=change, **camel_dict_to_snake_dict(retval)) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/wafv2_web_acl_info.py b/plugins/modules/wafv2_web_acl_info.py new file mode 100644 index 00000000000..8b8c79ef75a --- /dev/null +++ b/plugins/modules/wafv2_web_acl_info.py @@ -0,0 +1,150 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: wafv2_web_acl_info +version_added: 1.5.0 +author: + - "Markus Bergholz (@markuman)" +short_description: wafv2_web_acl +description: + - Info about web acl +requirements: + - boto3 + - botocore +options: + name: + description: + - The name of the web acl. + required: true + type: str + scope: + description: + - Scope of wafv2 web acl. + required: true + choices: ["CLOUDFRONT","REGIONAL"] + type: str + +extends_documentation_fragment: +- amazon.aws.aws +- amazon.aws.ec2 + +''' + +EXAMPLES = ''' +- name: get web acl + community.aws.wafv2_web_acl_info: + name: test05 + scope: REGIONAL + register: out +''' + +RETURN = """ +arn: + description: web acl arn + sample: arn:aws:wafv2:eu-central-1:11111111:regional/webacl/test05/318c1ab9-fa74-4b3b-a974-f92e25106f61 + type: str + returned: Always, as long as the web acl exists +description: + description: Description of the web acl + sample: Some web acl description + returned: Always, as long as the web acl exists + type: str +capacity: + description: Current capacity of the web acl + sample: 140 + returned: Always, as long as the web acl exists + type: int +name: + description: Web acl name + sample: test02 + returned: Always, as long as the web acl exists + type: str +rules: + description: Current rules of the web acl + returned: Always, as long as the web acl exists + type: list + sample: + - name: admin_protect + override_action: + none: {} + priority: 1 + statement: + managed_rule_group_statement: + name: AWSManagedRulesAdminProtectionRuleSet + vendor_name: AWS + visibility_config: + cloud_watch_metrics_enabled: true + metric_name: admin_protect + sampled_requests_enabled: true +visibility_config: + description: Visibility config of the web acl + returned: Always, as long as the web acl exists + type: dict + sample: + cloud_watch_metrics_enabled: true + metric_name: blub + sampled_requests_enabled: false +""" +from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule, is_boto3_error_code, get_boto3_client_method_parameters +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict, ansible_dict_to_boto3_tag_list +from ansible_collections.community.aws.plugins.module_utils.wafv2 import wafv2_list_web_acls + +try: + from botocore.exceptions import ClientError, BotoCoreError +except ImportError: + pass # caught by AnsibleAWSModule + + +def get_web_acl(wafv2, name, scope, id, fail_json_aws): + try: + response = wafv2.get_web_acl( + Name=name, + Scope=scope, + Id=id + ) + except (BotoCoreError, ClientError) as e: + fail_json_aws(e, msg="Failed to get wafv2 web acl.") + return response + + +def main(): + + arg_spec = dict( + name=dict(type='str', required=True), + scope=dict(type='str', required=True, choices=['CLOUDFRONT', 'REGIONAL']) + ) + + module = AnsibleAWSModule( + argument_spec=arg_spec + ) + + state = module.params.get("state") + name = module.params.get("name") + scope = module.params.get("scope") + + wafv2 = module.client('wafv2') + # check if web acl exists + response = wafv2_list_web_acls(wafv2, scope, module.fail_json_aws) + + id = None + retval = {} + + for item in response.get('WebACLs'): + if item.get('Name') == name: + id = item.get('Id') + + if id: + existing_acl = get_web_acl(wafv2, name, scope, id, module.fail_json_aws) + retval = camel_dict_to_snake_dict(existing_acl.get('WebACL')) + + module.exit_json(**retval) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/wafv2/aliases b/tests/integration/targets/wafv2/aliases new file mode 100644 index 00000000000..b28081f9633 --- /dev/null +++ b/tests/integration/targets/wafv2/aliases @@ -0,0 +1,9 @@ +cloud/aws +shippable/aws/group1 + +wafv2_resources +wafv2_resources_info +wafv2_rule_group +wafv2_rule_group_info +wafv2_web_acl +wafv2_web_acl_info diff --git a/tests/integration/targets/wafv2/defaults/main.yml b/tests/integration/targets/wafv2/defaults/main.yml new file mode 100644 index 00000000000..a019a97eaab --- /dev/null +++ b/tests/integration/targets/wafv2/defaults/main.yml @@ -0,0 +1,11 @@ +--- +web_acl_name: '{{ resource_prefix }}-web-acl' +rule_group_name: '{{ resource_prefix }}-rule-group' +alb_name: "my-alb-{{ resource_prefix | regex_search('([0-9]+)$') }}" +tg_name: "my-tg-{{ resource_prefix | regex_search('([0-9]+)$') }}" +cidr: + main: 10.228.228.0/22 + a: 10.228.228.0/24 + b: 10.228.229.0/24 + c: 10.228.230.0/24 + d: 10.228.231.0/24 \ No newline at end of file diff --git a/tests/integration/targets/wafv2/tasks/alb.yml b/tests/integration/targets/wafv2/tasks/alb.yml new file mode 100644 index 00000000000..6ecb0abb01d --- /dev/null +++ b/tests/integration/targets/wafv2/tasks/alb.yml @@ -0,0 +1,106 @@ +############################################ +# create ALB and necessary dependencies +# to test wafv2_resources +############################################ +- name: create VPC + ec2_vpc_net: + cidr_block: "{{ cidr.main }}" + name: '{{ resource_prefix }}_vpc' + state: present + register: vpc + +- name: create internet gateway + ec2_vpc_igw: + vpc_id: '{{ vpc.vpc.id }}' + state: present + tags: + Name: '{{ resource_prefix }}' + register: igw + +- name: create public subnet + ec2_vpc_subnet: + cidr: '{{ item.cidr }}' + az: '{{ aws_region}}{{ item.az }}' + vpc_id: '{{ vpc.vpc.id }}' + state: present + tags: + Public: '{{ item.public|string }}' + Name: '{{ item.public|ternary(''public'', ''private'') }}-{{ item.az }}' + with_items: + - cidr: "{{ cidr.a }}" + az: a + public: 'True' + - cidr: "{{ cidr.b }}" + az: b + public: 'True' + - cidr: "{{ cidr.c }}" + az: a + public: 'False' + - cidr: "{{ cidr.d }}" + az: b + public: 'False' + register: subnets + +- ec2_vpc_subnet_info: + filters: + vpc-id: '{{ vpc.vpc.id }}' + register: vpc_subnets + +- name: create list of subnet ids + set_fact: + alb_subnets: "{{ (vpc_subnets.subnets| selectattr('tags.Public', 'equalto', 'True')| map(attribute='id')| list) }}" + private_subnets: "{{ (vpc_subnets.subnets| selectattr('tags.Public', 'equalto', 'False')| map(attribute='id')| list) }}" + +- name: create a route table + ec2_vpc_route_table: + vpc_id: '{{ vpc.vpc.id }}' + tags: + Name: igw-route + Created: '{{ resource_prefix }}' + subnets: '{{ alb_subnets + private_subnets }}' + routes: + - dest: 0.0.0.0/0 + gateway_id: '{{ igw.gateway_id }}' + register: route_table + +- ec2_group: + name: '{{ resource_prefix }}' + description: security group for Ansible ALB integration tests + state: present + vpc_id: '{{ vpc.vpc.id }}' + rules: + - proto: tcp + from_port: 1 + to_port: 65535 + cidr_ip: 0.0.0.0/0 + register: sec_group + +- name: create a target group for testing + elb_target_group: + name: '{{ tg_name }}' + protocol: http + port: 80 + vpc_id: '{{ vpc.vpc.id }}' + state: present + register: tg + +- name: create ALB with a listener + elb_application_lb: + name: "{{ alb_name }}" + subnets: "{{ alb_subnets }}" + security_groups: "{{ sec_group.group_id }}" + state: present + wait: yes + listeners: + - Protocol: HTTP + Port: 80 + DefaultActions: + - Type: forward + TargetGroupName: "{{ tg_name }}" + register: alb + +- 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 new file mode 100644 index 00000000000..8b195ab570c --- /dev/null +++ b/tests/integration/targets/wafv2/tasks/create_webacl.yml @@ -0,0 +1,151 @@ +####################### +## 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: 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: 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: 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: verify create + assert: + that: + - out is not changed \ No newline at end of file diff --git a/tests/integration/targets/wafv2/tasks/main.yml b/tests/integration/targets/wafv2/tasks/main.yml new file mode 100644 index 00000000000..fa6e7fb3d86 --- /dev/null +++ b/tests/integration/targets/wafv2/tasks/main.yml @@ -0,0 +1,210 @@ +--- +- module_defaults: + group/aws: + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_token: "{{ security_token | default(omit) }}" + region: "{{ aws_region }}" + + block: + - include_tasks: alb.yml + - include_tasks: create_webacl.yml + - include_tasks: rule_group.yml + - include_tasks: test_webacl.yml + - include_tasks: waf_resources.yml + + ############################## + # test delete wafv2 resources + ############################## + - name: remove rule group + wafv2_rule_group: + name: "{{ rule_group_name }}" + state: absent + scope: REGIONAL + register: out + + - name: verify change + assert: + that: + - out is changed + + - name: immutable remove rule group + wafv2_rule_group: + name: "{{ rule_group_name }}" + state: absent + scope: REGIONAL + register: out + + - name: verify no change + assert: + that: + - out is not changed + + - name: delete web acl + wafv2_web_acl: + name: "{{ web_acl_name }}" + state: absent + scope: REGIONAL + register: out + + - name: verify change + assert: + that: + - out is changed + + + - name: immutable delete web acl + wafv2_web_acl: + name: "{{ web_acl_name }}" + state: absent + scope: REGIONAL + register: out + + - name: verify not change + assert: + that: + - out is not changed + + always: + ################################### + # always delete wafv2 components + ################################### + - name: remove test alb from waf web acs + wafv2_resources: + name: "{{ web_acl_name }}" + scope: REGIONAL + state: absent + arn: "{{ alb.load_balancer_arn }}" + ignore_errors: true + + - name: always delete web acl + wafv2_web_acl: + name: "{{ web_acl_name }}" + state: absent + scope: REGIONAL + ignore_errors: true + + - name: take care rule group is removed + wafv2_rule_group: + name: "{{ rule_group_name }}" + state: absent + scope: REGIONAL + ignore_errors: true + + ######################### + # remove alb and its deps + ######################### + - name: destroy ALB + elb_application_lb: + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + region: '{{ aws_region }}' + name: '{{ alb_name }}' + state: absent + wait: true + wait_timeout: 600 + ignore_errors: true + + - name: destroy target group if it was created + elb_target_group: + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + region: '{{ aws_region }}' + name: '{{ tg_name }}' + protocol: http + port: 80 + vpc_id: '{{ vpc.vpc.id }}' + state: absent + wait: true + wait_timeout: 600 + register: remove_tg + retries: 5 + delay: 3 + until: remove_tg is success + when: tg is defined + ignore_errors: true + + - name: destroy sec group + ec2_group: + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + region: '{{ aws_region }}' + name: '{{ sec_group.group_name }}' + description: security group for Ansible ALB integration tests + state: absent + vpc_id: '{{ vpc.vpc.id }}' + register: remove_sg + retries: 10 + delay: 5 + until: remove_sg is success + ignore_errors: true + + - name: remove route table + ec2_vpc_route_table: + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + region: '{{ aws_region }}' + vpc_id: '{{ vpc.vpc.id }}' + route_table_id: '{{ route_table.route_table.route_table_id }}' + lookup: id + state: absent + register: remove_rt + retries: 10 + delay: 5 + until: remove_rt is success + ignore_errors: true + + - name: destroy subnets + ec2_vpc_subnet: + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + region: '{{ aws_region }}' + cidr: '{{ item.cidr }}' + vpc_id: '{{ vpc.vpc.id }}' + state: absent + register: remove_subnet + retries: 10 + delay: 5 + until: remove_subnet is success + with_items: + - cidr: 10.228.228.0/24 + - cidr: 10.228.229.0/24 + - cidr: 10.228.230.0/24 + - cidr: 10.228.231.0/24 + ignore_errors: true + + - name: destroy internet gateway + ec2_vpc_igw: + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + region: '{{ aws_region }}' + vpc_id: '{{ vpc.vpc.id }}' + tags: + Name: '{{ resource_prefix }}' + state: absent + register: remove_igw + retries: 10 + delay: 5 + until: remove_igw is success + ignore_errors: true + + - name: destroy VPC + ec2_vpc_net: + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + region: '{{ aws_region }}' + cidr_block: 10.228.228.0/22 + name: '{{ resource_prefix }}_vpc' + state: absent + register: remove_vpc + retries: 10 + delay: 5 + until: remove_vpc is success + ignore_errors: true diff --git a/tests/integration/targets/wafv2/tasks/rule_group.yml b/tests/integration/targets/wafv2/tasks/rule_group.yml new file mode 100644 index 00000000000..bb2b12303f5 --- /dev/null +++ b/tests/integration/targets/wafv2/tasks/rule_group.yml @@ -0,0 +1,681 @@ +#################################### +# Create and test rule group +#################################### +- name: check_mode create rule group + wafv2_rule_group: + name: "{{ rule_group_name }}" + state: present + description: hallo eins + scope: REGIONAL + capacity: 500 + rules: + - name: eins + priority: 1 + action: + allow: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: fsd + statement: + byte_match_statement: + search_string: ansible.com + positional_constraint: CONTAINS + field_to_match: + single_header: + name: host + text_transformations: + - type: LOWERCASE + priority: 0 + cloudwatch_metrics: yes + tags: + A: B + C: D + register: out + check_mode: yes + +- name: check_mode verify create + assert: + that: + - out is changed + +- name: create rule group + wafv2_rule_group: + name: "{{ rule_group_name }}" + state: present + description: hallo eins + scope: REGIONAL + capacity: 500 + rules: + - name: eins + priority: 1 + action: + allow: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: fsd + statement: + byte_match_statement: + search_string: ansible.com + positional_constraint: CONTAINS + field_to_match: + single_header: + name: host + text_transformations: + - type: LOWERCASE + priority: 0 + cloudwatch_metrics: yes + tags: + A: B + C: D + register: out + +- name: verify create + assert: + that: + - out is changed + +- name: rule group info + wafv2_rule_group_info: + name: "{{ rule_group_name }}" + state: present + scope: REGIONAL + register: out + +- name: verify one rule + assert: + that: + - out.rules | count == 1 + +- name: immutable create rule group + wafv2_rule_group: + name: "{{ rule_group_name }}" + state: present + description: hallo eins + scope: REGIONAL + capacity: 500 + rules: + - name: eins + priority: 1 + action: + allow: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: fsd + statement: + byte_match_statement: + search_string: ansible.com + positional_constraint: CONTAINS + field_to_match: + single_header: + name: host + text_transformations: + - type: LOWERCASE + priority: 0 + cloudwatch_metrics: yes + tags: + A: B + C: D + register: out + +- name: verify immutable create + assert: + that: + - out is not changed + +- name: change description + wafv2_rule_group: + name: "{{ rule_group_name }}" + state: present + description: hallo eins zwei + scope: REGIONAL + capacity: 500 + rules: + - name: eins + priority: 1 + action: + allow: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: fsd + statement: + byte_match_statement: + search_string: ansible.com + positional_constraint: CONTAINS + field_to_match: + single_header: + name: host + text_transformations: + - type: LOWERCASE + priority: 0 + cloudwatch_metrics: yes + tags: + A: B + C: D + register: out + +- name: verify change + assert: + that: + - out is changed + +- name: iummutable change description + wafv2_rule_group: + name: "{{ rule_group_name }}" + state: present + description: hallo eins zwei + scope: REGIONAL + capacity: 500 + rules: + - name: eins + priority: 1 + action: + allow: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: fsd + statement: + byte_match_statement: + search_string: ansible.com + positional_constraint: CONTAINS + field_to_match: + single_header: + name: host + text_transformations: + - type: LOWERCASE + priority: 0 + cloudwatch_metrics: yes + tags: + A: B + C: D + register: out + +- name: verify no change + assert: + that: + - out is not changed + +- name: add rule + wafv2_rule_group: + name: "{{ rule_group_name }}" + state: present + description: hallo eins zwei + scope: REGIONAL + capacity: 500 + rules: + - name: eins + priority: 1 + action: + allow: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: fsd + statement: + byte_match_statement: + search_string: ansible.com + positional_constraint: CONTAINS + field_to_match: + single_header: + name: host + text_transformations: + - type: LOWERCASE + priority: 0 + - name: zwei + priority: 2 + action: + block: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: ddos + statement: + or_statement: + statements: + - byte_match_statement: + search_string: ansible.com + positional_constraint: CONTAINS + field_to_match: + single_header: + name: host + text_transformations: + - type: LOWERCASE + priority: 0 + - xss_match_statement: + field_to_match: + body: {} + text_transformations: + - type: NONE + priority: 0 + cloudwatch_metrics: yes + tags: + A: B + C: D + register: out + +- name: verify change + assert: + that: + - out is changed + +- name: immutable add rule + wafv2_rule_group: + name: "{{ rule_group_name }}" + state: present + description: hallo eins zwei + scope: REGIONAL + capacity: 500 + rules: + - name: eins + priority: 1 + action: + allow: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: fsd + statement: + byte_match_statement: + search_string: ansible.com + positional_constraint: CONTAINS + field_to_match: + single_header: + name: host + text_transformations: + - type: LOWERCASE + priority: 0 + - name: zwei + priority: 2 + action: + block: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: ddos + statement: + or_statement: + statements: + - byte_match_statement: + search_string: ansible.com + positional_constraint: CONTAINS + field_to_match: + single_header: + name: host + text_transformations: + - type: LOWERCASE + priority: 0 + - xss_match_statement: + field_to_match: + body: {} + text_transformations: + - type: NONE + priority: 0 + cloudwatch_metrics: yes + tags: + A: B + C: D + register: out + +- name: verify no change + assert: + that: + - out is not changed + +- name: change rule + wafv2_rule_group: + name: "{{ rule_group_name }}" + state: present + description: hallo eins zwei + scope: REGIONAL + capacity: 500 + rules: + - name: eins + priority: 1 + action: + allow: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: fsd + statement: + byte_match_statement: + search_string: ansible.com + positional_constraint: CONTAINS + field_to_match: + single_header: + name: host + text_transformations: + - type: LOWERCASE + priority: 0 + - name: zwei + priority: 3 + action: + block: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: ddos + statement: + or_statement: + statements: + - byte_match_statement: + search_string: ansible.com + positional_constraint: CONTAINS + field_to_match: + single_header: + name: host + text_transformations: + - type: LOWERCASE + priority: 0 + - xss_match_statement: + field_to_match: + body: {} + text_transformations: + - type: NONE + priority: 0 + cloudwatch_metrics: yes + tags: + A: B + C: D + register: out + +- name: verify change + assert: + that: + - out is changed + +- name: immutable change rule + wafv2_rule_group: + name: "{{ rule_group_name }}" + state: present + description: hallo eins zwei + scope: REGIONAL + capacity: 500 + rules: + - name: eins + priority: 1 + action: + allow: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: fsd + statement: + byte_match_statement: + search_string: ansible.com + positional_constraint: CONTAINS + field_to_match: + single_header: + name: host + text_transformations: + - type: LOWERCASE + priority: 0 + - name: zwei + priority: 3 + action: + block: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: ddos + statement: + or_statement: + statements: + - byte_match_statement: + search_string: ansible.com + positional_constraint: CONTAINS + field_to_match: + single_header: + name: host + text_transformations: + - type: LOWERCASE + priority: 0 + - xss_match_statement: + field_to_match: + body: {} + text_transformations: + - type: NONE + priority: 0 + cloudwatch_metrics: yes + tags: + A: B + C: D + register: out + +- name: verify no change + assert: + that: + - out is not changed + +- name: change rule again + wafv2_rule_group: + name: "{{ rule_group_name }}" + state: present + description: hallo eins zwei + scope: REGIONAL + capacity: 500 + rules: + - name: zwei + priority: 1 + action: + block: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: ddos + statement: + or_statement: + statements: + - byte_match_statement: + search_string: ansible.com + positional_constraint: CONTAINS + field_to_match: + single_header: + name: host + text_transformations: + - type: LOWERCASE + priority: 0 + - xss_match_statement: + field_to_match: + body: {} + text_transformations: + - type: NONE + priority: 0 + - name: eins + priority: 2 + action: + allow: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: fsd + statement: + byte_match_statement: + search_string: ansible.com + positional_constraint: CONTAINS + field_to_match: + single_header: + name: host + text_transformations: + - type: LOWERCASE + priority: 0 + cloudwatch_metrics: yes + tags: + A: B + C: D + register: out + +- name: verify change + assert: + that: + - out is changed + +- name: add one rule + wafv2_rule_group: + name: "{{ rule_group_name }}" + state: present + description: hallo eins zwei + scope: REGIONAL + capacity: 500 + purge_rules: no + rules: + - name: allow-admin-svg + priority: 3 + action: + allow: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: allow-admin-svg + statement: + byte_match_statement: + search_string: admin.svg + positional_constraint: CONTAINS + field_to_match: + uri_path: {} + text_transformations: + - type: LOWERCASE + priority: 0 + cloudwatch_metrics: yes + tags: + A: B + C: D + register: out + +- name: verify change + assert: + that: + - out is changed + +- name: rule group info + wafv2_rule_group_info: + name: "{{ rule_group_name }}" + state: present + scope: REGIONAL + register: out + +- name: verify create + assert: + that: + - out.rules | count == 3 + +- name: immutable add one rule + wafv2_rule_group: + name: "{{ rule_group_name }}" + state: present + description: hallo eins zwei + scope: REGIONAL + capacity: 500 + purge_rules: no + rules: + - name: allow-admin-svg + priority: 3 + action: + allow: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: allow-admin-svg + statement: + byte_match_statement: + search_string: admin.svg + positional_constraint: CONTAINS + field_to_match: + uri_path: {} + text_transformations: + - type: LOWERCASE + priority: 0 + cloudwatch_metrics: yes + tags: + A: B + C: D + register: out + +- name: verify no change + assert: + that: + - out is not changed + +- name: purge rules + wafv2_rule_group: + name: "{{ rule_group_name }}" + state: present + description: hallo eins zwei + scope: REGIONAL + capacity: 500 + purge_rules: yes + rules: + - name: allow-admin-svg + priority: 3 + action: + allow: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: allow-admin-svg + statement: + byte_match_statement: + search_string: admin.svg + positional_constraint: CONTAINS + field_to_match: + uri_path: {} + text_transformations: + - type: LOWERCASE + priority: 0 + cloudwatch_metrics: yes + tags: + A: B + C: D + register: out + +- name: verify change + assert: + that: + - out is changed + +- name: absent one rule rules + wafv2_rule_group: + name: "{{ rule_group_name }}" + state: absent + description: hallo eins zwei + scope: REGIONAL + rules: + - name: allow-admin-svg + priority: 3 + action: + allow: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: allow-admin-svg + statement: + byte_match_statement: + search_string: admin.svg + positional_constraint: CONTAINS + field_to_match: + uri_path: {} + text_transformations: + - type: LOWERCASE + priority: 0 + register: out + +- name: verify change + assert: + that: + - out is changed + +- name: rule group info + wafv2_rule_group_info: + name: "{{ rule_group_name }}" + state: present + scope: REGIONAL + register: out + +- name: verify change + assert: + that: + - out.rules | count == 0 diff --git a/tests/integration/targets/wafv2/tasks/test_webacl.yml b/tests/integration/targets/wafv2/tasks/test_webacl.yml new file mode 100644 index 00000000000..2749450ab26 --- /dev/null +++ b/tests/integration/targets/wafv2/tasks/test_webacl.yml @@ -0,0 +1,184 @@ +############################### +# test web acl +############################### +- name: get web acl + wafv2_web_acl_info: + name: "{{ web_acl_name }}" + scope: REGIONAL + register: out + +- name: verify rules + assert: + that: + - out.rules | count == 2 + +- name: change web acl description + wafv2_web_acl: + name: "{{ web_acl_name }}" + state: present + description: hallo eins drei + 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: verify change + assert: + that: + - out is changed + + +- name: add 1 rules + wafv2_web_acl: + name: "{{ web_acl_name }}" + state: present + description: hallo eins drei + scope: REGIONAL + default_action: Allow + sampled_requests: no + cloudwatch_metrics: yes + metric_name: blub + purge_rules: no + rules: + - name: bla + priority: 8 + 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: verify change + assert: + that: + - out is changed + +- name: get web acl + wafv2_web_acl_info: + name: "{{ web_acl_name }}" + scope: REGIONAL + register: out + +- name: verify rules + assert: + that: + - out.rules | count == 3 + +- name: reduce rules to 1 + wafv2_web_acl: + name: "{{ web_acl_name }}" + state: present + description: hallo eins drei + scope: REGIONAL + default_action: Allow + sampled_requests: no + cloudwatch_metrics: yes + metric_name: blub + rules: + - name: admin_protect + priority: 1 + override_action: + none: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: admin_protect + statement: + managed_rule_group_statement: + vendor_name: AWS + name: AWSManagedRulesAdminProtectionRuleSet + tags: + A: B + C: D + register: out + +- name: verify change + assert: + that: + - out is changed + +- name: get web acl + wafv2_web_acl_info: + name: "{{ web_acl_name }}" + scope: REGIONAL + register: out + +- name: verify rules + assert: + that: + - out.rules | count == 1 + +- name: immutable change web acl + wafv2_web_acl: + name: "{{ web_acl_name }}" + state: present + description: hallo eins drei + scope: REGIONAL + default_action: Allow + sampled_requests: no + cloudwatch_metrics: yes + metric_name: blub + rules: + - name: admin_protect + priority: 1 + override_action: + none: {} + visibility_config: + sampled_requests_enabled: yes + cloud_watch_metrics_enabled: yes + metric_name: admin_protect + statement: + managed_rule_group_statement: + vendor_name: AWS + name: AWSManagedRulesAdminProtectionRuleSet + tags: + A: B + C: D + register: out + +- name: verify no change + assert: + that: + - out is not changed diff --git a/tests/integration/targets/wafv2/tasks/waf_resources.yml b/tests/integration/targets/wafv2/tasks/waf_resources.yml new file mode 100644 index 00000000000..75cb2d29a6b --- /dev/null +++ b/tests/integration/targets/wafv2/tasks/waf_resources.yml @@ -0,0 +1,59 @@ +################################ +# test wafv2 resouces +# bind ALB to WAFv2 ACL +################################ +- name: check_mode add test alb to test waf + wafv2_resources: + name: "{{ web_acl_name }}" + scope: REGIONAL + state: present + arn: "{{ alb.load_balancer_arn }}" + check_mode: yes + register: out + +- name: verify check_mode create + assert: + that: + - out is changed + +- name: add test alb to test waf + wafv2_resources: + name: "{{ web_acl_name }}" + scope: REGIONAL + state: present + arn: "{{ alb.load_balancer_arn }}" + register: out + retries: 10 + delay: 15 + until: out.failed == false + +- name: verify create + assert: + that: + - out is changed + +- name: get web acl + wafv2_resources_info: + name: "{{ web_acl_name }}" + scope: REGIONAL + register: out + +- name: immutable add test alb from test waf + wafv2_resources: + name: "{{ web_acl_name }}" + scope: REGIONAL + state: present + arn: "{{ alb.load_balancer_arn }}" + register: out + +- name: verify immutable create + assert: + that: + - out is not changed + +- name: remove test alb from test waf + wafv2_resources: + name: "{{ web_acl_name }}" + scope: REGIONAL + state: absent + arn: "{{ alb.load_balancer_arn }}"