Skip to content

Commit

Permalink
Merge pull request #450 from markuman/wafv2
Browse files Browse the repository at this point in the history
add wafv2 modules

Reviewed-by: https://github.com/apps/ansible-zuul
  • Loading branch information
ansible-zuul[bot] authored Apr 21, 2021
2 parents dbf3697 + 2cdc723 commit 2ee7d6f
Show file tree
Hide file tree
Showing 16 changed files with 3,003 additions and 0 deletions.
6 changes: 6 additions & 0 deletions meta/runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
146 changes: 146 additions & 0 deletions plugins/module_utils/wafv2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

try:
from botocore.exceptions import ClientError, BotoCoreError
except ImportError:
pass # caught by AnsibleAWSModule


def wafv2_list_web_acls(wafv2, scope, fail_json_aws, nextmarker=None):
# there is currently no paginator for wafv2
req_obj = {
'Scope': scope,
'Limit': 100
}
if nextmarker:
req_obj['NextMarker'] = nextmarker

try:
response = wafv2.list_web_acls(**req_obj)
except (BotoCoreError, ClientError) as e:
fail_json_aws(e, msg="Failed to list wafv2 web acl.")

if response.get('NextMarker'):
response['WebACLs'] += wafv2_list_web_acls(wafv2, scope, fail_json_aws, nextmarker=response.get('NextMarker')).get('WebACLs')
return response


def wafv2_list_rule_groups(wafv2, scope, fail_json_aws, nextmarker=None):
# there is currently no paginator for wafv2
req_obj = {
'Scope': scope,
'Limit': 100
}
if nextmarker:
req_obj['NextMarker'] = nextmarker

try:
response = wafv2.list_rule_groups(**req_obj)
except (BotoCoreError, ClientError) as e:
fail_json_aws(e, msg="Failed to list wafv2 rule group.")

if response.get('NextMarker'):
response['RuleGroups'] += wafv2_list_rule_groups(wafv2, scope, fail_json_aws, nextmarker=response.get('NextMarker')).get('RuleGroups')
return response


def wafv2_snake_dict_to_camel_dict(a):
retval = {}
for item in a.keys():
if isinstance(a.get(item), dict):
if 'Ip' in item:
retval[item.replace('Ip', 'IP')] = wafv2_snake_dict_to_camel_dict(a.get(item))
elif 'Arn' == item:
retval['ARN'] = wafv2_snake_dict_to_camel_dict(a.get(item))
else:
retval[item] = wafv2_snake_dict_to_camel_dict(a.get(item))
elif isinstance(a.get(item), list):
retval[item] = []
for idx in range(len(a.get(item))):
retval[item].append(wafv2_snake_dict_to_camel_dict(a.get(item)[idx]))
elif 'Ip' in item:
retval[item.replace('Ip', 'IP')] = a.get(item)
elif 'Arn' == item:
retval['ARN'] = a.get(item)
else:
retval[item] = a.get(item)
return retval


def nested_byte_values_to_strings(rule, keyname):
"""
currently valid nested byte values in statements array are
- OrStatement
- AndStatement
- NotStatement
"""
if rule.get('Statement', {}).get(keyname):
for idx in range(len(rule.get('Statement', {}).get(keyname, {}).get('Statements'))):
if rule['Statement'][keyname]['Statements'][idx].get('ByteMatchStatement'):
rule['Statement'][keyname]['Statements'][idx]['ByteMatchStatement']['SearchString'] = \
rule.get('Statement').get(keyname).get('Statements')[idx].get('ByteMatchStatement').get('SearchString').decode('utf-8')

return rule


def byte_values_to_strings_before_compare(rules):
for idx in range(len(rules)):
if rules[idx].get('Statement', {}).get('ByteMatchStatement', {}).get('SearchString'):
rules[idx]['Statement']['ByteMatchStatement']['SearchString'] = \
rules[idx].get('Statement').get('ByteMatchStatement').get('SearchString').decode('utf-8')

else:
for statement in ['AndStatement', 'OrStatement', 'NotStatement']:
if rules[idx].get('Statement', {}).get(statement):
rules[idx] = nested_byte_values_to_strings(rules[idx], statement)

return rules


def compare_priority_rules(existing_rules, requested_rules, purge_rules, state):
diff = False
existing_rules = sorted(existing_rules, key=lambda k: k['Priority'])
existing_rules = byte_values_to_strings_before_compare(existing_rules)
requested_rules = sorted(requested_rules, key=lambda k: k['Priority'])

if purge_rules and state == 'present':
merged_rules = requested_rules
if len(existing_rules) == len(requested_rules):
for idx in range(len(existing_rules)):
if existing_rules[idx] != requested_rules[idx]:
diff = True
break
else:
diff = True

else:
# find same priority rules
# * pop same priority rule from existing rule
# * compare existing rule
merged_rules = []
ex_idx_pop = []
for existing_idx in range(len(existing_rules)):
for requested_idx in range(len(requested_rules)):
if existing_rules[existing_idx].get('Priority') == requested_rules[requested_idx].get('Priority'):
if state == 'present':
ex_idx_pop.append(existing_idx)
if existing_rules[existing_idx] != requested_rules[requested_idx]:
diff = True
elif existing_rules[existing_idx] == requested_rules[requested_idx]:
ex_idx_pop.append(existing_idx)
diff = True

prev_count = len(existing_rules)
for idx in ex_idx_pop:
existing_rules.pop(idx)

if state == 'present':
merged_rules = existing_rules + requested_rules

if len(merged_rules) != prev_count:
diff = True
else:
merged_rules = existing_rules

return diff, merged_rules
177 changes: 177 additions & 0 deletions plugins/modules/wafv2_resources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/usr/bin/python
# Copyright: Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type


DOCUMENTATION = '''
---
module: wafv2_resources
version_added: 1.5.0
author:
- "Markus Bergholz (@markuman)"
short_description: wafv2_web_acl
description:
- Apply or remove wafv2 to other aws resources.
requirements:
- boto3
- botocore
options:
state:
description:
- Whether the rule is present or absent.
choices: ["present", "absent"]
required: true
type: str
name:
description:
- The name of the web acl.
type: str
scope:
description:
- Scope of waf
choices: ["CLOUDFRONT","REGIONAL"]
type: str
arn:
description:
- AWS resources (ALB, API Gateway or AppSync GraphQL API) ARN
type: str
required: true
extends_documentation_fragment:
- amazon.aws.aws
- amazon.aws.ec2
'''

EXAMPLES = '''
- name: add test alb to waf string03
community.aws.wafv2_resources:
name: string03
scope: REGIONAL
state: present
arn: "arn:aws:elasticloadbalancing:eu-central-1:111111111:loadbalancer/app/test03/dd83ea041ba6f933"
'''

RETURN = """
resource_arns:
description: Current resources where the wafv2 is applied on
sample:
- "arn:aws:elasticloadbalancing:eu-central-1:111111111:loadbalancer/app/test03/dd83ea041ba6f933"
returned: Always, as long as the wafv2 exists
type: list
"""
from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule, is_boto3_error_code, get_boto3_client_method_parameters
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict, ansible_dict_to_boto3_tag_list
from ansible_collections.community.aws.plugins.module_utils.wafv2 import wafv2_list_web_acls

try:
from botocore.exceptions import ClientError, BotoCoreError
except ImportError:
pass # caught by AnsibleAWSModule


def get_web_acl(wafv2, name, scope, id, fail_json_aws):
try:
response = wafv2.get_web_acl(
Name=name,
Scope=scope,
Id=id
)
except (BotoCoreError, ClientError) as e:
fail_json_aws(e, msg="Failed to get wafv2 web acl.")
return response


def list_wafv2_resources(wafv2, arn, fail_json_aws):
try:
response = wafv2.list_resources_for_web_acl(
WebACLArn=arn
)
except (BotoCoreError, ClientError) as e:
fail_json_aws(e, msg="Failed to list wafv2 web acl.")
return response


def add_wafv2_resources(wafv2, waf_arn, arn, fail_json_aws):
try:
response = wafv2.associate_web_acl(
WebACLArn=waf_arn,
ResourceArn=arn
)
except (BotoCoreError, ClientError) as e:
fail_json_aws(e, msg="Failed to add wafv2 web acl.")
return response


def remove_resources(wafv2, arn, fail_json_aws):
try:
response = wafv2.disassociate_web_acl(
ResourceArn=arn
)
except (BotoCoreError, ClientError) as e:
fail_json_aws(e, msg="Failed to remove wafv2 web acl.")
return response


def main():

arg_spec = dict(
state=dict(type='str', required=True, choices=['present', 'absent']),
name=dict(type='str'),
scope=dict(type='str', choices=['CLOUDFRONT', 'REGIONAL']),
arn=dict(type='str', required=True)
)

module = AnsibleAWSModule(
argument_spec=arg_spec,
supports_check_mode=True,
required_if=[['state', 'present', ['name', 'scope']]]
)

state = module.params.get("state")
name = module.params.get("name")
scope = module.params.get("scope")
arn = module.params.get("arn")
check_mode = module.check_mode

wafv2 = module.client('wafv2')

# check if web acl exists

response = wafv2_list_web_acls(wafv2, scope, module.fail_json_aws)

id = None
retval = {}
change = False

for item in response.get('WebACLs'):
if item.get('Name') == name:
id = item.get('Id')

if id:
existing_acl = get_web_acl(wafv2, name, scope, id, module.fail_json_aws)
waf_arn = existing_acl.get('WebACL').get('ARN')

retval = list_wafv2_resources(wafv2, waf_arn, module.fail_json_aws)

if state == 'present':
if retval:
if arn not in retval.get('ResourceArns'):
change = True
if not check_mode:
retval = add_wafv2_resources(wafv2, waf_arn, arn, module.fail_json_aws)

elif state == 'absent':
if retval:
if arn in retval.get('ResourceArns'):
change = True
if not check_mode:
retval = remove_resources(wafv2, arn, module.fail_json_aws)

module.exit_json(changed=change, **camel_dict_to_snake_dict(retval))


if __name__ == '__main__':
main()
Loading

0 comments on commit 2ee7d6f

Please sign in to comment.