[cloud][aws] New module: aws_waf_rule module (#33124)
Add a new module for managing AWS WAF rules Preceded by aws_waf_condition and to be succeeded by aws_waf_web_acl
This commit is contained in:
parent
69cd705634
commit
4e30eff651
2 changed files with 440 additions and 0 deletions
316
lib/ansible/modules/cloud/amazon/aws_waf_rule.py
Normal file
316
lib/ansible/modules/cloud/amazon/aws_waf_rule.py
Normal file
|
@ -0,0 +1,316 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright (c) 2017 Will Thames
|
||||
# Copyright (c) 2015 Mike Mochan
|
||||
# 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_rule
|
||||
short_description: create and delete WAF Rules
|
||||
description:
|
||||
- Read the AWS documentation for WAF
|
||||
U(https://aws.amazon.com/documentation/waf/)
|
||||
version_added: "2.5"
|
||||
|
||||
author:
|
||||
- Mike Mochan (@mmochan)
|
||||
- Will Thames (@willthames)
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
options:
|
||||
name:
|
||||
description: Name of the Web Application Firewall rule
|
||||
required: yes
|
||||
metric_name:
|
||||
description:
|
||||
- A friendly name or description for the metrics for the rule
|
||||
- The name can contain only alphanumeric characters (A-Z, a-z, 0-9); the name can't contain whitespace.
|
||||
- You can't change metric_name after you create the rule
|
||||
- Defaults to the same as name with disallowed characters removed
|
||||
state:
|
||||
description: whether the rule should be present or absent
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
conditions:
|
||||
description: >
|
||||
list of conditions used in the rule. Each condition should
|
||||
contain I(type): which is one of [C(byte), C(geo), C(ip), C(size), C(sql) or C(xss)]
|
||||
I(negated): whether the condition should be negated, and C(condition),
|
||||
the name of the existing condition. M(aws_waf_condition) can be used to
|
||||
create new conditions
|
||||
purge_conditions:
|
||||
description:
|
||||
- Whether or not to remove conditions that are not passed when updating `conditions`.
|
||||
Defaults to false.
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
||||
- name: create WAF rule
|
||||
aws_waf_rule:
|
||||
name: my_waf_rule
|
||||
conditions:
|
||||
- name: my_regex_condition
|
||||
type: regex
|
||||
negated: no
|
||||
- name: my_geo_condition
|
||||
type: geo
|
||||
negated: no
|
||||
- name: my_byte_condition
|
||||
type: byte
|
||||
negated: yes
|
||||
|
||||
- name: remove WAF rule
|
||||
aws_waf_rule:
|
||||
name: "my_waf_rule"
|
||||
state: absent
|
||||
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
rule:
|
||||
description: WAF rule contents
|
||||
returned: always
|
||||
type: complex
|
||||
contains:
|
||||
metric_name:
|
||||
description: Metric name for the rule
|
||||
returned: always
|
||||
type: string
|
||||
sample: ansibletest1234rule
|
||||
name:
|
||||
description: Friendly name for the rule
|
||||
returned: always
|
||||
type: string
|
||||
sample: ansible-test-1234_rule
|
||||
predicates:
|
||||
description: List of conditions used in the rule
|
||||
returned: always
|
||||
type: complex
|
||||
contains:
|
||||
data_id:
|
||||
description: ID of the condition
|
||||
returned: always
|
||||
type: string
|
||||
sample: 8251acdb-526c-42a8-92bc-d3d13e584166
|
||||
negated:
|
||||
description: Whether the sense of the condition is negated
|
||||
returned: always
|
||||
type: bool
|
||||
sample: false
|
||||
type:
|
||||
description: type of the condition
|
||||
returned: always
|
||||
type: string
|
||||
sample: ByteMatch
|
||||
rule_id:
|
||||
description: ID of the WAF rule
|
||||
returned: always
|
||||
type: string
|
||||
sample: 15de0cbc-9204-4e1f-90e6-69b2f415c261
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
try:
|
||||
import botocore
|
||||
except ImportError:
|
||||
pass # handled by AnsibleAWSModule
|
||||
|
||||
from ansible.module_utils.aws.core import AnsibleAWSModule
|
||||
from ansible.module_utils.ec2 import boto3_conn, get_aws_connection_info, ec2_argument_spec
|
||||
from ansible.module_utils.ec2 import camel_dict_to_snake_dict
|
||||
from ansible.module_utils.aws.waf import get_change_token, list_rules_with_backoff, MATCH_LOOKUP
|
||||
from ansible.module_utils.aws.waf import get_web_acl_with_backoff, list_web_acls_with_backoff
|
||||
|
||||
|
||||
def get_rule_by_name(client, module, name):
|
||||
rules = [d['RuleId'] for d in list_rules(client, module) if d['Name'] == name]
|
||||
if rules:
|
||||
return rules[0]
|
||||
|
||||
|
||||
def get_rule(client, module, rule_id):
|
||||
try:
|
||||
return client.get_rule(RuleId=rule_id)['Rule']
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||
module.fail_json_aws(e, msg='Could not get WAF rule')
|
||||
|
||||
|
||||
def list_rules(client, module):
|
||||
try:
|
||||
return list_rules_with_backoff(client)
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||
module.fail_json_aws(e, msg='Could not list WAF rules')
|
||||
|
||||
|
||||
def find_and_update_rule(client, module, rule_id):
|
||||
rule = get_rule(client, module, rule_id)
|
||||
rule_id = rule['RuleId']
|
||||
|
||||
existing_conditions = dict((condition_type, dict()) for condition_type in MATCH_LOOKUP)
|
||||
desired_conditions = dict((condition_type, dict()) for condition_type in MATCH_LOOKUP)
|
||||
all_conditions = dict()
|
||||
|
||||
for condition_type in MATCH_LOOKUP:
|
||||
method = 'list_' + MATCH_LOOKUP[condition_type]['method'] + 's'
|
||||
all_conditions[condition_type] = dict()
|
||||
try:
|
||||
paginator = client.get_paginator(method)
|
||||
func = paginator.paginate().build_full_result
|
||||
except (KeyError, botocore.exceptions.OperationNotPageableError):
|
||||
# list_geo_match_sets and list_regex_match_sets do not have a paginator
|
||||
# and throw different exceptions
|
||||
func = getattr(client, method)
|
||||
try:
|
||||
pred_results = func()[MATCH_LOOKUP[condition_type]['conditionset'] + 's']
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||
module.fail_json_aws(e, msg='Could not list %s conditions' % condition_type)
|
||||
for pred in pred_results:
|
||||
pred['DataId'] = pred[MATCH_LOOKUP[condition_type]['conditionset'] + 'Id']
|
||||
all_conditions[condition_type][pred['Name']] = camel_dict_to_snake_dict(pred)
|
||||
all_conditions[condition_type][pred['DataId']] = camel_dict_to_snake_dict(pred)
|
||||
|
||||
for condition in module.params['conditions']:
|
||||
desired_conditions[condition['type']][condition['name']] = condition
|
||||
|
||||
reverse_condition_types = dict((v['type'], k) for (k, v) in MATCH_LOOKUP.items())
|
||||
for condition in rule['Predicates']:
|
||||
existing_conditions[reverse_condition_types[condition['Type']]][condition['DataId']] = camel_dict_to_snake_dict(condition)
|
||||
|
||||
insertions = list()
|
||||
deletions = list()
|
||||
|
||||
for condition_type in desired_conditions:
|
||||
for (condition_name, condition) in desired_conditions[condition_type].items():
|
||||
if condition_name not in all_conditions[condition_type]:
|
||||
module.fail_json(msg="Condition %s of type %s does not exist" % (condition_name, condition_type))
|
||||
condition['data_id'] = all_conditions[condition_type][condition_name]['data_id']
|
||||
if condition['data_id'] not in existing_conditions[condition_type]:
|
||||
insertions.append(format_for_insertion(condition))
|
||||
|
||||
if module.params['purge_conditions']:
|
||||
for condition_type in existing_conditions:
|
||||
deletions.extend([format_for_deletion(condition) for condition in existing_conditions[condition_type].values()
|
||||
if not all_conditions[condition_type][condition['data_id']]['name'] in desired_conditions[condition_type]])
|
||||
|
||||
changed = bool(insertions or deletions)
|
||||
if changed:
|
||||
try:
|
||||
client.update_rule(RuleId=rule_id, ChangeToken=get_change_token(client, module),
|
||||
Updates=insertions + deletions)
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||
module.fail_json_aws(e, msg='Could not update rule conditions')
|
||||
|
||||
return changed, get_rule(client, module, rule_id)
|
||||
|
||||
|
||||
def format_for_insertion(condition):
|
||||
return dict(Action='INSERT',
|
||||
Predicate=dict(Negated=condition['negated'],
|
||||
Type=MATCH_LOOKUP[condition['type']]['type'],
|
||||
DataId=condition['data_id']))
|
||||
|
||||
|
||||
def format_for_deletion(condition):
|
||||
return dict(Action='DELETE',
|
||||
Predicate=dict(Negated=condition['negated'],
|
||||
Type=condition['type'],
|
||||
DataId=condition['data_id']))
|
||||
|
||||
|
||||
def remove_rule_conditions(client, module, rule_id):
|
||||
conditions = get_rule(client, module, rule_id)['Predicates']
|
||||
updates = [format_for_deletion(camel_dict_to_snake_dict(condition)) for condition in conditions]
|
||||
try:
|
||||
client.update_rule(RuleId=rule_id,
|
||||
ChangeToken=get_change_token(client, module), Updates=updates)
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||
module.fail_json_aws(e, msg='Could not remove rule conditions')
|
||||
|
||||
|
||||
def ensure_rule_present(client, module):
|
||||
name = module.params['name']
|
||||
rule_id = get_rule_by_name(client, module, name)
|
||||
params = dict()
|
||||
if rule_id:
|
||||
return find_and_update_rule(client, module, rule_id)
|
||||
else:
|
||||
params['Name'] = module.params['name']
|
||||
metric_name = module.params['metric_name']
|
||||
if not metric_name:
|
||||
metric_name = re.sub(r'[^a-zA-Z0-9]', '', module.params['name'])
|
||||
params['MetricName'] = metric_name
|
||||
params['ChangeToken'] = get_change_token(client, module)
|
||||
try:
|
||||
new_rule = client.create_rule(**params)['Rule']
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||
module.fail_json_aws(e, msg='Could not create rule')
|
||||
return find_and_update_rule(client, module, new_rule['RuleId'])
|
||||
|
||||
|
||||
def find_rule_in_web_acls(client, module, rule_id):
|
||||
web_acls_in_use = []
|
||||
try:
|
||||
all_web_acls = list_web_acls_with_backoff(client)
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||
module.fail_json_aws(e, msg='Could not list Web ACLs')
|
||||
for web_acl in all_web_acls:
|
||||
try:
|
||||
web_acl_details = get_web_acl_with_backoff(client, web_acl['WebACLId'])
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||
module.fail_json_aws(e, msg='Could not get Web ACL details')
|
||||
if rule_id in [rule['RuleId'] for rule in web_acl_details['Rules']]:
|
||||
web_acls_in_use.append(web_acl_details['Name'])
|
||||
return web_acls_in_use
|
||||
|
||||
|
||||
def ensure_rule_absent(client, module):
|
||||
rule_id = get_rule_by_name(client, module, module.params['name'])
|
||||
in_use_web_acls = find_rule_in_web_acls(client, module, rule_id)
|
||||
if in_use_web_acls:
|
||||
web_acl_names = ', '.join(in_use_web_acls)
|
||||
module.fail_json(msg="Rule %s is in use by Web ACL(s) %s" %
|
||||
(module.params['name'], web_acl_names))
|
||||
if rule_id:
|
||||
remove_rule_conditions(client, module, rule_id)
|
||||
try:
|
||||
return True, client.delete_rule(RuleId=rule_id, ChangeToken=get_change_token(client, module))
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||
module.fail_json_aws(e, msg='Could not delete rule')
|
||||
return False, {}
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
name=dict(required=True),
|
||||
metric_name=dict(),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
conditions=dict(type='list'),
|
||||
purge_conditions=dict(type='bool', default=False)
|
||||
),
|
||||
)
|
||||
module = AnsibleAWSModule(argument_spec=argument_spec)
|
||||
state = module.params.get('state')
|
||||
|
||||
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)
|
||||
|
||||
if state == 'present':
|
||||
(changed, results) = ensure_rule_present(client, module)
|
||||
else:
|
||||
(changed, results) = ensure_rule_absent(client, module)
|
||||
|
||||
module.exit_json(changed=changed, rule=camel_dict_to_snake_dict(results))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -7,6 +7,11 @@
|
|||
security_token: "{{ security_token }}"
|
||||
no_log: yes
|
||||
|
||||
|
||||
##################################################
|
||||
# aws_waf_condition tests
|
||||
##################################################
|
||||
|
||||
- name: create WAF IP condition
|
||||
aws_waf_condition:
|
||||
name: "{{ resource_prefix }}_ip_condition"
|
||||
|
@ -228,10 +233,129 @@
|
|||
recreate_waf_regex_condition.condition.regex_match_tuples[0].regex_pattern_set_id !=
|
||||
create_waf_regex_condition.condition.regex_match_tuples[0].regex_pattern_set_id
|
||||
|
||||
##################################################
|
||||
# aws_waf_rule tests
|
||||
##################################################
|
||||
|
||||
- name: create WAF rule
|
||||
aws_waf_rule:
|
||||
name: "{{ resource_prefix }}_rule"
|
||||
conditions:
|
||||
- name: "{{ resource_prefix }}_regex_condition"
|
||||
type: regex
|
||||
negated: no
|
||||
- name: "{{ resource_prefix }}_geo_condition"
|
||||
type: geo
|
||||
negated: no
|
||||
- name: "{{ resource_prefix }}_byte_condition"
|
||||
type: byte
|
||||
negated: no
|
||||
purge_conditions: yes
|
||||
<<: *aws_connection_info
|
||||
register: create_aws_waf_rule
|
||||
|
||||
- name: check WAF rule
|
||||
assert:
|
||||
that:
|
||||
- create_aws_waf_rule.changed
|
||||
- create_aws_waf_rule.rule.predicates|length == 3
|
||||
|
||||
- name: recreate WAF rule
|
||||
aws_waf_rule:
|
||||
name: "{{ resource_prefix }}_rule"
|
||||
conditions:
|
||||
- name: "{{ resource_prefix }}_regex_condition"
|
||||
type: regex
|
||||
negated: no
|
||||
- name: "{{ resource_prefix }}_geo_condition"
|
||||
type: geo
|
||||
negated: no
|
||||
- name: "{{ resource_prefix }}_byte_condition"
|
||||
type: byte
|
||||
negated: no
|
||||
<<: *aws_connection_info
|
||||
register: create_aws_waf_rule
|
||||
|
||||
- name: check WAF rule did not change
|
||||
assert:
|
||||
that:
|
||||
- not create_aws_waf_rule.changed
|
||||
- create_aws_waf_rule.rule.predicates|length == 3
|
||||
|
||||
- name: add further WAF rules relying on purge_conditions defaulting to false
|
||||
aws_waf_rule:
|
||||
name: "{{ resource_prefix }}_rule"
|
||||
conditions:
|
||||
- name: "{{ resource_prefix }}_ip_condition"
|
||||
type: ip
|
||||
negated: yes
|
||||
- name: "{{ resource_prefix }}_sql_condition"
|
||||
type: sql
|
||||
negated: no
|
||||
- name: "{{ resource_prefix }}_xss_condition"
|
||||
type: xss
|
||||
negated: no
|
||||
<<: *aws_connection_info
|
||||
register: add_conditions_to_aws_waf_rule
|
||||
|
||||
- name: check WAF rule added rules
|
||||
assert:
|
||||
that:
|
||||
- add_conditions_to_aws_waf_rule.changed
|
||||
- add_conditions_to_aws_waf_rule.rule.predicates|length == 6
|
||||
|
||||
- name: remove some rules through purging conditions
|
||||
aws_waf_rule:
|
||||
name: "{{ resource_prefix }}_rule"
|
||||
conditions:
|
||||
- name: "{{ resource_prefix }}_ip_condition"
|
||||
type: ip
|
||||
negated: yes
|
||||
- name: "{{ resource_prefix }}_xss_condition"
|
||||
type: xss
|
||||
negated: no
|
||||
- name: "{{ resource_prefix }}_byte_condition"
|
||||
type: byte
|
||||
negated: no
|
||||
- name: "{{ resource_prefix }}_size_condition"
|
||||
type: size
|
||||
negated: no
|
||||
purge_conditions: yes
|
||||
<<: *aws_connection_info
|
||||
register: add_and_remove_waf_rule_conditions
|
||||
|
||||
- name: check WAF rules were updated as expected
|
||||
assert:
|
||||
that:
|
||||
- add_and_remove_waf_rule_conditions.changed
|
||||
- add_and_remove_waf_rule_conditions.rule.predicates|length == 4
|
||||
|
||||
- name: attempt to remove an in use condition
|
||||
aws_waf_condition:
|
||||
name: "{{ resource_prefix }}_size_condition"
|
||||
type: size
|
||||
state: absent
|
||||
<<: *aws_connection_info
|
||||
ignore_errors: yes
|
||||
register: remove_in_use_condition
|
||||
|
||||
- name: check failure was sensible
|
||||
assert:
|
||||
that:
|
||||
- remove_in_use_condition.failed
|
||||
- "'Condition {{ resource_prefix }}_size_condition is in use' in remove_in_use_condition.msg"
|
||||
|
||||
always:
|
||||
- debug:
|
||||
msg: "****** TEARDOWN STARTS HERE ******"
|
||||
|
||||
- name: remove WAF rule
|
||||
aws_waf_rule:
|
||||
name: "{{ resource_prefix }}_rule"
|
||||
state: absent
|
||||
<<: *aws_connection_info
|
||||
ignore_errors: yes
|
||||
|
||||
- name: remove XSS condition
|
||||
aws_waf_condition:
|
||||
name: "{{ resource_prefix }}_xss_condition"
|
||||
|
|
Loading…
Reference in a new issue