From fbfa5624c339562f81263fc3ecddf3c9bc0edbff Mon Sep 17 00:00:00 2001 From: Will Thames Date: Thu, 24 Aug 2017 05:09:27 +1000 Subject: [PATCH] [cloud] New module: aws_waf_facts module (#26671) Initial implementation of waf_facts module * Enhance waf_facts module to provide more info Support check_mode trivially Enhance rule and predicate information Use AWSretry and wrap proper exception handling Finish documentation Remove arbitrary limits Meet latest ansible standards. * Rename module to use aws_ prefix. Fix copyright. Fix metadata version. --- .../modules/cloud/amazon/aws_waf_facts.py | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 lib/ansible/modules/cloud/amazon/aws_waf_facts.py diff --git a/lib/ansible/modules/cloud/amazon/aws_waf_facts.py b/lib/ansible/modules/cloud/amazon/aws_waf_facts.py new file mode 100644 index 0000000000..98c9b84621 --- /dev/null +++ b/lib/ansible/modules/cloud/amazon/aws_waf_facts.py @@ -0,0 +1,232 @@ +#!/usr/bin/python +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +module: aws_waf_facts +short_description: Retrieve facts for WAF ACLs, Rule , Conditions and Filters. +description: + - Retrieve facts for WAF ACLs, Rule , Conditions and Filters. +version_added: "2.4" +options: + name: + description: + - The name of a Web Application Firewall + +author: + - Mike Mochan (@mmochan) + - Will Thames (@willthames) +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +- name: obtain all WAF facts + aws_waf_facts: + +- name: obtain all facts for a single WAF + aws_waf_facts: + name: test_waf +''' + +RETURN = ''' +wafs: + description: The WAFs that match the passed arguments + returned: success + type: complex + contains: + name: + description: A friendly name or description of the WebACL + returned: always + type: string + sample: test_waf + default_action: + description: The action to perform if none of the Rules contained in the WebACL match. + returned: always + type: int + sample: BLOCK + metric_name: + description: A friendly name or description for the metrics for this WebACL + returned: always + type: string + sample: test_waf_metric + rules: + description: An array that contains the action for each Rule in a WebACL , the priority of the Rule + returned: always + type: complex + contains: + action: + description: The action to perform if the Rule matches + returned: always + type: string + sample: BLOCK + metric_name: + description: A friendly name or description for the metrics for this Rule + returned: always + type: string + sample: ipblockrule + name: + description: A friendly name or description of the Rule + returned: always + type: string + sample: ip_block_rule + predicates: + description: The Predicates list contains a Predicate for each + ByteMatchSet, IPSet, SizeConstraintSet, SqlInjectionMatchSet or XssMatchSet + object in a Rule + returned: always + type: list + sample: + [ + { + "byte_match_set_id": "47b822b5-abcd-1234-faaf-1234567890", + "byte_match_tuples": [ + { + "field_to_match": { + "type": "QUERY_STRING" + }, + "positional_constraint": "STARTS_WITH", + "target_string": "bobbins", + "text_transformation": "NONE" + } + ], + "name": "bobbins", + "negated": false, + "type": "ByteMatch" + } + ] +''' + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info +from ansible.module_utils.ec2 import camel_dict_to_snake_dict, HAS_BOTO3, AWSRetry + + +try: + import botocore +except ImportError: + pass # caught by imported HAS_BOTO3 + + +@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +def get_rule_with_backoff(client, rule_id): + return client.get_rule(RuleId=rule_id) + + +@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +def get_byte_match_set_with_backoff(client, byte_match_set_id): + return client.get_byte_match_set(ByteMatchSetId=byte_match_set_id)['ByteMatchSet'] + + +@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +def get_ip_set_with_backoff(client, ip_set_id): + return client.get_ip_set(IPSetId=ip_set_id)['IPSet'] + + +@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +def get_size_constraint_set_with_backoff(client, size_constraint_set_id): + return client.get_size_constraint_set(SizeConstraintSetId=size_constraint_set_id)['SizeConstraintSet'] + + +@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +def get_sql_injection_match_set_with_backoff(client, sql_injection_match_set_id): + return client.get_sql_injection_match_set(SqlInjectionMatchSetId=sql_injection_match_set_id)['SqlInjectionMatchSet'] + + +@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +def get_xss_match_set_with_backoff(client, xss_match_set_id): + return client.get_xss_match_set(XssMatchSetId=xss_match_set_id)['XssMatchSet'] + + +def get_rule(client, rule_id): + rule = get_rule_with_backoff(client, rule_id)['Rule'] + match_sets = { + 'ByteMatch': get_byte_match_set_with_backoff, + 'IPMatch': get_ip_set_with_backoff, + 'SizeConstraint': get_size_constraint_set_with_backoff, + 'SqlInjectionMatch': get_sql_injection_match_set_with_backoff, + 'XssMatch': get_xss_match_set_with_backoff + } + if 'Predicates' in rule: + for predicate in rule['Predicates']: + if predicate['Type'] in match_sets: + predicate.update(match_sets[predicate['Type']](client, predicate['DataId'])) + # replaced by Id from the relevant MatchSet + del(predicate['DataId']) + return rule + + +@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +def get_web_acl_with_backoff(client, web_acl_id): + return client.get_web_acl(WebACLId=web_acl_id) + + +def get_web_acl(client, module, web_acl_id): + try: + web_acl = get_web_acl_with_backoff(client, web_acl_id) + except botocore.exceptions.ClientError as e: + module.fail_json(msg="Couldn't obtain web acl", + exception=traceback.format_exc(), + **camel_dict_to_snake_dict(e.response)) + + if web_acl['WebACL']: + try: + for rule in web_acl['WebACL']['Rules']: + rule.update(get_rule(client, rule['RuleId'])) + except botocore.exceptions.ClientError as e: + module.fail_json(msg="Couldn't obtain web acl rule", + exception=traceback.format_exc(), + **camel_dict_to_snake_dict(e.response)) + return camel_dict_to_snake_dict(web_acl['WebACL']) + + +@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +def list_web_acls_with_backoff(client): + paginator = client.get_paginator('list_web_acls') + return paginator.paginate().build_full_result()['WebACLs'] + + +def list_web_acls(client, module): + try: + return list_web_acls_with_backoff(client) + except botocore.exceptions.ClientError as e: + module.fail_json(msg="Couldn't obtain web acls", + exception=traceback.format_exc(), + **camel_dict_to_snake_dict(e.response)) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + name=dict(required=False), + ) + ) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_BOTO3: + module.fail_json(msg='boto3 and botocore are required.') + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + client = boto3_conn(module, conn_type='client', resource='waf', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except botocore.exceptions.NoCredentialsError as e: + module.fail_json(msg="Can't authorize connection - " + str(e)) + + web_acls = list_web_acls(client, module) + name = module.params['name'] + if name: + web_acls = [web_acl for web_acl in web_acls if + web_acl['Name'] == name] + if not web_acls: + module.fail_json(msg="WAF named %s not found" % name) + module.exit_json(wafs=[get_web_acl(client, module, web_acl['WebACLId']) + for web_acl in web_acls]) + + +if __name__ == '__main__': + main()