From 77f5a8f422984993d247dbdb52805223a2ab90d1 Mon Sep 17 00:00:00 2001 From: Dennis Conrad Date: Tue, 1 May 2018 21:04:03 +0100 Subject: [PATCH] Add AWS Inspector Target Module (#37464) * Add AWS Inspector Target Module * "ansible-test sanity" Fixes * * Rename module * Add integration test * Incorporate feedback from s-hertel --- .../cloud/amazon/aws_inspector_target.py | 246 ++++++++++++++++++ .../integration/targets/aws_inspector/aliases | 2 + .../targets/aws_inspector/defaults/main.yml | 3 + .../targets/aws_inspector/tasks/main.yml | 96 +++++++ 4 files changed, 347 insertions(+) create mode 100644 lib/ansible/modules/cloud/amazon/aws_inspector_target.py create mode 100644 test/integration/targets/aws_inspector/aliases create mode 100644 test/integration/targets/aws_inspector/defaults/main.yml create mode 100644 test/integration/targets/aws_inspector/tasks/main.yml diff --git a/lib/ansible/modules/cloud/amazon/aws_inspector_target.py b/lib/ansible/modules/cloud/amazon/aws_inspector_target.py new file mode 100644 index 00000000000..2af37e2250d --- /dev/null +++ b/lib/ansible/modules/cloud/amazon/aws_inspector_target.py @@ -0,0 +1,246 @@ +#!/usr/bin/python +# Copyright (c) 2018 Dennis Conrad for Sainsbury's +# 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_inspector_target +short_description: Create, Update and Delete Amazon Inspector Assessment + Targets +description: Creates, updates, or deletes Amazon Inspector Assessment Targets + and manages the required Resource Groups. +version_added: "2.6" +author: "Dennis Conrad (@dennisconrad)" +options: + name: + description: + - The user-defined name that identifies the assessment target. The name + must be unique within the AWS account. + required: true + state: + description: + - The state of the assessment target. + choices: + - absent + - present + default: present + tags: + description: + - Tags of the EC2 instances to be added to the assessment target. + - Required if C(state=present). +extends_documentation_fragment: + - aws + - ec2 +requirements: + - boto3 + - botocore +''' + +EXAMPLES = ''' +- name: Create my_target Assessment Target + aws_inspector_target: + name: my_target + tags: + role: scan_target + +- name: Update Existing my_target Assessment Target with Additional Tags + aws_inspector_target: + name: my_target + tags: + env: dev + role: scan_target + +- name: Delete my_target Assessment Target + aws_inspector_target: + name: my_target + state: absent +''' + +RETURN = ''' +arn: + description: The ARN that specifies the Amazon Inspector assessment target. + returned: success + type: string + sample: "arn:aws:inspector:eu-west-1:123456789012:target/0-O4LnL7n1" +created_at: + description: The time at which the assessment target was created. + returned: success + type: string + sample: "2018-01-29T13:48:51.958000+00:00" +name: + description: The name of the Amazon Inspector assessment target. + returned: success + type: string + sample: "my_target" +resource_group_arn: + description: The ARN that specifies the resource group that is associated + with the assessment target. + returned: success + type: string + sample: "arn:aws:inspector:eu-west-1:123456789012:resourcegroup/0-qY4gDel8" +tags: + description: The tags of the resource group that is associated with the + assessment target. + returned: success + type: list + sample: {"role": "scan_target", "env": "dev"} +updated_at: + description: The time at which the assessment target was last updated. + returned: success + type: string + sample: "2018-01-29T13:48:51.958000+00:00" +''' + +from ansible.module_utils.aws.core import AnsibleAWSModule +from ansible.module_utils.ec2 import AWSRetry +from ansible.module_utils.ec2 import ( + HAS_BOTO3, + ansible_dict_to_boto3_tag_list, + boto3_tag_list_to_ansible_dict, + camel_dict_to_snake_dict, + compare_aws_tags, +) + +try: + import botocore +except ImportError: + pass # caught by imported HAS_BOTO3 + + +@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +def main(): + argument_spec = dict( + name=dict(required=True), + state=dict(choices=['absent', 'present'], default='present'), + tags=dict(type='dict'), + ) + + required_if = [['state', 'present', ['tags']]] + + module = AnsibleAWSModule( + argument_spec=argument_spec, + supports_check_mode=False, + required_if=required_if, + ) + + if not HAS_BOTO3: + module.fail_json(msg='boto3 and botocore are required for this module') + + name = module.params.get('name') + state = module.params.get('state').lower() + tags = module.params.get('tags') + if tags: + tags = ansible_dict_to_boto3_tag_list(tags, 'key', 'value') + + client = module.client('inspector') + + try: + existing_target_arn = client.list_assessment_targets( + filter={'assessmentTargetNamePattern': name}, + ).get('assessmentTargetArns')[0] + + existing_target = camel_dict_to_snake_dict( + client.describe_assessment_targets( + assessmentTargetArns=[existing_target_arn], + ).get('assessmentTargets')[0] + ) + + existing_resource_group_arn = existing_target.get('resource_group_arn') + existing_resource_group_tags = client.describe_resource_groups( + resourceGroupArns=[existing_resource_group_arn], + ).get('resourceGroups')[0].get('tags') + + target_exists = True + except ( + botocore.exceptions.BotoCoreError, + botocore.exceptions.ClientError, + ) as e: + module.fail_json_aws(e, msg="trying to retrieve targets") + except IndexError: + target_exists = False + + if state == 'present' and target_exists: + ansible_dict_tags = boto3_tag_list_to_ansible_dict(tags) + ansible_dict_existing_tags = boto3_tag_list_to_ansible_dict( + existing_resource_group_tags + ) + tags_to_add, tags_to_remove = compare_aws_tags( + ansible_dict_tags, + ansible_dict_existing_tags + ) + if not (tags_to_add or tags_to_remove): + existing_target.update({'tags': ansible_dict_existing_tags}) + module.exit_json(changed=False, **existing_target) + else: + try: + updated_resource_group_arn = client.create_resource_group( + resourceGroupTags=tags, + ).get('resourceGroupArn') + + client.update_assessment_target( + assessmentTargetArn=existing_target_arn, + assessmentTargetName=name, + resourceGroupArn=updated_resource_group_arn, + ) + + updated_target = camel_dict_to_snake_dict( + client.describe_assessment_targets( + assessmentTargetArns=[existing_target_arn], + ).get('assessmentTargets')[0] + ) + + updated_target.update({'tags': ansible_dict_tags}) + module.exit_json(changed=True, **updated_target), + except ( + botocore.exceptions.BotoCoreError, + botocore.exceptions.ClientError, + ) as e: + module.fail_json_aws(e, msg="trying to update target") + + elif state == 'present' and not target_exists: + try: + new_resource_group_arn = client.create_resource_group( + resourceGroupTags=tags, + ).get('resourceGroupArn') + + new_target_arn = client.create_assessment_target( + assessmentTargetName=name, + resourceGroupArn=new_resource_group_arn, + ).get('assessmentTargetArn') + + new_target = camel_dict_to_snake_dict( + client.describe_assessment_targets( + assessmentTargetArns=[new_target_arn], + ).get('assessmentTargets')[0] + ) + + new_target.update({'tags': boto3_tag_list_to_ansible_dict(tags)}) + module.exit_json(changed=True, **new_target) + except ( + botocore.exceptions.BotoCoreError, + botocore.exceptions.ClientError, + ) as e: + module.fail_json_aws(e, msg="trying to create target") + + elif state == 'absent' and target_exists: + try: + client.delete_assessment_target( + assessmentTargetArn=existing_target_arn, + ) + module.exit_json(changed=True) + except ( + botocore.exceptions.BotoCoreError, + botocore.exceptions.ClientError, + ) as e: + module.fail_json_aws(e, msg="trying to delete target") + + elif state == 'absent' and not target_exists: + module.exit_json(changed=False) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/aws_inspector/aliases b/test/integration/targets/aws_inspector/aliases new file mode 100644 index 00000000000..9e624ab960b --- /dev/null +++ b/test/integration/targets/aws_inspector/aliases @@ -0,0 +1,2 @@ +cloud/aws +posix/ci/cloud/group3/aws diff --git a/test/integration/targets/aws_inspector/defaults/main.yml b/test/integration/targets/aws_inspector/defaults/main.yml new file mode 100644 index 00000000000..8777873f078 --- /dev/null +++ b/test/integration/targets/aws_inspector/defaults/main.yml @@ -0,0 +1,3 @@ +--- + +aws_inspector_scan_name: "aws_inspector_scan-{{ ansible_date_time.epoch }}" diff --git a/test/integration/targets/aws_inspector/tasks/main.yml b/test/integration/targets/aws_inspector/tasks/main.yml new file mode 100644 index 00000000000..36a3cfca9e7 --- /dev/null +++ b/test/integration/targets/aws_inspector/tasks/main.yml @@ -0,0 +1,96 @@ +--- + +- name: Set Connexion Information for All Tasks + set_fact: + aws_connection_info: &aws_connection_info + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_token: "{{ security_token }}" + region: "{{ aws_region }}" + no_log: yes + +- block: + - name: Create AWS Inspector Target Group + aws_inspector_target: + name: "{{ aws_inspector_scan_name }}" + state: present + tags: + Name: "{{ aws_inspector_scan_name }}" + changed: "no" + <<: *aws_connection_info + register: target_group_create + + - name: Create AWS Inspector Target Group (Verify) + aws_inspector_target: + name: "{{ aws_inspector_scan_name }}" + state: present + tags: + Name: "{{ aws_inspector_scan_name }}" + changed: "no" + <<: *aws_connection_info + register: target_group_create_verify + + - name: Assert Successful AWS Inspector Target Group Creation + assert: + that: + - target_group_create is changed + - target_group_create.name == aws_inspector_scan_name + - target_group_create.tags.Name == aws_inspector_scan_name + - target_group_create.tags.changed == "no" + - target_group_create_verify is not changed + - target_group_create_verify.name == aws_inspector_scan_name + - target_group_create_verify.tags.Name == aws_inspector_scan_name + - target_group_create_verify.tags.changed == "no" + + - name: Change AWS Inspector Target Group Tags + aws_inspector_target: + name: "{{ aws_inspector_scan_name }}" + state: present + tags: + Name: "{{ aws_inspector_scan_name }}" + changed: "yes" + <<: *aws_connection_info + register: target_group_tag_change + + - name: Change AWS Inspector Target Group Tags (Verify) + aws_inspector_target: + name: "{{ aws_inspector_scan_name }}" + state: present + tags: + Name: "{{ aws_inspector_scan_name }}" + changed: "yes" + <<: *aws_connection_info + register: target_group_tag_change_verify + + - name: Assert Successful AWS Inspector Target Group Tag Change + assert: + that: + - target_group_tag_change is changed + - target_group_tag_change.name == aws_inspector_scan_name + - target_group_tag_change.tags.Name == aws_inspector_scan_name + - target_group_tag_change.tags.changed == "yes" + - target_group_tag_change_verify is not changed + - target_group_tag_change_verify.name == aws_inspector_scan_name + - target_group_tag_change_verify.tags.Name == aws_inspector_scan_name + - target_group_tag_change_verify.tags.changed == "yes" + + always: + - name: Delete AWS Inspector Target Group + aws_inspector_target: + name: "{{ aws_inspector_scan_name }}" + state: absent + <<: *aws_connection_info + register: target_group_delete + + - name: Delete AWS Inspector Target Group (Verify) + aws_inspector_target: + name: "{{ aws_inspector_scan_name }}" + state: absent + <<: *aws_connection_info + register: target_group_delete_verify + + - name: Assert Successful AWS Inspector Target Group Deletion + assert: + that: + - target_group_delete is changed + - target_group_delete_verify is not changed