Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add wafv2 modules #450

Merged
merged 11 commits into from
Apr 21, 2021
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I suggest to add a unit-test for this function?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm basically it is already covered by the integration test.
I've added different rules circumstances that must be covered by those functions and that must not be covered by those functions.

Copy link
Member

@goneri goneri Apr 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I know and this is the reason why I put suggest in bold. PLEASE, Don't force yourself to do it!

I see two reasons to write unit-tests in your case:

  • sometime the behavior of the API change, it's a way to have a copy of the original expect input.
  • unit-tests run way way way way faster than integration tests. And if they fail we can configure the CI to avoid the functional tests. This way we save the valuable developer time and a lot of CI resources.
  • it's also rather easy for a developer to be able to run the unit-tests locally. It's a different story for the functional tests.

Well, that's 3 reasons actually ^^.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what it's worth you should be able to copy most of the unit-test framework for these transforms from https://github.com/ansible-collections/amazon.aws/blob/main/tests/unit/module_utils/core/test_scrub_none_parameters.py

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


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, unit-test would be great for the others too :-).

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