-
Notifications
You must be signed in to change notification settings - Fork 398
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #450 from markuman/wafv2
add wafv2 modules Reviewed-by: https://github.com/apps/ansible-zuul
- Loading branch information
Showing
16 changed files
with
3,003 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Oops, something went wrong.