From 9ccf6d09fc9afa650bb469d7f0fcdb128fa7eb4c Mon Sep 17 00:00:00 2001 From: Markus Bergholz Date: Mon, 1 Mar 2021 14:36:42 +0100 Subject: [PATCH 01/11] add wafv2 modules --- meta/runtime.yml | 6 + plugins/module_utils/wafv2.py | 119 ++ plugins/modules/wafv2_resources.py | 166 ++ plugins/modules/wafv2_resources_info.py | 122 ++ plugins/modules/wafv2_rule_group.py | 407 +++++ plugins/modules/wafv2_rule_group_info.py | 155 ++ plugins/modules/wafv2_web_acl.py | 364 +++++ plugins/modules/wafv2_web_acl_info.py | 134 ++ tests/integration/targets/wafv2/aliases | 9 + .../targets/wafv2/defaults/main.yml | 6 + .../integration/targets/wafv2/tasks/main.yml | 1433 +++++++++++++++++ 11 files changed, 2921 insertions(+) create mode 100644 plugins/module_utils/wafv2.py create mode 100644 plugins/modules/wafv2_resources.py create mode 100644 plugins/modules/wafv2_resources_info.py create mode 100644 plugins/modules/wafv2_rule_group.py create mode 100644 plugins/modules/wafv2_rule_group_info.py create mode 100644 plugins/modules/wafv2_web_acl.py create mode 100644 plugins/modules/wafv2_web_acl_info.py create mode 100644 tests/integration/targets/wafv2/aliases create mode 100644 tests/integration/targets/wafv2/defaults/main.yml create mode 100644 tests/integration/targets/wafv2/tasks/main.yml 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..4429b9c6b57 --- /dev/null +++ b/plugins/module_utils/wafv2.py @@ -0,0 +1,119 @@ +def wafv2_list_web_acls(wafv2, scope, Nextmarker=None): + # there is currently no paginator for wafv2 + req_obj = { + 'Scope': scope, + 'Limit': 100 + } + if Nextmarker: + req_obj['NextMarker'] = Nextmarker + response = wafv2.list_web_acls(**req_obj) + if response.get('NextMarker'): + response['WebACLs'] += wafv2_list_web_acls(wafv2, scope, Nextmarker=response.get('NextMarker')).get('WebACLs') + return response + +def wafv2_list_rule_groups(wafv2, scope, Nextmarker=None): + req_obj = { + 'Scope': scope, + 'Limit': 100 + } + if Nextmarker: + req_obj['NextMarker'] = Nextmarker + response = wafv2.list_rule_groups(**req_obj) + if response.get('NextMarker'): + response['RuleGroups'] += wafv2_list_rule_groups(wafv2, scope, 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 nestes_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] = nestes_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 \ No newline at end of file diff --git a/plugins/modules/wafv2_resources.py b/plugins/modules/wafv2_resources.py new file mode 100644 index 00000000000..36017114cd3 --- /dev/null +++ b/plugins/modules/wafv2_resources.py @@ -0,0 +1,166 @@ +#!/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: + - Create, modify and delete CloudWatch log group metric filter. + - CloudWatch log group metric filter can be use with M(community.aws.ec2_metric_alarm). +requirements: + - boto3 + - botocore +options: + state: + description: + - Whether the rule is present or absent. + choices: ["present", "absent"] + required: true + type: str + name: + description: + - The name 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, WaiterError +except ImportError: + pass # caught by AnsibleAWSModule + + +def get_web_acl(wafv2, name, scope, id): + response = wafv2.get_web_acl( + Name=name, + Scope=scope, + Id=id + ) + return response + + +def list_wafv2_resources(wafv2, arn): + response = wafv2.list_resources_for_web_acl( + WebACLArn=arn + ) + return response + + +def add_wafv2_resources(wafv2, waf_arn, arn): + response = wafv2.associate_web_acl( + WebACLArn=waf_arn, + ResourceArn=arn + ) + return response + + +def remove_resources(wafv2, arn): + response = wafv2.disassociate_web_acl( + ResourceArn=arn + ) + 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) + + 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) + waf_arn = existing_acl.get('WebACL').get('ARN') + + retval = list_wafv2_resources(wafv2, waf_arn) + + 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) + + elif state == 'absent': + if retval: + if arn in retval.get('ResourceArns'): + change = True + if not check_mode: + retval = remove_resources(wafv2, arn) + + 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..b039c4feff3 --- /dev/null +++ b/plugins/modules/wafv2_resources_info.py @@ -0,0 +1,122 @@ +#!/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 + +try: + from botocore.exceptions import ClientError, BotoCoreError, WaiterError +except ImportError: + pass # caught by AnsibleAWSModule + + +def get_web_acl(wafv2, name, scope, id): + response = wafv2.get_web_acl( + Name=name, + Scope=scope, + Id=id + ) + return response + + +def list_web_acls(wafv2, scope): + response = wafv2.list_web_acls( + Scope=scope, + Limit=100 + ) + return response + + +def list_wafv2_resources(wafv2, arn): + response = wafv2.list_resources_for_web_acl( + WebACLArn=arn + ) + 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) + + 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) + arn = existing_acl.get('WebACL').get('ARN') + + retval = camel_dict_to_snake_dict(list_wafv2_resources(wafv2, arn)) + + 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..6f87bb0488a --- /dev/null +++ b/plugins/modules/wafv2_rule_group.py @@ -0,0 +1,407 @@ +#!/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 CloudWatch log group metric filter. + - CloudWatch log group metric filter can be use with M(community.aws.ec2_metric_alarm). +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, WaiterError +except ImportError: + pass # caught by AnsibleAWSModule + + +class RuleGroup: + def __init__(self, wafv2, name, scope): + self.wafv2 = wafv2 + self.name = name + self.scope = scope + 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 + + response = self.wafv2.update_rule_group(**req_obj) + 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: + existing_group = self.wafv2.get_rule_group( + Name=self.name, + Scope=self.scope, + Id=id + ) + + return existing_group, id, locktoken + + def list(self): + return wafv2_list_rule_groups(self.wafv2, self.scope) + + def get(self): + if self.existing_group: + return self.existing_group + return None + + def remove(self): + response = self.wafv2.delete_rule_group( + Name=self.name, + Scope=self.scope, + Id=self.id, + LockToken=self.locktoken + ) + 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) + + response = self.wafv2.create_rule_group(**req_obj) + 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) + + 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..c8192108e6b --- /dev/null +++ b/plugins/modules/wafv2_rule_group_info.py @@ -0,0 +1,155 @@ +#!/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: + - Create, modify and delete CloudWatch log group metric filter. + - CloudWatch log group metric filter can be use with M(community.aws.ec2_metric_alarm). +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, WaiterError +except ImportError: + pass # caught by AnsibleAWSModule + + +def get_rule_group(wafv2, name, scope, id): + response = wafv2.get_rule_group( + Name=name, + Scope=scope, + Id=id + ) + 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) + 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) + 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..43d9f8adb86 --- /dev/null +++ b/plugins/modules/wafv2_web_acl.py @@ -0,0 +1,364 @@ +#!/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: + - Sampled 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 + 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 +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, WaiterError +except ImportError: + pass # caught by AnsibleAWSModule + + +class WebACL: + def __init__(self, wafv2, name, scope): + self.wafv2 = wafv2 + self.name = name + self.scope = scope + self.existing_acl, self.id, self.locktoken = self.get_web_acl() + + def update(self, default_action, description, rules, sampled_requests, cloudwatch_metrics, metric_name): + 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 + ) + return response + + def remove(self): + response = self.wafv2.delete_web_acl( + Name=self.name, + Scope=self.scope, + Id=self.id, + LockToken=self.locktoken + ) + 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: + existing_acl = self.wafv2.get_web_acl( + Name=self.name, + Scope=self.scope, + Id=id + ) + return existing_acl, id, locktoken + + def list(self): + return wafv2_list_web_acls(self.wafv2, self.scope) + + 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) + + response = self.wafv2.create_web_acl(**req_obj) + 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) + 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..1493947b259 --- /dev/null +++ b/plugins/modules/wafv2_web_acl_info.py @@ -0,0 +1,134 @@ +#!/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 +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, WaiterError +except ImportError: + pass # caught by AnsibleAWSModule + + +def get_web_acl(wafv2, name, scope, id): + response = wafv2.get_web_acl( + Name=name, + Scope=scope, + Id=id + ) + 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) + + 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) + 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..4e9af2d9951 --- /dev/null +++ b/tests/integration/targets/wafv2/defaults/main.yml @@ -0,0 +1,6 @@ +--- +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]+)$') }}" + diff --git a/tests/integration/targets/wafv2/tasks/main.yml b/tests/integration/targets/wafv2/tasks/main.yml new file mode 100644 index 00000000000..36fc60f7150 --- /dev/null +++ b/tests/integration/targets/wafv2/tasks/main.yml @@ -0,0 +1,1433 @@ +--- +- 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: + ############################################ + # create ALB and necessary dependencies + # to test wafv2_resources + ############################################ + - name: create 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: present + register: vpc + + - name: create 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 }}' + state: present + tags: + Name: '{{ resource_prefix }}' + register: igw + + - name: create public subnet + 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 }}' + 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: 10.228.228.0/24 + az: a + public: 'True' + - cidr: 10.228.229.0/24 + az: b + public: 'True' + - cidr: 10.228.230.0/24 + az: a + public: 'False' + - cidr: 10.228.231.0/24 + az: b + public: 'False' + register: subnets + + - ec2_vpc_subnet_info: + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + region: '{{ aws_region }}' + filters: + vpc-id: '{{ vpc.vpc.id }}' + register: vpc_subnets + + - name: create list of subnet ids + set_fact: + alb_subnets: '{{ vpc_subnets|community.general.json_query(''subnets[?tags.Public == `True`].id'') }}' + private_subnets: '{{ vpc_subnets|community.general.json_query(''subnets[?tags.Public != `True`].id'') }}' + + - name: create a 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 }}' + 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: + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + region: '{{ aws_region }}' + 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: + 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: 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 + 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 + + - name: test idempotence creating ALB with a listener + elb_application_lb: + name: "{{ alb_name }}" + subnets: "{{ alb_subnets }}" + security_groups: "{{ sec_group.group_id }}" + state: present + listeners: + - Protocol: HTTP + Port: 80 + DefaultActions: + - Type: forward + TargetGroupName: "{{ tg_name }}" + register: alb + + - assert: + that: + - not alb.changed + - alb.listeners|length == 1 + - alb.listeners[0].rules|length == 1 + + ####################### + ## 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 + + - 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 + + #################################### + # 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 + + ############################### + # 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 + + ################################ + # 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: 5 + 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 }}" + + ############################## + # 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 From c8d4665573b09048947c2a5e1ca9bd80c6d4ab42 Mon Sep 17 00:00:00 2001 From: Markus Bergholz Date: Mon, 1 Mar 2021 15:06:38 +0100 Subject: [PATCH 02/11] sanity fix and fragment integrationtest --- .../targets/wafv2/defaults/main.yml | 3 +- tests/integration/targets/wafv2/tasks/alb.yml | 153 ++ .../targets/wafv2/tasks/create_webacl.yml | 147 ++ .../integration/targets/wafv2/tasks/main.yml | 1233 +---------------- .../targets/wafv2/tasks/rule_group.yml | 681 +++++++++ .../targets/wafv2/tasks/test_webacl.yml | 184 +++ .../targets/wafv2/tasks/waf_resources.yml | 59 + 7 files changed, 1230 insertions(+), 1230 deletions(-) create mode 100644 tests/integration/targets/wafv2/tasks/alb.yml create mode 100644 tests/integration/targets/wafv2/tasks/create_webacl.yml create mode 100644 tests/integration/targets/wafv2/tasks/rule_group.yml create mode 100644 tests/integration/targets/wafv2/tasks/test_webacl.yml create mode 100644 tests/integration/targets/wafv2/tasks/waf_resources.yml diff --git a/tests/integration/targets/wafv2/defaults/main.yml b/tests/integration/targets/wafv2/defaults/main.yml index 4e9af2d9951..c03b6328ad1 100644 --- a/tests/integration/targets/wafv2/defaults/main.yml +++ b/tests/integration/targets/wafv2/defaults/main.yml @@ -2,5 +2,4 @@ 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]+)$') }}" - +tg_name: "my-tg-{{ resource_prefix | regex_search('([0-9]+)$') }}" \ 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..3105e5e0fa8 --- /dev/null +++ b/tests/integration/targets/wafv2/tasks/alb.yml @@ -0,0 +1,153 @@ +############################################ +# create ALB and necessary dependencies +# to test wafv2_resources +############################################ +- name: create 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: present + register: vpc + +- name: create 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 }}' + state: present + tags: + Name: '{{ resource_prefix }}' + register: igw + +- name: create public subnet + 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 }}' + 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: 10.228.228.0/24 + az: a + public: 'True' + - cidr: 10.228.229.0/24 + az: b + public: 'True' + - cidr: 10.228.230.0/24 + az: a + public: 'False' + - cidr: 10.228.231.0/24 + az: b + public: 'False' + register: subnets + +- ec2_vpc_subnet_info: + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + region: '{{ aws_region }}' + filters: + vpc-id: '{{ vpc.vpc.id }}' + register: vpc_subnets + +- name: create list of subnet ids + set_fact: + alb_subnets: '{{ vpc_subnets|community.general.json_query(''subnets[?tags.Public == `True`].id'') }}' + private_subnets: '{{ vpc_subnets|community.general.json_query(''subnets[?tags.Public != `True`].id'') }}' + +- name: create a 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 }}' + 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: + aws_access_key: '{{ aws_access_key }}' + aws_secret_key: '{{ aws_secret_key }}' + security_token: '{{ security_token }}' + region: '{{ aws_region }}' + 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: + 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: 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 + 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 + +- name: test idempotence creating ALB with a listener + elb_application_lb: + name: "{{ alb_name }}" + subnets: "{{ alb_subnets }}" + security_groups: "{{ sec_group.group_id }}" + state: present + listeners: + - Protocol: HTTP + Port: 80 + DefaultActions: + - Type: forward + TargetGroupName: "{{ tg_name }}" + register: alb + +- assert: + that: + - not 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..96f5e96bc75 --- /dev/null +++ b/tests/integration/targets/wafv2/tasks/create_webacl.yml @@ -0,0 +1,147 @@ +####################### +## 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 + +- 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 index 36fc60f7150..fa6e7fb3d86 100644 --- a/tests/integration/targets/wafv2/tasks/main.yml +++ b/tests/integration/targets/wafv2/tasks/main.yml @@ -7,1234 +7,11 @@ region: "{{ aws_region }}" block: - ############################################ - # create ALB and necessary dependencies - # to test wafv2_resources - ############################################ - - name: create 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: present - register: vpc - - - name: create 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 }}' - state: present - tags: - Name: '{{ resource_prefix }}' - register: igw - - - name: create public subnet - 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 }}' - 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: 10.228.228.0/24 - az: a - public: 'True' - - cidr: 10.228.229.0/24 - az: b - public: 'True' - - cidr: 10.228.230.0/24 - az: a - public: 'False' - - cidr: 10.228.231.0/24 - az: b - public: 'False' - register: subnets - - - ec2_vpc_subnet_info: - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' - region: '{{ aws_region }}' - filters: - vpc-id: '{{ vpc.vpc.id }}' - register: vpc_subnets - - - name: create list of subnet ids - set_fact: - alb_subnets: '{{ vpc_subnets|community.general.json_query(''subnets[?tags.Public == `True`].id'') }}' - private_subnets: '{{ vpc_subnets|community.general.json_query(''subnets[?tags.Public != `True`].id'') }}' - - - name: create a 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 }}' - 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: - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' - region: '{{ aws_region }}' - 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: - 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: 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 - 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 - - - name: test idempotence creating ALB with a listener - elb_application_lb: - name: "{{ alb_name }}" - subnets: "{{ alb_subnets }}" - security_groups: "{{ sec_group.group_id }}" - state: present - listeners: - - Protocol: HTTP - Port: 80 - DefaultActions: - - Type: forward - TargetGroupName: "{{ tg_name }}" - register: alb - - - assert: - that: - - not alb.changed - - alb.listeners|length == 1 - - alb.listeners[0].rules|length == 1 - - ####################### - ## 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 - - - 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 - - #################################### - # 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 - - ############################### - # 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 - - ################################ - # 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: 5 - 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 }}" + - 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 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..5c53018c04d --- /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: 5 + 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 }}" From 7f02218f1da8c5388a2b95fdfda0c0b336bac28c Mon Sep 17 00:00:00 2001 From: Markus Bergholz Date: Thu, 18 Mar 2021 09:39:42 +0100 Subject: [PATCH 03/11] fix linting --- plugins/module_utils/wafv2.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/wafv2.py b/plugins/module_utils/wafv2.py index 4429b9c6b57..f5f3981140a 100644 --- a/plugins/module_utils/wafv2.py +++ b/plugins/module_utils/wafv2.py @@ -11,6 +11,7 @@ def wafv2_list_web_acls(wafv2, scope, Nextmarker=None): response['WebACLs'] += wafv2_list_web_acls(wafv2, scope, Nextmarker=response.get('NextMarker')).get('WebACLs') return response + def wafv2_list_rule_groups(wafv2, scope, Nextmarker=None): req_obj = { 'Scope': scope, @@ -23,6 +24,7 @@ def wafv2_list_rule_groups(wafv2, scope, Nextmarker=None): response['RuleGroups'] += wafv2_list_rule_groups(wafv2, scope, Nextmarker=response.get('NextMarker')).get('RuleGroups') return response + def wafv2_snake_dict_to_camel_dict(a): retval = {} for item in a.keys(): @@ -45,6 +47,7 @@ def wafv2_snake_dict_to_camel_dict(a): retval[item] = a.get(item) return retval + def nestes_byte_values_to_strings(rule, keyname): """ currently valid nested byte values in statements array are @@ -55,15 +58,18 @@ def nestes_byte_values_to_strings(rule, keyname): 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') + 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') - + 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): @@ -71,6 +77,7 @@ def byte_values_to_strings_before_compare(rules): 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']) @@ -116,4 +123,4 @@ def compare_priority_rules(existing_rules, requested_rules, purge_rules, state): else: merged_rules = existing_rules - return diff, merged_rules \ No newline at end of file + return diff, merged_rules From aa22c32ef9575911220423cd1dcdb416cc7bb0b3 Mon Sep 17 00:00:00 2001 From: Markus Bergholz Date: Thu, 18 Mar 2021 11:16:41 +0100 Subject: [PATCH 04/11] fix sanity --- plugins/module_utils/wafv2.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/module_utils/wafv2.py b/plugins/module_utils/wafv2.py index f5f3981140a..a599605a3c6 100644 --- a/plugins/module_utils/wafv2.py +++ b/plugins/module_utils/wafv2.py @@ -1,3 +1,7 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + def wafv2_list_web_acls(wafv2, scope, Nextmarker=None): # there is currently no paginator for wafv2 req_obj = { From 9dc70d0ab46a6ba0edf11126533bd9718c973d45 Mon Sep 17 00:00:00 2001 From: Markus Bergholz Date: Thu, 8 Apr 2021 11:56:11 +0200 Subject: [PATCH 05/11] add exceptions --- plugins/module_utils/wafv2.py | 23 ++++++-- plugins/modules/wafv2_resources.py | 65 +++++++++++--------- plugins/modules/wafv2_resources_info.py | 47 +++++++++------ plugins/modules/wafv2_rule_group.py | 51 ++++++++++------ plugins/modules/wafv2_rule_group_info.py | 24 ++++---- plugins/modules/wafv2_web_acl.py | 75 ++++++++++++++---------- plugins/modules/wafv2_web_acl_info.py | 21 ++++--- 7 files changed, 184 insertions(+), 122 deletions(-) diff --git a/plugins/module_utils/wafv2.py b/plugins/module_utils/wafv2.py index a599605a3c6..bae1be85acd 100644 --- a/plugins/module_utils/wafv2.py +++ b/plugins/module_utils/wafv2.py @@ -2,7 +2,7 @@ __metaclass__ = type -def wafv2_list_web_acls(wafv2, scope, Nextmarker=None): +def wafv2_list_web_acls(wafv2, scope, fail_json_aws, Nextmarker=None): # there is currently no paginator for wafv2 req_obj = { 'Scope': scope, @@ -10,22 +10,33 @@ def wafv2_list_web_acls(wafv2, scope, Nextmarker=None): } if Nextmarker: req_obj['NextMarker'] = Nextmarker - response = wafv2.list_web_acls(**req_obj) + + 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, Nextmarker=response.get('NextMarker')).get('WebACLs') + 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, Nextmarker=None): +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 - response = wafv2.list_rule_groups(**req_obj) + + 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, Nextmarker=response.get('NextMarker')).get('RuleGroups') + response['RuleGroups'] += wafv2_list_rule_groups(wafv2, scope, fail_json_aws, Nextmarker=response.get('NextMarker')).get('RuleGroups') return response diff --git a/plugins/modules/wafv2_resources.py b/plugins/modules/wafv2_resources.py index 36017114cd3..38757c1a18b 100644 --- a/plugins/modules/wafv2_resources.py +++ b/plugins/modules/wafv2_resources.py @@ -13,8 +13,7 @@ - "Markus Bergholz (@markuman)" short_description: wafv2_web_acl description: - - Create, modify and delete CloudWatch log group metric filter. - - CloudWatch log group metric filter can be use with M(community.aws.ec2_metric_alarm). + - Apply or remove wafv2 to other aws resources. requirements: - boto3 - botocore @@ -68,39 +67,51 @@ from ansible_collections.community.aws.plugins.module_utils.wafv2 import wafv2_list_web_acls try: - from botocore.exceptions import ClientError, BotoCoreError, WaiterError + from botocore.exceptions import ClientError, BotoCoreError except ImportError: pass # caught by AnsibleAWSModule -def get_web_acl(wafv2, name, scope, id): - response = wafv2.get_web_acl( - Name=name, - Scope=scope, - Id=id - ) +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): - response = wafv2.list_resources_for_web_acl( - WebACLArn=arn - ) +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): - response = wafv2.associate_web_acl( - WebACLArn=waf_arn, - ResourceArn=arn - ) +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): - response = wafv2.disassociate_web_acl( - ResourceArn=arn - ) +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 @@ -129,7 +140,7 @@ def main(): # check if web acl exists - response = wafv2_list_web_acls(wafv2, scope) + response = wafv2_list_web_acls(wafv2, scope, module.fail_json_aws) id = None retval = {} @@ -140,24 +151,24 @@ def main(): id = item.get('Id') if id: - existing_acl = get_web_acl(wafv2, name, scope, 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) + 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) + 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) + retval = remove_resources(wafv2, arn, module.fail_json_aws) module.exit_json(changed=change, **camel_dict_to_snake_dict(retval)) diff --git a/plugins/modules/wafv2_resources_info.py b/plugins/modules/wafv2_resources_info.py index b039c4feff3..2829cfa1829 100644 --- a/plugins/modules/wafv2_resources_info.py +++ b/plugins/modules/wafv2_resources_info.py @@ -55,32 +55,41 @@ from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict, ansible_dict_to_boto3_tag_list try: - from botocore.exceptions import ClientError, BotoCoreError, WaiterError + from botocore.exceptions import ClientError, BotoCoreError except ImportError: pass # caught by AnsibleAWSModule -def get_web_acl(wafv2, name, scope, id): - response = wafv2.get_web_acl( - Name=name, - Scope=scope, - Id=id - ) +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): - response = wafv2.list_web_acls( - Scope=scope, - Limit=100 - ) +def list_web_acls(wafv2, scope, fail_json_aws): + try: + response = wafv2.list_web_acls( + Scope=scope, + Limit=100 + ) + except (BotoCoreError, ClientError) as e: + fail_json_aws(e, msg="Failed to list wafv2 web acl.") return response -def list_wafv2_resources(wafv2, arn): - response = wafv2.list_resources_for_web_acl( - WebACLArn=arn - ) +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 @@ -100,7 +109,7 @@ def main(): wafv2 = module.client('wafv2') # check if web acl exists - response = list_web_acls(wafv2, scope) + response = list_web_acls(wafv2, scope, module.fail_json_aws) id = None retval = {} @@ -110,10 +119,10 @@ def main(): id = item.get('Id') if id: - existing_acl = get_web_acl(wafv2, name, scope, 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)) + retval = camel_dict_to_snake_dict(list_wafv2_resources(wafv2, arn, module.fail_json_aws)) module.exit_json(**retval) diff --git a/plugins/modules/wafv2_rule_group.py b/plugins/modules/wafv2_rule_group.py index 6f87bb0488a..d66d4864bb5 100644 --- a/plugins/modules/wafv2_rule_group.py +++ b/plugins/modules/wafv2_rule_group.py @@ -13,8 +13,7 @@ - "Markus Bergholz (@markuman)" short_description: wafv2_web_acl description: - - Create, modify and delete CloudWatch log group metric filter. - - CloudWatch log group metric filter can be use with M(community.aws.ec2_metric_alarm). + - Create, modify and delete wafv2 rule groups. requirements: - boto3 - botocore @@ -209,16 +208,17 @@ 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, WaiterError + from botocore.exceptions import ClientError, BotoCoreError except ImportError: pass # caught by AnsibleAWSModule class RuleGroup: - def __init__(self, wafv2, name, scope): + 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): @@ -238,7 +238,10 @@ def update(self, description, rules, sampled_requests, cloudwatch_metrics, metri if description: req_obj['Description'] = description - response = self.wafv2.update_rule_group(**req_obj) + 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): @@ -255,16 +258,19 @@ def get_group(self): existing_group = None if id: - existing_group = self.wafv2.get_rule_group( - Name=self.name, - Scope=self.scope, - Id=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) + return wafv2_list_rule_groups(self.wafv2, self.scope, self.fail_json_aws) def get(self): if self.existing_group: @@ -272,12 +278,15 @@ def get(self): return None def remove(self): - response = self.wafv2.delete_rule_group( - Name=self.name, - Scope=self.scope, - Id=self.id, - LockToken=self.locktoken - ) + 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): @@ -299,7 +308,11 @@ def create(self, capacity, description, rules, sampled_requests, cloudwatch_metr if tags: req_obj['Tags'] = ansible_dict_to_boto3_tag_list(tags) - response = self.wafv2.create_rule_group(**req_obj) + 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 @@ -348,7 +361,7 @@ def main(): if not metric_name: metric_name = name - rule_group = RuleGroup(module.client('wafv2'), name, scope) + rule_group = RuleGroup(module.client('wafv2'), name, scope, module.fail_json_aws) change = False retval = {} diff --git a/plugins/modules/wafv2_rule_group_info.py b/plugins/modules/wafv2_rule_group_info.py index c8192108e6b..afd399574ff 100644 --- a/plugins/modules/wafv2_rule_group_info.py +++ b/plugins/modules/wafv2_rule_group_info.py @@ -13,8 +13,7 @@ - "Markus Bergholz (@markuman)" short_description: wafv2_web_acl_info description: - - Create, modify and delete CloudWatch log group metric filter. - - CloudWatch log group metric filter can be use with M(community.aws.ec2_metric_alarm). + - Get informations about existing wafv2 rule groups. requirements: - boto3 - botocore @@ -102,17 +101,20 @@ from ansible_collections.community.aws.plugins.module_utils.wafv2 import wafv2_list_rule_groups try: - from botocore.exceptions import ClientError, BotoCoreError, WaiterError + from botocore.exceptions import ClientError, BotoCoreError except ImportError: pass # caught by AnsibleAWSModule -def get_rule_group(wafv2, name, scope, id): - response = wafv2.get_rule_group( - Name=name, - Scope=scope, - Id=id - ) +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 @@ -135,7 +137,7 @@ def main(): wafv2 = module.client('wafv2') # check if rule group exists - response = wafv2_list_rule_groups(wafv2, scope) + response = wafv2_list_rule_groups(wafv2, scope, module.fail_json_aws) id = None retval = {} @@ -145,7 +147,7 @@ def main(): existing_group = None if id: - existing_group = get_rule_group(wafv2, name, scope, 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) diff --git a/plugins/modules/wafv2_web_acl.py b/plugins/modules/wafv2_web_acl.py index 43d9f8adb86..cc2aba98a8a 100644 --- a/plugins/modules/wafv2_web_acl.py +++ b/plugins/modules/wafv2_web_acl.py @@ -164,42 +164,49 @@ 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, WaiterError + from botocore.exceptions import ClientError, BotoCoreError except ImportError: pass # caught by AnsibleAWSModule class WebACL: - def __init__(self, wafv2, name, scope): + 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): - 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 - ) + 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): - response = self.wafv2.delete_web_acl( - Name=self.name, - Scope=self.scope, - Id=self.id, - LockToken=self.locktoken - ) + 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): @@ -221,15 +228,18 @@ def get_web_acl(self): arn = item.get('ARN') if id: - existing_acl = self.wafv2.get_web_acl( - Name=self.name, - Scope=self.scope, - Id=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) + 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 = { @@ -248,9 +258,12 @@ def create(self, default_action, rules, sampled_requests, cloudwatch_metrics, me if tags: req_obj['Tags'] = ansible_dict_to_boto3_tag_list(tags) - response = self.wafv2.create_web_acl(**req_obj) - self.existing_acl, self.id, self.locktoken = self.get_web_acl() + 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 @@ -302,7 +315,7 @@ def main(): if not metric_name: metric_name = name - web_acl = WebACL(module.client('wafv2'), name, scope) + web_acl = WebACL(module.client('wafv2'), name, scope, module.fail_json_aws) change = False retval = {} diff --git a/plugins/modules/wafv2_web_acl_info.py b/plugins/modules/wafv2_web_acl_info.py index 1493947b259..9368853017c 100644 --- a/plugins/modules/wafv2_web_acl_info.py +++ b/plugins/modules/wafv2_web_acl_info.py @@ -83,17 +83,20 @@ from ansible_collections.community.aws.plugins.module_utils.wafv2 import wafv2_list_web_acls try: - from botocore.exceptions import ClientError, BotoCoreError, WaiterError + from botocore.exceptions import ClientError, BotoCoreError except ImportError: pass # caught by AnsibleAWSModule -def get_web_acl(wafv2, name, scope, id): - response = wafv2.get_web_acl( - Name=name, - Scope=scope, - Id=id - ) +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 @@ -114,7 +117,7 @@ def main(): wafv2 = module.client('wafv2') # check if web acl exists - response = wafv2_list_web_acls(wafv2, scope) + response = wafv2_list_web_acls(wafv2, scope, module.fail_json_aws) id = None retval = {} @@ -124,7 +127,7 @@ def main(): id = item.get('Id') if id: - existing_acl = get_web_acl(wafv2, name, scope, 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) From 58daed7e91e4fb0c0898a23369c39da5bf4b08ce Mon Sep 17 00:00:00 2001 From: Markus Bergholz Date: Thu, 8 Apr 2021 13:39:05 +0200 Subject: [PATCH 06/11] add botocore exceptions --- plugins/module_utils/wafv2.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/module_utils/wafv2.py b/plugins/module_utils/wafv2.py index bae1be85acd..a951cd811b2 100644 --- a/plugins/module_utils/wafv2.py +++ b/plugins/module_utils/wafv2.py @@ -1,6 +1,11 @@ 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 From ce72329db0373eeb0e264b8eaedd8d1aedd2fd42 Mon Sep 17 00:00:00 2001 From: Markus Bergholz Date: Thu, 8 Apr 2021 15:22:13 +0200 Subject: [PATCH 07/11] incr retries --- tests/integration/targets/wafv2/tasks/waf_resources.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/targets/wafv2/tasks/waf_resources.yml b/tests/integration/targets/wafv2/tasks/waf_resources.yml index 5c53018c04d..75cb2d29a6b 100644 --- a/tests/integration/targets/wafv2/tasks/waf_resources.yml +++ b/tests/integration/targets/wafv2/tasks/waf_resources.yml @@ -23,7 +23,7 @@ state: present arn: "{{ alb.load_balancer_arn }}" register: out - retries: 5 + retries: 10 delay: 15 until: out.failed == false From 0a759c2033fa3d0336046de8e0ad630395a1be1c Mon Sep 17 00:00:00 2001 From: Markus Bergholz Date: Thu, 8 Apr 2021 21:48:54 +0200 Subject: [PATCH 08/11] wait for alb --- tests/integration/targets/wafv2/tasks/alb.yml | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/tests/integration/targets/wafv2/tasks/alb.yml b/tests/integration/targets/wafv2/tasks/alb.yml index 3105e5e0fa8..009fbfe709e 100644 --- a/tests/integration/targets/wafv2/tasks/alb.yml +++ b/tests/integration/targets/wafv2/tasks/alb.yml @@ -118,6 +118,7 @@ subnets: "{{ alb_subnets }}" security_groups: "{{ sec_group.group_id }}" state: present + wait: yes listeners: - Protocol: HTTP Port: 80 @@ -131,23 +132,3 @@ - alb.changed - alb.listeners|length == 1 - alb.listeners[0].rules|length == 1 - -- name: test idempotence creating ALB with a listener - elb_application_lb: - name: "{{ alb_name }}" - subnets: "{{ alb_subnets }}" - security_groups: "{{ sec_group.group_id }}" - state: present - listeners: - - Protocol: HTTP - Port: 80 - DefaultActions: - - Type: forward - TargetGroupName: "{{ tg_name }}" - register: alb - -- assert: - that: - - not alb.changed - - alb.listeners|length == 1 - - alb.listeners[0].rules|length == 1 From 2d5790f967d8b24bf39f12ba2c16701930b60987 Mon Sep 17 00:00:00 2001 From: Markus Bergholz Date: Tue, 13 Apr 2021 10:57:11 +0200 Subject: [PATCH 09/11] fix function name, fix missing NextMarker usage --- plugins/module_utils/wafv2.py | 4 ++-- plugins/modules/wafv2_resources.py | 2 +- plugins/modules/wafv2_resources_info.py | 10 ++-------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/plugins/module_utils/wafv2.py b/plugins/module_utils/wafv2.py index a951cd811b2..8fd58bed2c8 100644 --- a/plugins/module_utils/wafv2.py +++ b/plugins/module_utils/wafv2.py @@ -68,7 +68,7 @@ def wafv2_snake_dict_to_camel_dict(a): return retval -def nestes_byte_values_to_strings(rule, keyname): +def nested_byte_values_to_strings(rule, keyname): """ currently valid nested byte values in statements array are - OrStatement @@ -93,7 +93,7 @@ def byte_values_to_strings_before_compare(rules): else: for statement in ['AndStatement', 'OrStatement', 'NotStatement']: if rules[idx].get('Statement', {}).get(statement): - rules[idx] = nestes_byte_values_to_strings(rules[idx], statement) + rules[idx] = nested_byte_values_to_strings(rules[idx], statement) return rules diff --git a/plugins/modules/wafv2_resources.py b/plugins/modules/wafv2_resources.py index 38757c1a18b..4bf5f1dcca3 100644 --- a/plugins/modules/wafv2_resources.py +++ b/plugins/modules/wafv2_resources.py @@ -26,7 +26,7 @@ type: str name: description: - - The name web acl. + - The name of the web acl. type: str scope: description: diff --git a/plugins/modules/wafv2_resources_info.py b/plugins/modules/wafv2_resources_info.py index 2829cfa1829..469fc3b7184 100644 --- a/plugins/modules/wafv2_resources_info.py +++ b/plugins/modules/wafv2_resources_info.py @@ -53,6 +53,7 @@ """ 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 @@ -73,14 +74,7 @@ def get_web_acl(wafv2, name, scope, id, fail_json_aws): def list_web_acls(wafv2, scope, fail_json_aws): - try: - response = wafv2.list_web_acls( - Scope=scope, - Limit=100 - ) - except (BotoCoreError, ClientError) as e: - fail_json_aws(e, msg="Failed to list wafv2 web acl.") - return response + return wafv2_list_web_acls(wafv2, scope, fail_json_aws) def list_wafv2_resources(wafv2, arn, fail_json_aws): From f0c6c9efcda08716449a46e3f8ce30b9c84aad12 Mon Sep 17 00:00:00 2001 From: Markus Bergholz Date: Fri, 16 Apr 2021 11:02:13 +0200 Subject: [PATCH 10/11] fix documentation and integrationtest --- plugins/modules/wafv2_web_acl.py | 36 +++++++++++++++- plugins/modules/wafv2_web_acl_info.py | 13 ++++++ .../targets/wafv2/defaults/main.yml | 8 +++- tests/integration/targets/wafv2/tasks/alb.yml | 42 ++++--------------- .../targets/wafv2/tasks/create_webacl.yml | 4 ++ 5 files changed, 66 insertions(+), 37 deletions(-) diff --git a/plugins/modules/wafv2_web_acl.py b/plugins/modules/wafv2_web_acl.py index cc2aba98a8a..ee70c65e54c 100644 --- a/plugins/modules/wafv2_web_acl.py +++ b/plugins/modules/wafv2_web_acl.py @@ -46,7 +46,7 @@ type: str sampled_requests: description: - - Sampled requests, true or false. + - Whether to store a sample of the web requests, true or false. type: bool default: false cloudwatch_metrics: @@ -68,6 +68,27 @@ - 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. @@ -150,6 +171,19 @@ 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 diff --git a/plugins/modules/wafv2_web_acl_info.py b/plugins/modules/wafv2_web_acl_info.py index 9368853017c..8b8c79ef75a 100644 --- a/plugins/modules/wafv2_web_acl_info.py +++ b/plugins/modules/wafv2_web_acl_info.py @@ -69,6 +69,19 @@ 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 diff --git a/tests/integration/targets/wafv2/defaults/main.yml b/tests/integration/targets/wafv2/defaults/main.yml index c03b6328ad1..a019a97eaab 100644 --- a/tests/integration/targets/wafv2/defaults/main.yml +++ b/tests/integration/targets/wafv2/defaults/main.yml @@ -2,4 +2,10 @@ 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]+)$') }}" \ No newline at end of file +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 index 009fbfe709e..6ecb0abb01d 100644 --- a/tests/integration/targets/wafv2/tasks/alb.yml +++ b/tests/integration/targets/wafv2/tasks/alb.yml @@ -4,21 +4,13 @@ ############################################ - name: create 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 + cidr_block: "{{ cidr.main }}" name: '{{ resource_prefix }}_vpc' state: present register: vpc - name: create 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 }}' state: present tags: @@ -27,10 +19,6 @@ - name: create public subnet 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 }}' az: '{{ aws_region}}{{ item.az }}' vpc_id: '{{ vpc.vpc.id }}' @@ -39,40 +27,32 @@ Public: '{{ item.public|string }}' Name: '{{ item.public|ternary(''public'', ''private'') }}-{{ item.az }}' with_items: - - cidr: 10.228.228.0/24 + - cidr: "{{ cidr.a }}" az: a public: 'True' - - cidr: 10.228.229.0/24 + - cidr: "{{ cidr.b }}" az: b public: 'True' - - cidr: 10.228.230.0/24 + - cidr: "{{ cidr.c }}" az: a public: 'False' - - cidr: 10.228.231.0/24 + - cidr: "{{ cidr.d }}" az: b public: 'False' register: subnets - ec2_vpc_subnet_info: - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' - region: '{{ aws_region }}' filters: vpc-id: '{{ vpc.vpc.id }}' register: vpc_subnets - name: create list of subnet ids set_fact: - alb_subnets: '{{ vpc_subnets|community.general.json_query(''subnets[?tags.Public == `True`].id'') }}' - private_subnets: '{{ vpc_subnets|community.general.json_query(''subnets[?tags.Public != `True`].id'') }}' + 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: - 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: igw-route @@ -84,10 +64,6 @@ register: route_table - ec2_group: - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' - region: '{{ aws_region }}' name: '{{ resource_prefix }}' description: security group for Ansible ALB integration tests state: present @@ -101,10 +77,6 @@ - name: create a target group for testing 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 diff --git a/tests/integration/targets/wafv2/tasks/create_webacl.yml b/tests/integration/targets/wafv2/tasks/create_webacl.yml index 96f5e96bc75..8b195ab570c 100644 --- a/tests/integration/targets/wafv2/tasks/create_webacl.yml +++ b/tests/integration/targets/wafv2/tasks/create_webacl.yml @@ -97,6 +97,10 @@ 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: From 2cdc7237a807f204ed3481c634d192fc294c614b Mon Sep 17 00:00:00 2001 From: Markus Bergholz Date: Mon, 19 Apr 2021 19:19:39 +0200 Subject: [PATCH 11/11] lowercase nextmarker --- plugins/module_utils/wafv2.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/module_utils/wafv2.py b/plugins/module_utils/wafv2.py index 8fd58bed2c8..7b1b63d6fcf 100644 --- a/plugins/module_utils/wafv2.py +++ b/plugins/module_utils/wafv2.py @@ -7,14 +7,14 @@ pass # caught by AnsibleAWSModule -def wafv2_list_web_acls(wafv2, scope, fail_json_aws, Nextmarker=None): +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 + if nextmarker: + req_obj['NextMarker'] = nextmarker try: response = wafv2.list_web_acls(**req_obj) @@ -22,18 +22,18 @@ def wafv2_list_web_acls(wafv2, scope, fail_json_aws, Nextmarker=None): 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') + 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): +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 + if nextmarker: + req_obj['NextMarker'] = nextmarker try: response = wafv2.list_rule_groups(**req_obj) @@ -41,7 +41,7 @@ def wafv2_list_rule_groups(wafv2, scope, fail_json_aws, Nextmarker=None): 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') + response['RuleGroups'] += wafv2_list_rule_groups(wafv2, scope, fail_json_aws, nextmarker=response.get('NextMarker')).get('RuleGroups') return response